Elemente de Informatică Mobilă

Anunțuri

  • 19.11.2024 09:45 EC004 test parțial closed book din ce s-a discutat la curs: 2, 3, 4, 6
  • 26.09.2024 bine ați venit în semestrul 1, anul universitar 2024 - 2025

Resurse


Orar


Curs

SeriaZiOraSalaInstructor
C2 + opționalimarți ambele10.00-12.00EG301Dragoș
C2 + opționalijoi pare12.00-14.00PR002Dragoș

Laborator

GrupaZiOraSalaAsistent
OPT4marți08.00-10.00EG103bAna
341C2marți18.00-20.00EG103bCristian
OPT1miercuri08.00-10.00EG105Iulia
OPT2miercuri08.00-10.00EG206Ana
343C2miercuri12.00-14.00EG206Nic
341C2miercuri14.00-16.00EG206Nic
343C2miercuri16.00-18.00EG103bAlex
342C2joi14.00-16.00EG103bVlad
343C2joi16.00-18.00EG103bVlad/Corina
Kotlin PrjTBDTeamsBianca/Vlad/Nic/Dragoș

Săptămâna de laborator începe joi (14.00) și se termină miercuri (16.00).

Contribuții laborator

Oricine poate contribui pentru a îmbunătăți laboratoarele. Scheletul laboratoarelor se găsește pe Github. Textul laboratoarelor îl găsiti în acest repo. În partea de sus dreaptă a fiecărei pagini există un buton de edit pe care îl puteți folosi pentru a sugera modificări.

Introducere

Curs 01 - Introducere PPTX

Android Internals

Curs 02 - Android intro PPTX

Generalități despre radio

Curs 03 - Generalități despre radio PPTX

Curs 03 - performanța rețelelor PPTX

Acces la mediu

Curs 04 - Accesul la mediu PPTX

Exemplu Octave de generare coduri CDMA

Sisteme celulare

Curs 05 - Sisteme celulare PPTX

Rețele WiFi

Curs 06 - Rețele WiFi

Mobilitate la nivel rețea

Curs 07 - Mobilitate la nivel rețea

Mobilitate la nivel transport

Curs 08 - mobilitate la nivel transport

SIP și VoIP

Curs 09 - VoIP și SIP PPTX

Rețele prin satelit

Curs 10 - Rețele prin Satelit PPTX

Poziționare

Curs 11 - Poziționare PPTX

  • Material suplimentar -- prezentare cercetare din proiectul A-WEAR despre standarde si metode recente de poziționare. Nu se cere la examen.

Introducere în Programarea Android

În acest laborator vom vedea cum putem instala mediul de dezvoltare Android Studio, dezvolta și rula prima noastră aplicație Android.

Android

Android este un OS bazat pe o versiune modificată de Linux (pentru gestiunea componentelor hardware, a proceselor și a memoriei) și biblioteci Java (pentru telefonie (audio/video), conectivitate, grafică, programarea interfețelor cu utilizatorul). Modificarile aduse Linux sunt multe, printre care drivere noi , optimizari pentru a reduce consumul de energie, schimbari de arhitectura etc.

Este un produs open-source (putând fi dezvoltat de producătorii de dispozitive mobile cu extensii proprietare pentru a-și particulariza platforma), dezvoltat în prezent de Google.

În condițiile în care pe piața dispozitivelor mobile aplicațiile sunt cele care aduc avantajul competițional, beneficiul Android este reprezentat de abordarea unitară pentru dezvoltarea aplicațiilor. Cu alte cuvinte, o aplicație dezvoltată conform API-ului Android va putea rula pe mai multe dispozitive mobile pe care este instalat sistemul de operare respectiv.

Arhitectura Android

Arhitectura sistemului de operare Android are la baza kernel-ul Linux si aduce peste el mai multe modificari pentru a functiona pe un mediu constrans in primul rand de power consumption. Pe parcursul laboratoarelor vom vedea ce decizii de design au fost luate in dezvoltarea acestui OS si cum au fost acestea influentate de catre hardware.

Kernelul Linux (cu unele modificări) conține driver-ele pentru diferitele componente hardware (ecran, cameră foto, tastatură, antenă WiFi, memorie flash, dispozitive audio), fiind responsabil cu gestiunea proceselor, memoriei, perifericelor (audio/video, GPS, WiFi), dispozitivelor de intrare/ieșire, rețelei și a consumului de energie; de asemenea, au fost implementate și unele îmbunătățiri.

Binder, sistemul de comunicație inter-proces (IPC), a fost adaptat, întrucât reprezintă mediul de comunicație principal dintre aplicații și sistemul de operare (nu avem system calls din aplicatii, inclusiv funcțiile (serviciile) dispozitivului mobil; expunerea sa este realizată prin intermediul AIDL (Android Interface Definition Language) prin care pot fi manipulate obiecte transformate în primitive utilizate la comunicația propriu-zisă dintre aplicații și sistemul de operare.

Logger, sistemul de jurnalizare, este esențial în cazul în care trebuie realizată depanarea aplicațiilor, în special pentru a detecta anumite situații particulare (informații cu privire la rețea, senzori); acesta este capabil să agrege datele provenite atât de la aplicația propriu-zisă cât și de la sistemul de operare, datele fiind disponibile prin intermediul unor utilitare specializate.

YAFFS2 (Yet Another Flash File System) este un sistem de fișiere adecvat pentru cipuri flash bazate pe porți NAND; platforma Android este stocată pe mai multe partiții, ceea ce îi conferă flexibilitate la actualizări, împiedicând modificarea sa în timpul rulării (/boot - conține secvența de pornire, /system - stochează fișierele de sistem și aplicațiile încorporate, /recovery - deține o imagine din care se poate restaura sistemul de operare, /data - include aplicațiile instalate și datele aferente acestora, /cache - utilizată pentru fișiere temporare, folosind memoria RAM, pentru acces rapid).

Bibliotecile (user-space) conțin codul care oferă principalele funcționalități a sistemului de operare Android, făcând legătura între kernel și aplicații. Sunt incluse aici motorul open-source pentru navigare WebKit, biblioteca FreeType pentru suportul seturilor de caractere, baza de date SQLite utilizată atât ca spațiu de stocare cât și pentru partajarea datelor specifice aplicațiilor, biblioteca libc (Bionic), biblioteca de sistem C bazată pe BSD și optimizată pentru dispozitive mobile bazate pe Linux, biblioteci pentru redarea și înregistrarea de conținut audio/video (bazate pe OpenCORE de la PacketVideo), biblioteci SSL pentru asigurarea securității pe Internet și Surface Manager, bibliotecă pentru controlul accesului la sistemul de afișare care suportă 2D și 3D. Aceste biblioteci nu sunt expuse prin API, reprezentând detalii de implementare Android.

Motorul Android rulează serviciile de platformă precum și aplicațiile care le utilizează, fiind reprezentat de:

  • ART (Android Runtime) este mașina virtuală Java care a fost implementată începând cu versiunea 5.0, folosind un tip de compilare AOH (Ahead of Time), în care bytecode-ul este transpus în cod mașină la momentul instalării, astfel încât acesta este executat direct de mediul dispozitivului mobil; compatibilitatea cu versiuni anterioare (care folosesc mașina virtuală Dalvik, ce se bazează pe un compilator JIT - Just in Time) este asigurată prin transformarea pachetelor în format .dex (Dalvik Executable) la momentul compilării, urmând ca translatarea în format .oat să se realizeze la momentul instalării; fiecare aplicație Android rulează în procesul propriu, într-o instanță a mașinii virtuale ART, izolând astfel codul și datele sale prin intermediul unor permisiuni, care se aplică inclusiv la comunicația prin intermediul interfețelor de comunicare oferite de sistemul de operare Android;
  • Zygote este procesul care gestionează toate aplicațiile, fiind lansat în execuție odată cu sistemul de operare:
    • inițial, creează o instanță a mașinii virtuale Java pentru sistemul de operare Android, în contextul căreia plasează serviciile de bază: gestiunea energiei, telefonie, furnizori de conținut, gestiunea pachetelor, serviciul de localizare, serviciul de notificări;
    • atunci când este necesar să lanseze în execuție o anumită aplicație, se clonează, partajând astfel componentele sistemului de operare Android, astfel încât să se asigure performanța (timp de execuție) și eficiența (memorie folosită), de vreme ce fiecare aplicație trebuie rulată în propria sa instanță a mașinii virtuale Java;
  • Cadrul pentru Aplicații expune diferitele funcționalități ale sistemului de operare Android către programatori, astfel încât aceștia să le poată utiliza în aplicațiile lor.
  • La nivelul de aplicații se regăsesc atât produsele împreună cu care este livrat dispozitivul mobil (Browser, Calculator, Camera, Contacts, Clock, FM Radio, Launcher, Music Player, Phone, S Note, S Planner, Video Player, Voice Recorder), cât și produsele instalate de pe Play Store sau cele dezvoltate de programatori.

Influențe asupra Design-ului Sistemului de Operare Mobil

Prin prisma constrangerilor aduse de catre hardware, sistemul de operare Android a introdus o serie de optimizari fata de ce gasim pe desktops:

Eficiența Energetică:

  • Gestionarea agresivă a proceselor: Android suspendă sau termină rapid aplicațiile inactive pentru a conserva energia.
  • Modul Doze: Introdus pentru a limita procesele de fundal și accesul la rețea în timpul perioadelor de inactivitate ale dispozitivului.
  • Job Scheduler: Grupează sarcinile de fundal ale aplicațiilor pentru a optimiza utilizarea bateriei.

Resurse Hardware Limitate:

  • Componente software cu un set de functionalitati restranse: De exemplu, este folosita biblioteca standard de C Bionic libc (spre deosebire de GLIBC) este optimizat pentru dispozitive încorporate.
  • Gestionarea eficientă a memoriei: Procesul Zygote permite partajarea memoriei între aplicații.
  • Compilarea Ahead of Time (AOT) a ART: Îmbunătățește performanța și reduce consumul de energie comparativ cu compilarea Just in Time (JIT).

Mediul de stocare Flash:

  • YAFFS2 și mai târziu F2FS: Concepute pentru memoria flash, luând în considerare uzura uniformă și operațiunile rapide de citire/scriere.
  • Lipsa memoriei swap, daca ai umplut memoria RAM, aplicatiile vor incepe sa fie inchise.

Considerații de Conectivitate:

  • Suport integrat pentru diverse tehnologii wireless (WiFi, Bluetooth, Celular).
  • Utilizarea eficientă a rețelei: Restricții pentru datele de fundal, mod de economisire a datelor.

Interfața Utilizator:

  • Design centrat pe touch: Componente UI optimizate pentru interacțiuni tactile.
  • Design responsiv: Asigurarea performanței fluide pe diverse dimensiuni și rezoluții de ecran.

Securitate și Confidențialitate:

  • Model de permisiuni: Control fin asupra accesului aplicațiilor la funcțiile dispozitivului și datele utilizatorului.
  • Izolarea datelor aplicațiilor: Fiecare aplicație rulează în propriul sandbox pentru securitate și gestionarea ușoară a datelor.
  • Procese de pornire securizată și pornire verificată, e.g. Titan

Mecanism de Actualizare:

  • Actualizări de sistem bazate pe partiții: Permite actualizări OS fără a afecta datele utilizatorului.
  • Google Play Services: Permite actualizări ale funcționalităților de bază fără upgrade-uri complete ale OS-ului.

Integrarea Senzorilor:

  • API-uri bogate pentru diverși senzori (accelerometru, giroscop, GPS) comuni în dispozitivele mobile.
  • Procesarea eficientă a datelor senzorilor pentru a echilibra colectarea informațiilor și consumul de energie.

Android Studio

Android Studio, dezvoltat de către Google, este un IDE (Integrated Development Environment) bazat pe IntelliJ IDEA de la JetBrains. Acesta, pe lângă funcționalitățile de bază precum code completion, syntax highlighting etc., integrează o serie de automatizări precum integrarea cu sistemul de build, package management, gestionarea toolchain-ului și multe altele.

Instalare

Pentru instalare vom descărca binarul de aici și vom urma pașii din setup, alegând mereu opțiunile recomandate. Pentru mai multe detalii despre instalare, puteți consulta acest ghid.

Prima Aplicatie

Pentru a crea un proiect în Android Studio:

  1. Porniți Android Studio.

  2. În dialogul Welcome to Android Studio, faceți clic pe New Project.

Se deschide fereastra New Project cu o listă de Project Templates furnizate de Android Studio.

În Android Studio, Project Template este un proiect Android care oferă blueprint pentru un anumit tip de aplicație. Template-urile creează structura proiectului și fișierele necesare pentru ca Android Studio să construiască proiectul.

  1. Asigurați-vă că Phone and Tablet este selectată.

  2. Faceți clic pe template-ul Empty Activity pentru a-l selecta ca template pentru proiect. Șablonul Empty Activity este utilizat pentru crearea unei aplicatii simple. Are un singur ecran și afișează textul "Hello Android!".

  3. Faceți clic pe Next. Se deschide dialogul New Project. Acesta are câteva câmpuri pentru configurarea proiectului.

  4. Configurați proiectul după cum urmează:

În câmpul Name, introduceți numele proiectului, de exemplu Hello Android.

Lăsați câmpul Package name așa cum este. Acesta reprezintă modul în care fișierele vor fi organizate în structura de fișiere. În acest caz, numele pachetului va fi com.example.greetingcard.

Lăsați câmpul Save location așa cum este. Acesta conține locația unde sunt salvate toate fișierele legate de proiect.

Selectați API 24: Android 7.0 (Nougat) din meniul din câmpul Minimum SDK. Minimum SDK indică versiunea minimă de Android pe care aplicația poate rula.

  1. Faceți clic pe Finish. Acest lucru poate dura ceva timp - este un moment bun pentru a vă lua o ceașcă de ceai! În timp ce Android Studio se configurează, o bară de progres și un mesaj indică dacă Android Studio încă configurează proiectul. Ar putea arăta astfel:

Un mesaj care arată similar cu acesta vă informează când configurarea proiectului este finalizată.

  1. Faceți clic pe Split în partea dreaptă sus a Android Studio, acest lucru vă permite să vizualizați atât codul, cât și designul. De asemenea, puteți face clic pe Code pentru a vizualiza doar codul sau faceți clic pe Design pentru a vizualiza doar designul.

După apăsarea Split, ar trebui să vedeți trei zone:

  • Vizualizarea Project (1) arată fișierele și directoarele proiectului
  • Vizualizarea Code (2) este locul unde editați codul
  • Vizualizarea Design (3) este locul unde previzualizați cum arată aplicația
  1. Faceți clic pe Build & Refresh. Poate dura ceva timp pentru a se construi, dar când este gata, previzualizarea arată o casetă de text care spune "Hello Android!". Empty Compose activity conține tot codul necesar pentru a crea această aplicație.

Rularea pe Telefon

Pentru a rula aplicatia pe un telefon Android, vom conecta dispozitivul prin USB la laptop si urmarind acest ghid vom activa modul de dezvoltator de pe telefon.

Modificarea Textului

Urmatorul lucru pe care il vom realiza este sa schimbam textul afisat.

Vom folosi Code View pentru a studia continutul fisierului MainActivity.kt. Observăm că există câteva funcții generate automat în acest cod, în special funcțiile onCreate() și setContent().

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        // Punct de intrare in aplicatie, un fel de main
        super.onCreate(savedInstanceState)
        setContent {
            GreetingCardTheme {
                // Un container de suprafață care folosește culoarea 'background' din temă
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

Compilatorul ia codul Kotlin scris, si il translateaza in bytecode de Java Virtual Machine

Funcția onCreate() este punctul de intrare în această aplicație Android și apelează alte funcții pentru a construi interfața utilizatorului. În programele Kotlin, funcția main() este punctul de intrare/punctul de început al execuției. În aplicațiile Android, funcția onCreate() îndeplinește acest rol.

Funcția setContent() din interiorul funcției onCreate() este utilizată pentru a defini layoutul prin funcții compozabile. Toate funcțiile marcate cu adnotarea @Composable pot fi apelate din funcția setContent() sau din alte funcții Composable. Adnotarea spune compilatorului Kotlin că această funcție este utilizată de Jetpack Compose (framework-ul ce se ocupa de interfata grafica) pentru a genera UI-ul.

O funcție compozabilă (composable function) în contextul Jetpack Compose, este o funcție utilizată pentru a defini elementele interfeței utilizator în mod declarativ.

În continuare, ne vom uita la funcția Greeting(). Funcția Greeting() este o funcție Composable, observăm adnotarea @Composable de deasupra ei. Această funcție Composable primește un input și generează ceea ce este afișat pe ecran.

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    // Afiseaza in interfata grafica un obiect (View) de tip casuta Text
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}

În prezent, funcția Greeting() primește un nume și afișează "Hello" pentru acea persoană.

  1. Actualizăm funcția Greeting() pentru a ne prezenta:
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Salut, numele meu este $name!",
        modifier = modifier
    )
}
  1. Vom rula pe telefon pentru a vedea schimbarea.

Excelent! Ai schimbat textul, dar te prezintă ca Android, ceea ce probabil nu este numele tău. În continuare, vom personaliza aplicatia pentru a te afisa numele nostru!

  1. Actualizează funcția GreetingPreview() cu numele tău.
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    GreetingCardTheme {
        Greeting("Meghan")
    }
}
`

Kotlin

În cele ce urmează, ne vom familiariza cu limbajul Kotlin. Kotlin este un limbaj de programare dezvoltat de JetBrains. Kotlin este complet interoperabil cu Java și rulează pe Java Virtual Machine (JVM). Acesta vine cu o serie de îmbunătățiri:

  • Sintaxă concisă: Kotlin reduce cantitatea de cod "boilerplate", făcând codul mai ușor de citit și de întreținut.
  • Safety by design: Prin design, Kotlin elimină anumite clase de erori, cum ar fi erorile de referință nulă (NullPointerExceptions).
  • Programare funcțională: Kotlin oferă suport excelent pentru programarea funcțională, inclusiv funcții de ordin superior și lambda-uri.
  • Interoperabilitate cu Java: Codul Kotlin poate fi folosit alături de cod Java existent, facilitând adoptarea treptată.
  • Suport pentru dezvoltare multiplataformă: Kotlin poate fi utilizat nu doar pentru dezvoltare Android, ci și pentru backend, web frontend și chiar aplicații iOS.
  • Coroutine-uri: Oferă o modalitate simplificată de a gestiona operațiunile asincrone și concurența.
  • Dezvoltare Android: Din 2019, Google a declarat Kotlin ca fiind limbajul preferat pentru dezvoltarea aplicațiilor Android.

Declaratia de functii.

/* Kotlin */

fun calculateSum(numbers: List<Int>): Int {
    return numbers.sum()
}

/* Java */
public int calculateSum(List<Integer> numbers) {
    int sum = 0;
    for (int number : numbers) {
        sum += number;
    }
    return sum;
}

Data classes.

/* Kotlin */

data class Person(val name: String, val age: Int)
/* Java */

class Person {
    private final String name;
    private final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Getters, setters, equals, hashCode, and toString methods manually implemented
}

Stilul functional ca first class citizen.

/* Kotlin */

val numbers = listOf(1, 2, 3, 4, 5)
val evenNumbers = numbers.filter { it % 2 == 0 } // Output: [2, 4]
/* Java */
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> evenNumbers = numbers.stream()
                                   .filter(n -> n % 2 == 0)
                                   .collect(Collectors.toList()); // Output: [2, 4]

Null Safety.

/* Kotlin */

var name: String = "John" // Non-nullable string
// name = null // Compilation error: Null cannot be a value of a non-null type String
/* Java */
String name = "John";
// name = null; // Valid in Java

Smart casting.

/* Kotlin */

fun printStringLength(text: Any) {
    if (text is String) {
        println("The length of the string is: ${text.length}") // No need for explicit casting
    }
}

printStringLength("Hello, Kotlin!") // Output: "The length of the string is: 14"
/* Java */

void printStringLength(Object text) {
    if (text instanceof String) {
        String str = (String) text; // Explicit casting required
        System.out.println("The length of the string is: " + str.length());
    }
}

printStringLength("Hello, Java!"); // Output: "The length of the string is: 12"

Corutine in limbaj.

/* Kotlin */

fun main() = runBlocking {
    launch {
        delay(1000L)
        println("Sarcină în corutină: Salut din corutină!")
    }
    println("Sarcină în main: Aștept...")
    delay(2000L)
}
/* Java */

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(() -> {
        try {
            Thread.sleep(1000);
            System.out.println("Sarcină în thread: Salut din thread!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    thread.start();
    System.out.println("Sarcină în main: Aștept...");
    Thread.sleep(2000);
}

Comutarea între corutine este mai rapidă decât comutarea între thread-uri, deoarece nu implică o comutare de context la nivel de sistem de operare.

Mai multe exemple de rulat în playground în ghidul "Learn Kotlin by Example"

Activitate Laborator

Acum ca ne-am familiarizat cu Kotlin si Android, vom dezvolta o aplicatie de aruncare cu zarul.

Vom creea un nou proiect Android cu numele Dice Roller.

Restructurare Cod Template

Restructurarea codului din template-ul de Empty Activity. Cum am observat deja, codul din template ne afiseaza mesajul Hello World. Vom modifica o parte din codul generat pentru a se asemăna mai mult cu tema unei aplicații de aruncare a zarului. Vom realiza urmatoarele modificari:

In laboratorul de astazi, cand importam automat dependinte, vom alege sursa cu Composable in nume.

  • Eliminăm funcția GreetingPreview().
  • Definim o funcție DiceWithButtonAndImage() cu adnotarea @Composable. Această funcție compozabilă reprezintă componentele UI ale layout-ului și conține, de asemenea, logica pentru click-ul butonului și afișarea imaginii.
  • Eliminăm funcția Greeting(name: String, modifier: Modifier = Modifier).
  • Definim o funcție DiceRollerApp() cu adnotările @Preview și @Composable.
  • Deoarece această aplicație constă doar dintr-un buton și o imagine, putem considera această funcție compozabilă ca fiind aplicația în sine. De aici si numele, DiceRollerApp().
/* MainActivity.kt */

@Preview
@Composable
fun DiceRollerApp() {

}

@Composable
fun DiceWithButtonAndImage() {

}

Deoarece am eliminat funcția Greeting(), apelul către Greeting("Android") din corpul lambda-ului DiceRollerTheme() este evidențiat cu roșu. Asta pentru că compilatorul nu mai poate găsi o referință la acea funcție.

  • Ștergem tot codul din interiorul lambda-ului setContent{} găsit în metoda onCreate().
  • În corpul lambda-ului setContent{}, apelează lambda-ul DiceRollerTheme{} și apoi în interiorul lambda-ului DiceRollerTheme{}, apelează funcția DiceRollerApp().
/* MainActivity.kt */
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        // Aici o sa punem numele temei, implicit este creata o tema cu numele proiectului
        // O gasiti si in uithemes/Theme.kt
        DiceRollerTheme {
            DiceRollerApp()
        }
    }
}

În funcția DiceRollerApp(), apelam DiceWithButtonAndImage(), functie in care vom defini interfata grafica.

/* MainActivity.kt */

@Preview
@Composable
fun DiceRollerApp() {
    DiceWithButtonAndImage()
}

Adaugare Modifier

Compose utilizează un obiect Modifier, care este o colecție de elemente care decorează sau modifică comportamentul elementelor UI Compose. Acesta va fi folosit pentru a stiliza componentele UI ale aplicației Dice Roller. Pentru a adăuga un modifier:

1. Vom modifica funcția DiceWithButtonAndImage() pentru a accepta un argument modifier de tip Modifier și îi vom atribui o valoare implicită de Modifier.

/* MainActivity.kt */

@Composable 
fun DiceWithButtonAndImage(modifier: Modifier = Modifier) {
}

Funcția permite transmiterea unui parametru modifier. Valoarea implicită a parametrului modifier este un obiect Modifier, de aici partea = Modifier din semnătura metodei. Valoarea implicită a unui parametru permite oricui apelează această metodă în viitor să decidă dacă să transmită o valoare pentru parametru. Dacă transmit propriul obiect Modifier, pot personaliza comportamentul și decorarea UI-ului. Dacă aleg să nu transmită un obiect Modifier, acesta va presupune valoarea implicită, care este obiectul Modifier simplu. Această practică poate fi aplicată oricărui parametru. Pentru mai multe informații despre argumentele implicite, consultați Argumente implicite.

2. Acum că funcția compozabilă DiceWithButtonAndImage() are un parametru modifier, vom transmite un modifier atunci când este apelată. Deoarece semnătura metodei pentru funcția DiceWithButtonAndImage() s-a schimbat, ar trebui transmis un obiect Modifier cu decorațiile dorite atunci când este apelată. Clasa Modifier este responsabilă pentru decorarea sau adăugarea de comportament unui compozabil în funcția DiceRollerApp(). În acest caz, există câteva decorații importante de adăugat la obiectul Modifier care este transmis funcției DiceWithButtonAndImage().

/* MainActivity.kt */

DiceWithButtonAndImage(modifier = Modifier)

3. Vom înlănțui o metodă fillMaxSize() pe obiectul Modifier astfel încât layout-ul să umple întregul ecran.

/* MainActivity.kt */

DiceWithButtonAndImage(modifier = Modifier
    .fillMaxSize()
)

4. Centrarea obiectelor de pe ecran. Vom înlănțui metoda wrapContentSize() pe obiectul Modifier și apoi vom transmite Alignment.Center ca argument pentru a centra componentele. Alignment.Center specifică că o componentă se centrează atât vertical, cât și orizontal.

Metoda wrapContentSize() specifică că spațiul disponibil ar trebui să fie cel puțin la fel de mare ca componentele din interior. Cu toate acestea, deoarece se utilizează metoda fillMaxSize(), dacă componentele din interiorul layout-ului sunt mai mici decât spațiul disponibil, un obiect Alignment poate fi transmis metodei wrapContentSize() care specifică modul în care componentele ar trebui aliniate în spațiul disponibil.

/* MainActivity.kt */

DiceWithButtonAndImage(modifier = Modifier
    .fillMaxSize()
    .wrapContentSize(Alignment.Center)
)

Creeam un layout vertical

În Compose, aranjamentele verticale sunt create cu funcția Column().

Funcția Column() este un layout compozabil care își plasează copiii într-o secvență verticală. În designul așteptat al aplicației, se poate observa că imaginea zarului este afișată vertical deasupra butonului de aruncare:

Pentru a crea un aranjament vertical:

/* MainActivity.kt  */
fun DiceWithButtonAndImage(modifier: Modifier = Modifier) {
    Column (
        modifier = modifier,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {}
}

1. În funcția DiceWithButtonAndImage(), se va adăuga o funcție Column().

2. Se va transmite argumentul modifier din semnătura metodei DiceWithButtonAndImage() către argumentul modifier al Column(). Argumentul modifier asigură că compozabilele din funcția Column() respectă constrângerile aplicate instanței modifier.

3. Se va transmite un argument horizontalAlignment funcției Column() și apoi se va seta la o valoare de Alignment.CenterHorizontally. Aceasta asigură că copiii din coloană sunt centrați pe ecranul dispozitivului în ceea ce privește lățimea.

Adaugarea butonului de roll

1. În fișierul strings.xml, vom adăuga un string și îl vom seta la valoarea Roll. res/values/strings.xml

<string name="roll">Roll</string>
MainActivity.kt
Column(
    modifier = modifier,
    horizontalAlignment = Alignment.CenterHorizontally
) {
    Button(onClick = { /*TODO*/ }) {
        Text(stringResource(R.string.roll))
    }
}

2. În corpul lambda al Column(), vom adăuga o funcție Button().

3. Next, vom adăuga o funcție Text() la Button() în corpul lambda al funcției.

4. Vor transmite ID-ul resursei string roll funcției stringResource() și vom transmite rezultatul componentei Text.

Adaugarea unei imagini

O altă componentă esențială a aplicației noastre este imaginea zarului, care afișează rezultatul atunci când utilizatorul atinge butonul Roll (Aruncă). Adăugăm imaginea cu un element compozabil Image, dar acesta necesită o resursă de imagine, așa că mai întâi trebuie să descărcăm câteva imagini furnizate pentru această aplicație.

Vom folosi imaginile de la acest URL

Adăugarea imaginilor cu zaruri în aplicația noastră

  1. În Android Studio, facem clic pe View > Tool Windows > Resource Manager.
  2. Facem clic pe + > Import Drawables pentru a deschide un browser de fișiere.
  1. Găsim și selectăm folderul cu cele șase imagini de zaruri și procedăm la încărcarea lor.
  1. Facem clic pe Next.
  1. Apare dialogul Import Drawables și arată unde merg fișierele de resurse în structura de fișiere.

  2. Facem clic pe Import pentru a confirma că dorim să importăm cele șase imagini.

Imaginile ar trebui să apară în panoul Resource Manager.

Important! Putem face referire la aceste imagini în codul nostru Kotlin cu ID-urile lor de resurse:

  • R.drawable.dice_1
  • R.drawable.dice_2
  • R.drawable.dice_3
  • R.drawable.dice_4
  • R.drawable.dice_5
  • R.drawable.dice_6

Adăugarea unui element compozabil Image

Imaginea zarului ar trebui să apară deasupra butonului Roll. Compose plasează în mod inerent componentele UI secvențial. Cu alte cuvinte, orice element compozabil declarat primul se afișează primul. Aceasta ar putea însemna că prima declarație se afișează deasupra sau înaintea elementului compozabil declarat după ea. Elementele compozabile din interiorul unui element compozabil Column vor apărea unul deasupra celuilalt pe dispozitiv. În această aplicație, folosim o Column pentru a stivui elementele compozabile vertical, prin urmare, orice element compozabil declarat primul în interiorul funcției Column() se afișează înaintea elementului compozabil declarat după el în aceeași funcție Column().

Pentru a adăuga un element compozabil Image:

1. În corpul funcției Column(), creăm o funcție Image() înainte de funcția Button().

/* MainActivity.kt */

Column(
    modifier = modifier,
    horizontalAlignment = Alignment.CenterHorizontally
) {
    Image()
    Button(onClick = { /*TODO*/ }) {
        Text(stringResource(R.string.roll))
    }
}

2. Transmitem funcției Image() un argument painter și îi atribuim o valoare painterResource care acceptă un argument de id de resurse drawable. Pentru moment, transmitem următorul id de resurse: R.drawable.dice_1.

Image(
    painter = painterResource(R.drawable.dice_1)
)

3. Ori de câte ori creăm o Image în aplicația noastră, ar trebui să oferim ceea ce se numește o "descriere a conținutului". Descrierile conținutului sunt o parte importantă a dezvoltării Android. Ele atașează descrieri componentelor UI respective pentru a crește accesibilitatea.

Image(
    painter = painterResource(R.drawable.dice_1),
    contentDescription = "1"
)

Acum toate componentele UI necesare sunt prezente. Dar butonul și imaginea se înghesuie.

4. Pentru a remedia acest lucru, adăugăm un element compozabil Spacer între elementele compozabile Image și Button. Un Spacer primește un Modifier ca parametru. În acest caz, Image este deasupra lui Button, deci trebuie să existe un spațiu vertical între ele. Prin urmare, înălțimea Modifier-ului poate fi setată pentru a se aplica la Spacer. Încercăm să setăm înălțimea la 16.dp. De obicei, dimensiunile dp sunt modificate în incremente de 4.dp.

Spacer(modifier = Modifier.height(16.dp))

Roll Magic

Acum că toate elementele compozabile necesare sunt prezente, vom modifica aplicația astfel încât o atingere a butonului să arunce zarurile.

/* MainActivity.kt */

fun DiceWithButtonAndImage(modifier: Modifier = Modifier) {
    var result = 1
    Column(
        modifier = modifier,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Image(
            painter = painterResource(R.drawable.dice_1),
            contentDescription = "1"
        )
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = { result = (1..6).random() }) {
            Text(stringResource(R.string.roll))
        }
    }
}

1. În funcția DiceWithButtonAndImage(), înainte de funcția Column(), creăm o variabilă result și o setăm la valoarea 1.

2. Privim la elementul compozabil Button. Vom observa că i se transmite un parametru onClick care este setat cu o pereche de acolade conținând comentariul /*TODO*/. Acoladele, în acest caz, reprezintă ceea ce se numește o lambda, zona din interiorul acoladelor fiind corpul lambda. Când o funcție este transmisă ca argument, poate fi denumită și "callback".

/* MainActivity.kt */
Button(onClick = { /*TODO*/ })

O lambda este o funcție literală, care este o funcție ca oricare alta, dar în loc să fie declarată separat cu cuvântul cheie fun, este scrisă inline și transmisă ca expresie. Elementul compozabil Button așteaptă să i se transmită o funcție ca parametru onClick. Acesta este locul perfect pentru a utiliza o lambda, și vom scrie corpul lambda în această secțiune.

3. În funcția Button(), eliminăm comentariul /*TODO*/ din valoarea corpului lambda al parametrului onClick.

4. O aruncare de zaruri este aleatorie. Pentru a reflecta acest lucru în cod, trebuie să folosim sintaxa corectă pentru a genera un număr aleatoriu. În Kotlin, putem utiliza metoda random() pe un interval de numere. În corpul lambda al onClick, setăm variabila result la un interval între 1 și 6 și apoi apelăm metoda random() pe acel interval. Ne amintim că, în Kotlin, intervalele sunt desemnate prin două puncte între primul număr din interval și ultimul număr din interval.

Acum putem interactiona cu butonul, dar mai avem de implementat functionalitatea din spatele lui.

Adăugăm o condiție la aplicația de aruncare a zarurilor

Pana acum, am creat o variabilă result și am setat-o manual la valoarea 1. În cele din urmă, valoarea variabilei result este resetată când butonul Roll este atins și ar trebui să determine ce imagine este afișată.

Elementele compozabile sunt fără stare în mod implicit, ceea ce înseamnă că nu păstrează o valoare și pot fi recompuse oricând de către sistem, rezultând în resetarea valorii. Cu toate acestea, Compose oferă o modalitate convenabilă de a evita acest lucru. Funcțiile compozabile pot stoca un obiect în memorie utilizând elementul compozabil remember.

/* MainActivity.kt */

var result by remember { mutableStateOf(1) }

1. Facem variabila result un element compozabil remember. Elementul compozabil remember necesită transmiterea unei funcții.

2. În corpul elementului compozabil remember, transmitem o funcție mutableStateOf() și apoi îi transmitem acestei funcții argumentul 1. Funcția mutableStateOf() returnează un observabil. Vom învăța mai multe despre observabile mai târziu, dar pentru moment, acest lucru înseamnă practic că atunci când valoarea variabilei result se schimbă, se declanșează o recompunere, valoarea lui result este reflectată, iar interfața utilizator se reîmprospătează.

Acum, când butonul este atins, variabila result este actualizată cu o valoare de număr aleatoriu.

3. Sub instanțierea variabilei result, creăm o variabilă imutabilă imageResource setată la o expresie when care acceptă variabila result și apoi setăm fiecare rezultat posibil la resursa sa drawable.

/* MainActivity.kt */

val imageResource = when (result) {
    1 -> R.drawable.dice_1
    2 -> R.drawable.dice_2
    3 -> R.drawable.dice_3
    4 -> R.drawable.dice_4
    5 -> R.drawable.dice_5
    else -> R.drawable.dice_6
}

4. Schimbăm ID-ul transmis parametrului painterResource al elementului compozabil Image de la resursa R.drawable.dice_1 la variabila imageResource.

5. Schimbăm parametrul contentDescription al elementului compozabil Image pentru a reflecta valoarea variabilei result prin convertirea variabilei result la un șir de caractere cu toString() și transmiterea acestuia ca contentDescription.

/* MainActivity.kt */

Image(
   painter = painterResource(imageResource),
   contentDescription = result.toString()
)

6. Rulăm aplicația noastră. Solutia acestui exercitiu o gasiti aici

Laborator 02. Structura unei Aplicații (I)

In acest laborator vom dezvolta o aplicatie explorând principala componenta a unei aplicatii android: activitatea.

Structura unei aplicatii Android

O aplicatie android este formata dintr-o serie de componente. Fiecare componentă este un punct de intrare prin care sistemul sau un utilizator poate accesa aplicația. Cele patru componente sunt urmatoarele:

  • Activities
  • Services
  • Broadcast receivers
  • Content providers

Activitatea

O activitate este punctul de intrare (entrypoint) pentru interacțiunea cu utilizatorul. Practic este fereastra pe care o vedem pe telefon. Aceasta reprezintă un singur ecran cu o interfață de utilizator. De exemplu, o aplicație de email ar putea avea o activitate care arată o listă de emailuri noi, o altă activity pentru a compune un email și o altă activitate pentru citirea emailurilor.

Deși activities lucrează împreună pentru a forma o experiență de utilizare coerentă în aplicația de email, fiecare activitate este independentă de celelalte. O aplicație diferită poate porni oricare dintre aceste activitati dacă aplicația de email permite acest lucru. De exemplu, o aplicație de cameră ar putea porni activity-ul din aplicația de email pentru compunerea unui nou email pentru a permite utilizatorului să partajeze o fotografie.

Pentru noi, o activitate va fi o subclas a clasei Activity.

Serviciul

Un service este un punct de intrare general pentru a menține o aplicație rulând în fundal din diverse motive. Este o componentă care rulează în fundal pentru a efectua operațiuni de lungă durată sau pentru a executa sarcini pentru procese remote.

Un service nu oferă o interfață de utilizator.

De exemplu, un service ar putea reda muzică în fundal în timp ce utilizatorul se află într-o altă aplicație, sau ar putea prelua date prin rețea fără a bloca interacțiunea utilizatorului cu o activitate. O altă componentă, cum ar fi o activitate, poate porni service-ul și îl poate lăsa să ruleze sau se poate lega de acesta pentru a interacționa cu el.

Pentru noi va fi o subclasa a Service.

Broadcast Receiver

Un broadcast receiver este o componentă care permite sistemului să transmită evenimente către aplicație în afara fluxului obișnuit al utilizatorului, astfel încât aplicația să poată răspunde la anunțuri de broadcast la nivelul întregului sistem. Este o forma de inter-process communication (IPC). Deoarece broadcast receivers sunt un alt punct de intrare bine definit în aplicație, sistemul poate transmite broadcast-uri chiar și către aplicații care nu rulează în prezent.

De exemplu, o aplicație poate programa o alarmă pentru a posta o notificare care să informeze utilizatorul despre un eveniment viitor. Deoarece alarma este livrată unui BroadcastReceiver din aplicație, nu este necesar ca aplicația să rămână în funcțiune până când alarma se declanșează.

Multe broadcast-uri provin din sistem, cum ar fi un broadcast care anunță că ecranul este oprit, bateria este descărcată sau o fotografie a fost capturată. De asemenea, aplicațiile pot iniția broadcast-uri, cum ar fi pentru a informa alte aplicații că anumite date au fost descărcate pe dispozitiv și sunt disponibile pentru a fi utilizate.

Content Provider

Un content provider gestionează un set partajat de date ale aplicației pe care le puteți stoca în sistemul de fișiere, într-o bază de date SQLite, pe web sau în orice altă locație de stocare persistentă la care aplicația poate accesa. Prin intermediul content provider-ului, alte aplicații pot interoga sau modifica datele, dacă content provider-ul permite acest lucru.

De exemplu, sistemul Android oferă un content provider care gestionează informațiile de contact ale utilizatorului. Orice aplicație cu permisiunile adecvate poate interoga content provider-ul, cum ar fi utilizarea ContactsContract.Data, pentru a citi și scrie informații despre o anumită persoană.

Pentru sistem, un content provider este un punct de intrare într-o aplicație pentru publicarea elementelor de date numite, identificate printr-o schemă URI. Astfel, o aplicație poate decide cum dorește să mapeze datele pe care le conține la un spațiu de nume URI, oferind aceste URI-uri altor entități care le pot utiliza la rândul lor pentru a accesa datele.

Procese

Când o componentă a aplicației pornește și aplicația nu are alte componente în execuție, sistemul Android începe un nou proces Linux pentru aplicație cu un singur thread. În mod implicit, toate componentele aceleiași aplicații rulează în același proces și fir, numit firul main.

Dacă o componentă a aplicației pornește și există deja un proces pentru acea aplicație, deoarece o altă componentă din aplicație a pornit deja, atunci componenta pornește în cadrul acelui proces și folosește același fir de execuție. Cu toate acestea, puteți aranja ca diferite componente din aplicație să ruleze în procese separate, și puteți crea extra threads suplimentare pentru orice proces.

Pe langa proces, va fi creat is un thread. Când o aplicație este lansată, sistemul creează un thread de execuție pentru aplicație, numit main thread. Acest thread este foarte important, deoarece este responsabil pentru trimiterea evenimentelor către widget-urile corespunzătoare ale interfeței utilizator, inclusiv evenimentele de desenare. De asemenea, este aproape întotdeauna firul în care aplicația interacționează cu componentele din pachetele android.widget și android.view ale kit-ului de instrumente UI Android. Din acest motiv, firul main este uneori numit UI thread.

Ce se intampla daca rulam o operatie blocanta pe main thread?

Mai jos avem o diagrama a cum un proces este creat pe Android atunci cand o activitate o aplicatie este pornita (prima activitate este lansata).

În mod implicit, toate componentele unei aplicații rulează în același proces, și majoritatea aplicațiilor nu modifică acest lucru. Cu toate acestea, dacă considerați că trebuie să controlați cărei proces îi aparține o anumită componentă, puteți face acest lucru în fișierul manifest.

Procese Zygote

Zygote este un proces în sistemul de operare Android care acționează ca rădăcină a tuturor proceselor de sistem și aplicații cu aceeași interfață binară a aplicației (ABI).

La ce este folosit Zygote?

  • Demonul init generează procesul Zygote când sistemul de operare Android este inițializat. Pe unele sisteme cu arhitectură duală, sunt generate două procese Zygote (pe 64 de biți și 32 de biți). Această pagină acoperă doar sistemele cu arhitectură unică.

  • Zygote poate genera imediat procese numite unspecialized app processes (USAP) sau poate aștepta să genereze procese după cum este necesar de către aplicații.

Crearea unei aplicații de tip View Android în Android Studio

Data trecuta am creat o aplicatie Android ce a folosit Compose ca framework pentru UI. De acum vom folosi Views ca framework. Spre deosebire de Compose, unde interfata grafica era definita direct din codul Kotlin, in Views, interfata grafica este descrisa intr-un fisier XML.

Crearea unui proiect nou

  1. Deschideți Android Studio.

  2. În dialogul Welcome to Android Studio, faceți clic pe Start a new Android Studio project.

  3. Selectați Empty View Activity (nu implicit). Faceți clic pe Next.

  4. Denumiți-vă aplicația cu un nume precum My First app

  5. Asigurați-vă că Languages este setată la Java/Kotlin.

  6. Apasati Finish

După acești pași, Android Studio va face urmatoarele lucruri:

  • Creează un dosar pentru proiectul dvs. Android Studio numit MyFirstApp. Acesta se află de obicei într-un dosar numit AndroidStudioProjects sub directorul dvs. principal.
  • Construiește proiectul (acest lucru poate dura câteva momente). Android Studio folosește Gradle ca sistem de build. Puteți urmări progresul build-ului în partea de jos a ferestrei Android Studio.

Structura proiectului

Poți privi ierarhia fișierelor pentru aplicația ta în multiple moduri, unul dintre acestea este în Project view. Project view îți arată fișierele și folderele structurate într-un mod convenabil pentru lucrul cu un proiect Android. (Aceasta nu corespunde întotdeauna cu ierarhia fișierelor! Pentru a vedea ierarhia fișierelor, alege Project files view făcând click pe (3).)

  1. Dublu-click pe folderul app (1) pentru a extinde ierarhia fișierelor app. (Vezi (1) în captura de ecran.)
  2. Dacă faci click pe Project (2), poți ascunde sau afișa Project view. S-ar putea să fie necesar să selectezi View > Tool Windows pentru a vedea această opțiune.
  3. Selecția curentă Project view (3) este Project > Android.

În Project > Android view vezi trei sau patru foldere la nivelul cel mai înalt sub folderul tău app: manifests, java, java (generated) și res. Este posibil să nu vezi java (generated) imediat.

  1. Extinde folderul manifests.

Acest folder conține AndroidManifest.xml. Acest fișier descrie toate componentele aplicației tale Android și este citit de sistemul de rulare Android atunci când aplicația ta este executată. 2. Extinde folderul java. Toate fișierele tale în limbaj Java sunt organizate aici. Folderul java conține trei subfoldere:

com.example.myfirstapp: Acest folder conține fișierele sursă Java/Kotlin pentru aplicația ta.

com.example.myfirstapp (androidTest): Acest folder este locul unde ai pune testele instrumentate, care sunt testele care rulează pe un dispozitiv Android. Începe cu un fișier de test schelet.

com.example.myfirstapp (test): Acest folder este locul unde ai pune testele unitare. Testele unitare nu necesită un dispozitiv Android pentru a rula. Începe cu un fișier de test unitar schelet. 3. Extinde folderul res. Acest folder conține toate resursele pentru aplicația ta, inclusiv imagini, fișiere de layout, șiruri, icoane și stilizare. Include aceste subfoldere:

drawable: Toate imaginile aplicației tale vor fi stocate în acest folder.

layout: Acest folder conține fișierele de layout UI pentru activitățile tale. În prezent, aplicația ta are o activitate care are un fișier de layout numit activity_main.xml. De asemenea, conține content_main.xml, fragment_first.xml și fragment_second.xml.

menu: Acest folder conține fișiere XML care descriu orice meniuri din aplicația ta.

mipmap: Acest folder conține icoanele de lansare pentru aplicația ta.

navigation: Acest folder conține graficul de navigare, care îi spune Android Studio cum să navigheze între diferite părți ale aplicației tale.

values: Acest folder conține resurse, cum ar fi șiruri și culori, utilizate în aplicația ta.

Rularea Aplicatiei

Pentru a permite Android Studio să comunice cu dispozitivul tău, trebuie să activezi USB Debugging pe dispozitivul tău Android.

Pe Android 4.2 și versiunile superioare, ecranul Developer options este ascuns implicit. Pentru a afișa Developer options și pentru a activa USB Debugging:

  • Pe dispozitiv, deschide Settings > About phone și apasă de șapte ori pe Build number.
  • Revino la ecranul anterior (Settings). Developer options apare la partea de jos a listei. Apasă pe Developer options.
  • Activează USB Debugging.

Acum poți conecta dispozitivul și rula aplicația din Android Studio. Vom rula peste

  • Conectează dispozitivul la mașina de dezvoltare cu un cablu USB. Pe dispozitiv, s-ar putea să fie necesar să accepți permisiunea pentru USB debugging de pe dispozitivul de dezvoltare.
  • În Android Studio, click pe Run în bara de unelte din partea de sus a ferestrei.Dialogul Select Deployment Target se deschide cu lista de emulatoare disponibile și dispozitive conectate.
  • Selectează dispozitivul tău și fă click pe OK. Android Studio instalează aplicația pe dispozitivul tău și o rulează.

Alternativ, se poate rula peste emulatorul AVD.

Aici gasiti un tutorial de cum puteti face asta peste Wifi.

Activity

O activitate in android reprezinta o fereastra vizibila dintr-o aplicatie. O aplicație Android este formată din una sau mai multe activități (slab cuplate între ele). Există întotdeauna o activitate principală care este afișată atunci când aplicația Android este lansată în execuție inițial.

O activitate poate invoca o altă activitate pentru a realiza diferite sarcini, prin intermediul unui obiect de tip intent.

O activitate este formata din doua parti, partea de cod Java care defineste ce se va intampla cand utilizatorul interactioneaza cu activitatea. Aceasta nu este altceva decat o clasa care mosteneste ApplicationContext:

 public class Activity extends ApplicationContext {
     ...
 }

Si intr-un mod similar cu HTML, un cod cod XML care este folosit pentru design-ul vizual al activitatii:

<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="wrap_content"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    tools:context="ro.pub.cs.systems.eim.lab02.graphicuserinterface.LifecycleMonitorActivity" >

In final, o activitate poate fi utilizată numai dacă este definită în fișierul AndroidManifest.xml:

<manifest ... >
  <application ... >
      <activity android:name=".ExampleActivity" />
      ...
  </application ... >
  ...
</manifest >

Apare notiunea de activitate principala, prima activitate care este lansata atunci cand pornim aplicatia. O activitate principală din cadrul unei aplicații Android este caracterizată prin următoarele proprietăți:

  • acțiunea are valoarea android.intent.action.MAIN, întrucât reprezintă punctul de intrare al aplicației Android;
  • categoria are valoarea android.intent.category.LAUNCHER, întrucât activitatea trebuie inclusă în meniul dispozitivului mobil pentru a putea fi lansată în execuție.
<?xml version="1.0" encoding="utf-8"?>
<manifest ... >
  <!-- ... -->
  <application ... >
    <activity
      android:name=".LifecycleMonitorActivity"
      android:label="@string/app_name" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>
</manifest>

Activity Lifecycle

O schimbare majora adusa sistemului de baza Linux de catre Android a fost un nou ciclu de viata al proceselor. Pentru a economisii energie, procesele nu ruleaza decat atunci cand sunt folosite de catre utilizator. Regasim acest lucru in lifecycle-ul activitatilor.

Din momentul în care activitatea este creată și până la momentul în care este distrusă, ea trece printr-o serie de etape, cunoscute sub denumirea de ciclul de viață al activității:

  • în execuție (eng. running) - activitatea se află în prim plan și este vizibilă, astfel încât utilizatorul poate interacționa cu aceasta prin intermediul interfeței grafice pe care o oferă;
  • întreruptă temporar (eng. paused) - activitatea se află în fundal și este (parțial) vizibilă; o astfel de situație este întâlnită în momentul în care o altă activitate a fost pornită, însă interfața sa grafică este transparentă sau nu ocupă întreaga suprafață a dispozitivului de afișare; în acest caz, activitatea este încă activă în sensul că obiectul de tip Activity este stocat în memorie, fiind atașată în continuare procesului responsabil cu gestiunea ferestrelor și menținându-se starea tuturor componentelor sale; totuși, ea poate fi distrusă de sistemul de operare dacă necesarul de memorie disponibilă nu poate fi întrunit din cauza sa;
  • oprită (eng. stopped) - activitatea se află în fundal și este complet ascunsă; o astfel de situație este întâlnită în momentul în care o altă activitate a fost pornită, iar interfața sa grafică ocupă întreaga suprafață a dispozitivului de afișare; și în acest caz, activitatea este activă în sensul că obiectul de tip Activity fiind stocat în memorie, menținându-se starea tuturor componentelor sale, dar detașându-se de procesul responsabil cu gestiunea ferestrelor; ea poate fi distrusă de sistemul de operare dacă necesarul de memorie disponibilă nu poate fi întrunit din cauza sa;
  • inexistentă - activitatea a fost terminată sau distrusă de sistemul de operare, rularea sa impunând crearea tuturor componentelor sale ca și când ar fi accesată inițial.

Tranziția unei activități dintr-o stare în alta este notificată prin intermediul unor callbacks, care pot fi suprascrise pentru a realiza diferite operații necesare pentru gestiunea memoriei, asigurarea persistenței informațiilor și a consistenței aplicației Android în situația producerii de diferite evenimente:


public class LifecycleMonitorActivity extends Activity {

  /* Apelată în momentul în care activitatea este
     creată; această metodă va fi folosită pentru
     inițializări statice: */
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    * ...
  }
  /* apelată înainte ca activitatea să apară pe ecran; */
  @Override
  protected void onStart() {
    super.onStart();
    * ...
  }

  /* apelată înainte ca activitatea să interacționeze cu
    utilizatorul; această metodă va fi folosită pentru a     
    porni servicii sau cod care trebuie să ruleze atâta timp 
    cât aplicația este afișată pe ecran; este urmată 
    întotdeauna de metoda `onPause()`; */
  @Override
  protected void onResume() {
    super.onResume();
    * ...
  }
  
  /* apelată înainte ca activitatea să fie întreruptă
    temporar, iar o altă activitate să fie reluată; această 
    metodă va fi utilizată pentru a opri servicii sau cod 
    care nu trebuie să ruleze atâta timp cât activitatea se 
    află în fundal (întrucât consumă timp de procesor) și 
    pentru a salva starea diferitelor componente în
    vederea asigurării persistenței și a consistenței 
    aplicației înainte
    și după evenimentul care a produs suspendarea sa */
  @Override
  protected void onPause() {
    super.onPause();
    * ...
  }
   
  /*  apelată în momentul în care activitatea este ascunsă,
    fie din cauză că urmează să fie distrusă, fie din cauză 
    că o altă activitate, a cărei interfață grafică ocupă 
    întreaga suprafață a dispozitivului de afișare, urmează 
    să devină vizibilă;
  */
  @Override
  protected void onStop() {
    super.onStop();
    * ...
  }

  /* apelată înainte ca activitatea să se termine sau să
    fie distrusă de către sistemul de operare (fie manual, 
    fie automat) din lipsă de memorie; această metodă va fi 
    utilizată pentru a elibera resursele ocupate.
  */ 
  @Override
  protected void onDestroy() {
    super.onDestroy();
    * ...
  }
 
  /* apelată atunci când activitatea a fost oprită și
     ulterior repornită; este urmată întotdeauna de metoda
     `onStart()`; */
  @Override
  protected void onRestart() {
    super.onRestart();
    * ...
  }
  
}


public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
}        

public override fun onRestart() {
        super.onRestart()
        ...
}

Salvarea Stării

Pentru a salva starea, cum ar fi textul completat de utilizator intr-un obiect de tip EditText din interfata grafica vom suprascrie metoda onSaveInstanceState() Ea primește ca parametru un obiect de tip Bundle, care este un fel de hashmap, în care vor fi plasate datele din cadrul activității care se doresc a fi salvate, acestea putând fi identificate prin intermediul unei chei (de tip String).


@Override
protected void onSaveInstanceState(Bundle savedInstanceState) {
  /* Trebuie sa apelam metoda din clasa de baza întrucât API-ul Android
  furnizează o implementare implicită pentru salvarea stării unei
  activități, parcurgând ierarhia de componente grafice (obiecte de tip
  `View`) care au asociat un identificator (`android:id`), folosit drept
  cheie în obiectul `Bundle`. Astfel, de regulă, pentru elementele
  interfeței grafice, nu este necesar să se mențină starea, acest lucru
  fiind realizat în mod automat, cu respectarea condiției menționate. 
  super.onSaveInstanceState(savedInstanceState); */

  /* Determian o referinta pentru obiectul de tip EditText din interfata grafica
     cu ID-ul username_edit_text */
  EditText usernameEditText = (EditText)findViewById(R.id.username_edit_text);
  savedInstanceState.putString("SOME_STRING_USED_AS_KEY", usernameEditText.getText().toString());
}


override fun onSaveInstanceState(savedInstanceState: Bundle) {
    /* Trebuie sa apelam metoda din clasa de baza întrucât API-ul Android
    furnizează o implementare implicită pentru salvarea stării unei
    activități, parcurgând ierarhia de componente grafice (obiecte de tip
    `View`) care au asociat un identificator (`android:id`), folosit drept
    cheie în obiectul `Bundle`. Astfel, de regulă, pentru elementele
    interfeței grafice, nu este necesar să se mențină starea, acest lucru
    fiind realizat în mod automat, cu respectarea condiției menționate. 
    super.onSaveInstanceState(savedInstanceState); */

    super.onSaveInstanceState(savedInstanceState)

    /* Determian o referinta pentru obiectul de tip EditText din interfata grafica
       cu ID-ul username_edit_text */
    val usernameEditText = findViewById(R.id.username_edit_text)
    savedInstanceState.putString("SOME_STRING_USED_AS_KEY", usernameEditText.text.toString())
}

Restaurarea Stării

Încărcarea conținutului din obiectul de tip Bundle (în vederea restaurării stării) poate fi realizată:

  1. în metoda onCreate():

    @Override
    protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_lifecycle_monitor);
      EditText usernameEditText = (EditText)findViewById(R.id.username_edit_text);
      if ((savedInstanceState != null) && (savedInstanceState.getString(Constants.USERNAME_EDIT_TEXT) != null)) {
        usernameEditText.setText(savedInstanceState.getString(Constants.USERNAME_EDIT_TEXT));
      }
    }

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_lifecycle_monitor)
    
    val usernameEditText = findViewById(R.id.username_edit_text)
    
    savedInstanceState?.getString(Constants.USERNAME_EDIT_TEXT)?.let { username ->
        usernameEditText.setText(username)
    }
}
  1. prin intermediul metodei onRestoreInstanceState(), apelată în mod automat între metodele onStart() și onResume(); această abordare permite separarea dintre codul folosit la crearea ferestrei și codul utilizat la restaurarea stării unei ferestre

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
      super.onRestoreInstanceState(savedInstanceState);
      EditText usernameEditText= (EditText)findViewById(R.id.username_edit_text);
      if (savedInstanceState.getString(Constants.USERNAME_EDIT_TEXT) != null) {
          usernameEditText.setText(savedInstanceState.getString(Constants.USERNAME_EDIT_TEXT));
      }
    }

    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        super.onRestoreInstanceState(savedInstanceState)
        
        val usernameEditText = findViewById(R.id.username_edit_text)
        
        savedInstanceState.getString(Constants.USERNAME_EDIT_TEXT)?.let { username ->
            usernameEditText.setText(username)
        }
    }

Gradle

Sistemul de build compilează resursele aplicației și codul sursă și le împachetează în Android Application Package (APK) (formatul de distribuire al aplicatilor Android) sau Android App Bundles (AAB) pe care le poți testa, desfășura, semna și distribui. Pentru a realiza acest lucru, in ecosistemul Android se folosește Gradle.

Gradle este un sistem open-source de automatizare a build-urilor. Acesta are avantajul unui DSL bazat pe Groovy sau Kotlin și beneficiile Ant și Maven. Cu Gradle, poți manipula cu ușurință procesul de build și logica acestuia pentru a crea mai multe versiuni ale aplicației tale. Este mult mai ușor de utilizat și mult mai concis și flexibil în comparație cu Ant sau Maven folosite individual.

Regulile pentru construiea aplicației Android sunt precizate în fișiere build.gradle, care se definesc pentru fiecare modul și proiect constituent.

Project build.gradle

// În blocul buildscript, definiți setările necesare pentru a construi proiectul.
buildscript {
    // În blocul repositories, adăugați numele depozitelor unde Gradle 
    // ar trebui să caute plugin-urile pe care le folosiți.
    repositories {
        google()
        mavenCentral()
    }
    // Blocul dependencies conține dependențele necesare pentru plugin-uri
    // în acest caz, plugin-urile Gradle și Kotlin. Nu puneți dependențele modulului
    // în acest bloc.
    dependencies {
        classpath "com.android.tools.build:gradle:8.2.2"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20"
    }
}
// Structura blocului allprojects este similară cu cea a blocului buildscript,
// dar aici definiți depozitele pentru toate modulele voastre, nu pentru Gradle
// însuși. De obicei, nu definiți secțiunea dependencies pentru allprojects.
// Dependențele pentru fiecare modul sunt diferite și ar trebui să se afle în
// build.gradle la nivel de modul.
allprojects {
    repositories {
        google()
        mavenCentral()
    }
}
// Un task reprezintă o parte a muncii în procesul de construire. 
// Acesta curăță fișierele de build când este executat. 
tasks.register('clean', Delete) {
    delete rootProject.buildDir
}

Project build.gradle

// Specifică o listă de plugin-uri necesare pentru a construi modulul. Plugin-ul
// com.android.application este necesar pentru a configura setările specifice
// Android ale procesului de build. Aici puteți folosi și com.android.library dacă
// creați un modul de bibliotecă. Plugin-ul kotlin-android vă permite să folosiți
// limbajul Kotlin în modulul vostru.
plugins {
    id "com.android.application"
    id "kotlin-android"
}

// În blocul android, plasați toate opțiunile specifice platformei pentru modul.
android {

    // Definirea unui namespace este necesară pentru lucruri precum accesul la
    // resurse. Aceasta se afla înainte în fișierul AndroidManifest.xml sub
    // proprietatea package, dar acum a fost mutată.
    namespace "com.kodeco.socializify"

    // Opțiunea compileSdk indică nivelul API cu care va fi compilată aplicația
    // voastră. Cu alte cuvinte, nu puteți folosi funcții dintr-un API mai înalt decât
    // această valoare. Aici, ați setat valoarea pentru a utiliza API-urile din
    // Android Tiramisu (Android 13). 
    compileSdk 34
 
    // Blocul defaultConfig conține opțiuni care vor fi aplicate implicit
    // tuturor versiunilor de build (de exemplu, debug, release, etc) ale aplicației
    defaultConfig {
        // applicationId este identificatorul aplicației voastre. Acesta ar trebui să fie
        // unic pentru a putea publica sau actualiza cu succes aplicația pe Google Play
        // Store. Dacă îl lăsați nedefinit, sistemul de build va folosi namespace ca
        // applicationId.
        applicationId "com.kodeco.socializify"

        // Pentru a seta cel mai scăzut nivel API suportat, folosiți minSdkVersion.
        // Aplicația voastră nu va fi disponibilă în Play Store pentru dispozitivele care
        // rulează pe niveluri API mai scăzute.
        minSdkVersion 23

        // Parametrul targetSdkVersion definește nivelul maxim API pe care aplicația
        // voastră a fost testată. Cu alte cuvinte, sunteți siguri că aplicația voastră
        // funcționează corect pe dispozitivele cu această versiune SDK și nu necesită
        // niciun comportament de compatibilitate înapoi. Cea mai bună abordare este să
        // testați temeinic o aplicație folosind cel mai recent API, păstrând valoarea
        // targetSdkVersion egală cu compileSdk.
        targetSdkVersion 34

        // versionCode este o valoare numerică pentru versiunea aplicației.
        versionCode 1
        // versionName este un șir de caractere ușor de înțeles pentru utilizatori, reprezentând versiunea aplicației.
        versionName "1.0"
    }
    // Blocul buildFeatures vă permite să activați anumite funcționalități, cum
    // ar fi View binding sau Compose. 
    buildFeatures {
        viewBinding true
    }
    // Gradle 8.2 suportă implicit JVM 17, deci forțați proiectul să folosească
    // Java 17 prin intermediul suportului pentru Java toolchain oferit de Gradle.
    kotlin {
        jvmToolchain(17)
    }
}
// Blocul dependencies conține toate dependențele necesare pentru acest modul.
dependencies {
    implementation fileTree(include: ["*.jar"], dir: "libs")
    implementation "androidx.appcompat:appcompat:1.6.1"
    implementation "com.google.android.material:material:1.9.0"
}

Logcat

Pentru a putea depana o aplicatie si a vedea eventualele mesaje de debug Android foloseste LogCat. Aveasta nevoie apare de la faptul ca o aplicatie android nu poate scrie la stdout sau stderr intr-un mod conventional.

Pentru a scrie un mesaj la log vom folosi functia Log:

    Log.w("SOME_TAG_TO_FILTER_IN_LOGCAT", "A pornit aplicatia cu bine!");

Pentru a vedea mesajele de log pentru aplicația ta, urmează pașii următori.

  1. În Android Studio, construiește și rulează aplicația ta pe un dispozitiv fizic sau pe un emulator.
  2. Selectează View > Tool Windows > Logcat din bara de meniu.

Implicit, Logcat derulează până la final. Făcând click în fereastra Logcat sau derulând în sus folosind roata mouse-ului dezactivează această caracteristică. Pentru a o reactiva, fă click pe iconița Scroll to the End din bara de unelte. De asemenea, poți folosi bara de unelte pentru a goli, pauza sau reporni Logcat.

Arhitectura logcat

Sistemul de jurnalizare constă dintr-un driver de kernel și buffere de kernel pentru stocarea mesajelor de jurnal Android, clase C, C++ și Java pentru efectuarea înregistrărilor în jurnal și pentru accesarea mesajelor de jurnal, un program independent pentru vizualizarea mesajelor de jurnal (logcat) și capacitatea de a vizualiza și filtra mesajele de jurnal de pe mașina gazdă.

Există patru buffere de jurnal diferite în kernelul Linux, care oferă jurnalizare pentru diferite părți ale sistemului. Accesul la diferitele buffere se face prin noduri de dispozitiv în sistemul de fișiere, în /dev/log. Cele patru buffere de jurnal Android sunt main, events, radio și system. Jurnalul main este pentru aplicație, events este pentru informații despre evenimentele sistemului, radio este pentru informații legate de telefon, iar system este pentru mesaje de sistem de nivel scăzut și depanare.

Cum citim log-urile?

Fiecare log conține o dată, marcaj temporal, ID de proces și thread, tag, numele pachetului, prioritate și mesaj asociat cu acesta. Tag-urile diferite au culori unice care ajută la identificarea tipului de log(de exemplu SOME_TAG_TO_FILTER_IN_LOGCAT in exemplul de mai sus). Fiecare înregistrare în log are o prioritate care poate fi FATAL, ERROR, WARNING, INFO, DEBUG sau VERBOSE.

De exemplu, următorul mesaj de log are o prioritate de DEBUG și un tag de ProfileInstaller:

2022-12-29 04:00:18.823 30249-30321 ProfileInstaller com.google.samples.apps.sunflower D Installing profile for com.google.samples.apps.sunflower

Android Debug Bridge (ADB)

Android Debug Bridge este un utilitar în linie de comandă care permite comunicarea cu cu un dispozitiv mobil fizic sau cu un emulator, prin intermediul unui program client-server ce include 3 componente:

  • un client, apelat prin comanda adb (alți clienți sunt plugin-ul ADT, ADM-ul, Layout Inspector);
  • un server (rulează ca proces de fundal), care gestionează comunicarea dintre client și daemonul ce rulează pe emulator sau dispozitivul mobil fizic;
  • un daemon, care rulează ca un proces de fundal pentru fiecare emulator sau dispozitiv mobil fizic.

ADB este integrat în SDK-ul de Android, regăsindu-se în directorul platform-tools.

Aici gasiti instructiunile de conectare prin ADB la dispozitiv peste Wifi.

Comenzi ADB

  • comenzile ADB pot fi rulate din linia de comandă sau din script, având următorul format:
    student@eim-lab:/opt/android-sdk-linux/platform-tools$ adb [-d|-e|-s <serialNumber>] <command>
    
  • înainte de a utiliza comenzi adb este important să fie cunoscut identificatorul dispozitivului care este conectat la serverul adb, acesta putând fi identificat prin comanda adb devices:
    student@eim-lab:/opt/android-sdk-linux/platform-tools$ adb devices
    
    List of devices attached 
    emulator-5556           device
    192.168.56.101:5555 device
    0123456789ABCDEF    device
    
  • conexiunea la emulator se realizează folosind comanda adb -s <serialNumber> shell

Depanarea este foarte importantă în procesul de realizare a aplicațiilor pentru dispozitive mobile. Există însă unele diferențe față de depanarea programelor pentru calculator, întrucât aplicațiile rulează pe un alt dispozitiv, fiind necesare programe specializate. De asemenea, fiind vorba de dispozitive mobile, apar și anumite evenimente specifice, cum ar fi apeluri telefonice, primirea unui mesaj, descărcărea bateriei, întreruperi ce trebuie tratate intr-un fel sau altul.

Activitate de Laborator

1. În contul de Gitlab de la facultate, să se creeze un repo denumit Laborator02 in care vom pune aplicatia la care vom lucra astazi.

  • Să se cloneze scheletul laboratorului.
  • Să se încarce conținutul descărcat în cadrul depozitului Laborator02 de pe contul Gitlab personal. Ne intereseaza doar folder-ul labtasks.

2. Să se încarce în mediul integrat de dezvoltare Android Studio proiectul ActivityLifecycleMonitor, folosind opțiunea Open an Existing Android Studio Project.

3. În clasa LifecycleMonitorActivity din pachetul ro.pub.cs.systems.eim.lab02.activitylifecyclemonitor.graphicuserinterface, să se suprascrie metodele care monitorizează ciclul de viață al unei activități; fiecare dintre acestea va trebui să apeleze metoda părinte și să notifice apelarea sa prin intermediul unui mesaj, având prioritatea DEBUG și eticheta activitylifecyclemonitor:Log.d(Constants.TAG, "??? method was invoked");

  - onRestart()
  - onStart()
  - onResume()
  - onPause()
  - onStop()
  - onDestroy()

3. Daca ne uitam in Logcat cand ruleaza aplicatia, o sa vedem foarte multe mesaje. Vom filtra in LogCat să afișeze doar mesajele care au eticheta activitylifecycle, generate de aplicația ro.pub.systems.eim.lab02.activitylifecyclemonitor și au cel puțin prioritatea debug.

4. Să se modifice mesajul din metoda onCreate(), astfel încât să se indice dacă activitatea a mai fost lansată în execuție anterior sau nu (dacă există o stare a activității care trebuie restaurată).

5. Să se inspecteze mesajele care sunt generate la producerea următoarelor evenimente:

  - se apasă butonul *Home*
  - se apasă butonul *Back*
  - se apasă butonul *OK* din cadrul aplicației (indiferent dacă datele de autentificare sunt corecte sau nu)
  - se apasă butonul *lista app* 
genymotion, Nexus 5X API 24, butoane "hardware"
onC rea te()onR est ar t()onS tar t()onR esu me ()onP aus e()onS top ()onD est roy ()onS ave Ins t()onR est ore Ins t()
buton Home132
buton Back123
buton _OK_in appniciunadin tremet odenuseape lea ză
buton lista app132
apel tele fonic132
acce ptare12
resp ingere
rotire ecran5613427

6. Să se dezactiveze opțiunea de salvare a stării. În fișierul activity_lifecycle_monitor.xml, pentru fiecare dintre elementele grafice pentru care se dorește să se dezactiveze opțiunea de salvare a stării, se va completa proprietatea android:saveEnabled="false".

Să se observe care este comportamentul în privința informațiilor reținute în elementele grafice de tip EditText, respectiv CheckBox, în condițiile în care activitatea este distrusă (se apasă butonul Home, astfel încât să se apeleze metodele onPause() și onStop(), apoi se închide aplicația. Să se repornească aplicația din meniul dispozitivului mobil.

7. Să se implementeze metoda onSaveInstanceState(), astfel încât, în condițiile în care este bifat elementul grafic de tip CheckBox, să se salveze informațiile din interfața cu utilizatorul. Să se observe comportamentul aplicației în condițiile producerii evenimentului de rotire de ecran.

8. Să se implementeze metoda onRestoreInstanceState() astfel încât să se restaureze starea elementelor grafice. Să se observe comportamentul aplicației în condițiile producerii evenimentului de rotire de ecran.

Să se transfere comportamentul de restaurare a stării pe metoda onCreate() și să se identifice diferențele de implementare (Hint).

9. Utilitarul adb (Android Debug Bridge) se află în locația unde ați instalat SDK-ul pentru studio, de exemplu /opt/android-sdk/platform-tools/. Să se utilizeze comanda adb pentru a copia un fisier pe telefon si de pe telefon:

  • adb devices - vizualizează dispozitivele disponibile (telefoane sau emulatoare)
  • adb -s DEVICE shell ls -l /sdcard/ - dacă e unul singur, nu mai e nevoie de -s
  • adb pull /sdcard/Download . - pentru a descărca fișiere/directoare din device în mașina de dezvoltare
  • adb push fișier.local /sdcard/Download/ - încărcare
  • adb shell obținerea unui prompt în device

10. Conectare adb over wifi dacă suntem cu telefonul și mașina de dezvoltare în acelși subnet

A. Android >= 11

  • Developer Options > Wireless debugging > după enable apare IP:PORT
adb connect IP:port
  • telefonul devine vizibil la IP:PORT în adb devices

B. Android <= 10

  • conectare prin cablu usb pentru aceste comenzi:
adb tcpip 5555
adb shell ip -f inet addr show wlan0 # se obține IP
adb connect IP:5555
  • telefonul devine vizibil la IP:5555 în adb devices
  • on finish
adb -s IP:5555 disconnect  IP:5555
adb -s IP:5555 usb # seems not necessary

11. Să se încarce modificările realizate în cadrul laboratorului pe Gitlab folosint nume sugestive pentru commits.

Laborator 03. Proiectarea Interfețelor Grafice

In acest laborator vom explora ce widgets putem folosi pentru construirea interfeței grafice.

Mecanisme pentru construirea unei interfețe grafice

O interfață grafică poate fi construită în mai multe moduri:

I. Android View

  1. prin definirea componentelor UI și a modului lor de dispunere în cadrul unui fișier .xml, asociat fiecarei activități (sau fragment) în parte, situație adecvată cazurilor în care interfața grafică este statică;
  2. programatic, prin instanțierea unor componente UI direct în codul sursă (cu stabilirea proprietăților respective) al activității (sau fragmentului), abordare potrivită pentru situațiile în care interfața grafică are o structură dinamică (este actualizată în funcție de unele condiții specifice identificate în momentul execuției).

II. Jetpack Compose:

Este complet declarativ, ceea ce înseamnă că descrieți UI-ul prin apelarea unei serii de functions care transformă datele într-o ierarhie UI. Când datele se schimbă, framework-ul reexecută automat aceste functii, actualizând UI-ul.

Pentru a putea oferii oportunitatea de a coda atat in Java cat si in Kotlin, acest laborator se va concentra pe metoda "View file based", aceasta find compatibila cu ambele limbaje de programare. (Compose este folosibil doar cu Kotlin)

Construirea unei interfețe grafice în XML

Definirea interfetei

Pentru fiecare activitate se va construi un fișier .xml în directorul res/layout care va descrie conținutul interfeței grafice precum și modul de dispunere al controalelor componente.

<!-- activity_layout_sample.xml-->

<RelativeLayout 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" >
   
   <TextView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_centerHorizontal="true"
      android:layout_centerVertical="true"
      android:padding="@dimen/padding_medium"
      android:text="Hello world"

      <!-- ID il vom folosi pentru a face rost de o referinta din cod -->
      android:id="my_hello_world_text_id"
      tools:context=".MainActivity" />
      
</RelativeLayout>

Putem modifica interfata grafica direct din codul XML, sau prin intermediul editorului integrat din Android Studio ce are suport de vizualizare a interfetei (Design mode).

Fiecare control din cadrul interfeței grafice va fi reprezentat printr-un element corespunzător, denumirea acestuia fiind identică cu cea a clasei care îi implementează funcționalitatea (de regulă, din pachetul android.widget).

Un control care va fi referit ulterior (fie în codul sursă, fie în fișierul XML) trebuie să aibă asociat un identificator, indicat prin proprietatea android:id. Acesta are forma @+id/identificator (în momentul în care este definit), respectiv @id/identificator pentru referirile ulterioare. Pentru fiecare componentă ce definește un element grafic, se generează o referință în clasa id din fișierul R.java.

Elementele interfeței grafice sunt caracterizate prin anumite proprietăți, cum ar fi poziționarea, dimensiunile, conținutul pe care îl afișează, tipurile de date acceptate de la utilizator, informațiile ajutătoare. Fiecare parametru va fi indicat prin sintaxa android:proprietate="valoare" unde proprietate și valoare trebuie să respecte restricțiile definite în clasa ce descrie controlul respectiv.

Atasarea la o activitate

Pentru a atasa o interfata grafica descris in XML la o activitate, se va folosi layout din binding-urile generate R.java, care va putea fi utilizată pentru încărcarea interfeței grafice în cadrul metodei onCreate(Bundle savedInstanceState).



public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Aici atasam interfata grafica descrisa in fisierul
        // activty_layout_sample.xml din res/layouts la activitate
        setContentView(R.layout.activity_layout_sample);
        // ...
    }
}



class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Aici atasam interfata grafica descrisa in fisierul
        // activty_layout_sample.xml din res/layouts la activitate
        setContentView(R.layout.activity_main)
        // ...
    }
}

După încărcarea propriu-zisă a elementelor din cadrul interfeței grafice vor putea fi obținute referințe către ele prin intermediul metodei findViewById(), care:

  • primește ca parametru un identificator (întreg) definit (automat) în clasa id din fișierul generat R.java pentru toate componentele din cadrul interfeței grafice care au definit atributul android:id (pe baza căruia pot fi referite);
  • returnează un obiect de tip android.view.View, fiind necesar să se realizeze conversia explicită către tipul de control grafic dorit.

Acum, vom putea chema mai multe functii, in functie de tipul de obiect UI. Pe TextView vom putea chema set text, in schimb pentru butoane vom putea seta un handler care sa fie apelat la apasarea acestuia.



// Luam referinta la text field-ul cu 'Hello World'
TextView greetingTextView = (TextView)findViewById(R.id.my_hello_world_text_id);
// Il schimbam in Bye world.
greetingTextView.setText('Bye World')



val greetingTextView: TextView = findViewById(R.id.my_hello_world_text_id)
// Il schimbam in Bye world.
greetingTextView.text = "Bye World"

Componente UI

În cadrul Android View, o interfață grafică conține elemente care au capabilitatea de a afișa informații către utilizator, în diferite formate, respectiv de a interacționa cu acesta, preluând datele necesare realizării diverselor fluxuri operaționale din cadrul aplicației. Există și o categorie specială de controale grafice, responsabile numai cu gestiunea mecanismului de dispunere a celorlalte componente, determinând modul în care vor fi plasate în cadrul ferestrei precum și coordonatele la care vor fi poziționate.

Structura unei interfețe grafice este arborescentă. Întotdeauna, elementul rădăcină va fi un control care gestionează modul în care sunt dispuse componentele sale, în timp ce elementele frunză sunt controale grafice propriu-zise, vizibile pe ecran, cu o funcționalitate bine delimitată. Pe toate celelalte niveluri (intermediare) din această ierarhie se pot regăsi elemente de ambele tipuri (atât elemente grafice cât și mecanisme de dispunere a conținutului - care controlează în acest fel o secțiune din cadrul interfeței cu utilizatorul).

Clasa android.view.View reprezintă baza pentru construirea oricărei interfețe grafice dintr-o aplicație Android, fiind un obiect UI pe care toate celelalte il vor extinde. Ea definește o zonă rectangulară a dispozitivului de afișare (ecran), majoritatea controalelor grafice și a mecanismelor de dispunere a conținutului fiind derivate din aceasta.

1. Cele mai multe elemente grafice sunt definite în pachetul android.widget, fiind implementate controale care implementează cele mai multe dintre funcționalitățile uzuale (text labels, text fields, controale pentru redarea de conținut multimediat - imagini, filme -, butoane, elemente pentru gestiunea datei calendaristice și a timpului).

2. Controalele pentru gestiunea mecanismului de dispunere a conținutului au rolul de a determina modul în care sunt afișate elementele conținute. Acestea sunt derivate din clasa android.view.ViewGroup, definind mai multe reguli prin care se determină poziția la care vor fi plasate componentele pe care le includ.

Controale în Android (widget-uri)

În Android, un control (pentru care se utilizează și denumirea de widget) este de regulă derivat din clasa android.view.View, unde sunt definite câteva caracteristici de bază cu privire la dimensiuni și la modul de dispunere:

ATRIBUTTIP OBIECTDESCRIERE
layout_widthView / ViewGrouplățimea obiectului
layout_heightView / ViewGroupînălțimea obiectului
layout_marginTopView / ViewGroupspațiu suplimentar ce trebuie alocat în partea de sus a obiectului
layout_marginBottomView / ViewGroupspațiu suplimentar ce trebuie alocat în partea de jos a obiectului
layout_marginLeftView / ViewGroupspațiu suplimentar ce trebuie alocat în partea din stânga a obiectului
layout_marginRightView / ViewGroupspațiu suplimentar ce trebuie alocat în partea din dreapta a obiectului
layout_gravityViewmodul de poziționare a elementelor componente în cadrul unui container
layout_weightViewproporția pe care o are controlul, raportată la întregul conținut al containerului
layout_xView / ViewGrouppoziția pe coordonata x
layout_yView / ViewGrouppoziția pe coordonata y
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="16dp"
    android:layout_marginBottom="16dp"
    android:layout_marginLeft="8dp"
    android:layout_marginRight="8dp"
    android:layout_gravity="center"
    android:layout_weight="1"
    android:layout_x="100dp"
    android:layout_y="100dp"
    android:text="Example TextView" />

Unele dintre aceste proprietăți pot fi specificate și pentru containerele în care sunt cuprinse controalele, derivate din clasa android.view.ViewGroup.

Valorile pe care le pot lua atributele layout_width și layout_height sunt:

  • match_parent - dacă se dorește ca obiectul să ocupe tot spațiul pe care îl pune la dispoziție containerul său;

  • wrap_content - dacă se dorește ca obiectul să fie restrâns la dimensiunile conținutului său.

În situația în care nu se specifică cel puțin valorile layout_width și layout_height pentru un anumit control, se va genera o excepție întrucât interfața grafică nu poate fi încărcată corespunzător.

De asemenea, pot fi specificate valori absolute, exprimate în una din unitățile de măsură:

  • dp - pixel independent de rezoluție

Se recomandă să se utilizeze această unitate de măsură în momentul în care se specifică dimensiunea unui control în cadrul unui container. Se asigură astfel faptul că se utilizează o proporție adecvată pentru un control, indiferent de rezoluția ecranului, Android scalându-i dimensiunea automat.

  • sp - pixel independent de scală, echivalent cu dp

Specificarea dimensiunii ecranului unui dispozitiv mobil se face prin indicarea numărului de pixeli pe orizontală și pe verticală. Pentru a obține densitatea (rezoluția) dispozitivului respectiv, se împarte dimensiunea exprimată în pixeli la dimensiunea exprimată în inchi. Se va compara valoarea obținută cu una dintre valorile standard definite pentru încadrarea dispozitivului mobil într-o anumită categorie de rezoluție:

  • 120 dpi - ldpi (Low Density)
  • 160 dpi - mdpi (Medium Density)
  • 240 dpi - hdpi (High Density)
  • 320 dpi - xhdpi (Extra High Density)
  • 480 dpi - xxhdpi (Extra Extra High Density)

Formula de conversie între pixeli (px) și pixeli independenți de rezoluție (dp) este:

px = dp * (resolution_category) / 160

Se observă că 1 px este echivalent cu 1 dp pe un ecran cu rezoluția 160 dpi, considerată drept referință în Android.

Dimensiunea (exprimată în pixeli) a unui control poate fi obținută apelând metodele getWidth(), respectiv getHeight().

Controale de tip buton

În Android pot fi utilizate mai multe tipuri de butoane, între care Button (buton ce are atașat un text), ImageButton (buton ce are atașată o imagine), ToggleButton, CheckBox și Switch (controale ce pot avea două stări - selectat sau nu), RadioButton / RadioGroup (grupuri de butoane ce pot avea două stări - selectat sau nu, asigurându-se totodată excluziunea mutuală între acestea).

O componentă de tip buton ce are atașat un text este definită de clasa android.widget.Button, fiind caracterizată prin proprietatea text, ce conține mesajul pe care acesta îl va afișa.

Întrucât un buton este un tip de control ce interacționează cu utilizatorul, pentru aceasta trebuie definită o clasă ascultător (ce implementează View.OnClickListener) pentru tratarea evenimentelor de tip apăsare. Aceasta definește o metodă onClick() ce primește ca parametru componenta (de tip android.view.View) care a generat evenimentul respectiv. Există două mecanisme de tratare a unui eveniment:

  1. precizarea metodei care tratează evenimentul în fișierul XML corespunzător activității, având dezavantajul că nu se pot transmite parametrii clasei ascultător android:onClick="myButtonClickHandler"
  2. definirea unei clase ascultător în codul sursă
    1. clasă dedicată, având dezavantajul că nu poate accesa decât acele resurse ale activității care sunt publice (referința către obiectul de tip Activity trebuind să fie transmisă ca parametru);
    2. folosirea unei clase interne cu nume în cadrul activității, având avantajul posibilității accesării tuturor resurselor din cadrul acesteia, fără necesitatea transmiterii unei referințe către ea; aceasta este abordarea recomandată pentru tratarea evenimentelor pentru orice tip de control;
    3. folosirea unei clasei interne anonime în cadrul activității, având dezavantajul că trebuie (re)definită pentru fiecare control în parte, ceea ce se poate dovedi ineficient în cazul în care se poate elabora un cod comun pentru tratarea evenimentelor mai multor componente din cadrul interfeței grafice;
    4. utilizarea clasei corespunzătoare activității ca ascultător, prin implementarea interfeței respective și a metodei aferente, având dezavantajul unei scalări ineficiente în cazul în care există mai multe controale pentru care tratarea evenimentului se realizează în mod diferit.


public class MainActivity extends AppCompatActivity {

    private Button mButton;
    private TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Putting vaules in mButton and mTextView
        mButton = findViewById(R.id.button_send);
        mTextView = findViewById(R.id.text_after);

        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view)
            {    
                // Acest cod este chemat la apasarea unui buton
                mTextView.setText("This is the after Result");
            }
        });
    }
}



class MainActivity : AppCompatActivity() {
    private lateinit var mButton: Button
    private lateinit var mTextView: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Initializing mButton and mTextView
        mButton = findViewById(R.id.button_send)
        mTextView = findViewById(R.id.text_after)

        mButton.setOnClickListener {
            // Acest cod este chemat la apasarea unui buton
            mTextView.text = "This is the after Result"
        }
    }
}

În cazul folosirii de clase interne, membrii din clasa părinte ce se doresc a fi accesați trebuie să aibă imutabili (trebuie declarate cu atributul final).

Controale de tip text

Android pune la dispoziția programatorilor un set complet de controale de tip text, dintre care cele mai utilizate sunt TextView, EditText, AutoCompleteTextView și MultiCompleteTextView.

TextView

Controlul de tip TextView este utilizat pentru afișarea unui text către utilizator, fără ca acesta să aibă posibilitatea de a-l modifica.

Conținutul pe care îl afișează un obiect TextView este indicat de proprietatea text. De regulă, acesta referă o constantă definită în resursa care conține șirurile de caractere utilizate în cadrul aplicației. Gestiunea acestui atribut poate fi realizată prin metodele getter și setter respective.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/myTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Hello, Android!"
        android:textSize="24sp" />

</RelativeLayout>
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView textView = findViewById(R.id.myTextView);
        textView.setText("Hello, Android!");
    }
}

Pentru a defini lățimea și înălțimea unui control de tip TextView, este de preferat să se utilizeze următoarele unități de măsură:

  • ems (pentru lățime) - termen folosit în tipografie relativ la dimensiunea punctului unui set de caractere, oferind un control mai bun asupra modului în care este vizualizat textul, independent de dimensiunea propriu-zisă a semnelor respective;
  • număr de linii (pentru înălțime) - garantează afișarea netrunchiată a textului, indiferent de dimensiunea caracterelor;

EditText

EditText este o componentă utilizată pentru obținerea unui text de la utilizator. Implementarea sa pornește de la obiectul de tip TextView, astfel încât sunt moștenite toate proprietățile sale.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Enter text here" />

    <Button
        android:id="@+id/getTextButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="16dp"
        android:text="Get Text" />

    <TextView
        android:id="@+id/resultTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="16dp"
        android:textSize="18sp" />

</LinearLayout>
public class MainActivity extends AppCompatActivity {

    private EditText editText;
    private TextView resultTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        editText = findViewById(R.id.editText);
        resultTextView = findViewById(R.id.resultTextView);
        Button getTextButton = findViewById(R.id.getTextButton);

        getTextButton.setOnClickListener(v -> {
            String inputText = editText.getText().toString();
            resultTextView.setText("You entered: " + inputText);
        });
    }
}

În mod obișnuit, valoarea introdusă va fi afișată doar pe o linie. Dacă se dorește redimensionarea controlului pe măsură ce este introdus un text de dimensiuni mai mari, va trebui specificată proprietatea inputType="textMultiLine". De asemenea, se poate specifica explicit numărul de linii prin intermediul proprietății lines (în cazul în care aceasta nu este specificată, câmpul va fi redimensionat în mod automat pentru ca textul introdus să poată fi afișat; totuși indicarea acestui atribut este util pentru că impune o dimensiune fixă, utilizatorul având posibilitatea de a naviga în cadrul textului prin operația de derulare).

CheckBox

Controlul de tip CheckBox (din clasa android.widget.CheckBox) este tot un element de tip buton ce poate avea două stări (selectat și neselectat), accesibile prin intermediul proprietății checked (android:checked în fișierul XML și setChecked() sau toggle() în codul sursă).

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <CheckBox
        android:id="@+id/myCheckBox"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="My Checkbox" />

    <TextView
        android:id="@+id/statusTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:textSize="18sp" />

</LinearLayout>
public class MainActivity extends AppCompatActivity {

    private CheckBox checkBox;
    private TextView statusTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        checkBox = findViewById(R.id.myCheckBox);
        statusTextView = findViewById(R.id.statusTextView);

        checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
            updateStatus(isChecked);
        });

        // Set initial status
        updateStatus(checkBox.isChecked());
    }

    private void updateStatus(boolean isChecked) {
        String status = isChecked ? "Checked" : "Unchecked";
        statusTextView.setText("Checkbox status: " + status);
    }
}

Deși este definit un tip de eveniment asociat selecției sau deselecției acestei componente (specificat de interfața CompoundButton.OnCheckedChangeListener pentru care trebuie implementată metoda onCheckedChange()), de cele mai multe ori nu i se asociază o clasă ascultător, verificându-se starea sa prin intermediul metodei isChecked() la producerea unui alt eveniment.

Layouts

Controalele Android fac parte din cadrul unui grup (obiect de tip android.view.ViewGroup) care definește și modul în care acestea sunt dispuse în cadrul interfeței grafice precum și dimensiunile pe care le pot lua, motiv pentru care o astfel de componentă este referită și sub denumirea de layout. Acest element nu vizează însă tratarea evenimentelor legate de interacțiunea cu utilizatorul.

În Android, denumirea de layout este utilizată și pentru fișierele de resurse care definesc interfața grafică corespunzătoare unei activități, a unui fragment sau a unui alt element din interfața grafică, plasate în /res/layout (respectiv în /res/layout-land). Acestea nu trebuie însă confundate cu tipurile de controale care gestionează mecanismul de dispunere a diferitelor elemente grafice în cadrul interfeței.

Cele mai utilizate tipuri de grupuri de componente vizuale sunt LinearLayout, AbsoluteLayout, RelativeLayout, FrameLayout, TableLayout și GridLayout.

Elementele de tip layout pot fi imbricate (conținute) unele într-altele, astfel încât se pot proiecta interfețe grafice în care modul de dispunere al controalelor să fie foarte complex, prin combinarea funcționalităților pe care le oferă fiecare dintre componentele de tip ViewGroup. Restricția care trebuie respectată în acest caz este ca spațiile de nume indicate prin proprietatea xmlns să fie precizate doar o singură dată, de obiectul layout rădăcină.

Fiecare clasă de tip layout are și o clasă internă LayoutParams în care proprietățile referitoare la dimensiuni și margini sunt reținute în obiecte de tip layout_.... Ele vor fi aplicate tuturor controalelor grafice conținute. Cele mai frecvent utilizate atribute sunt:

  • layout_height, layout_width - definesc lățimea și înălțimea componentei, putând avea valorile:

    • match_parent - va ocupa tot spațiul pus la dispoziție de componenta în care este conținută (fără padding);
    • wrap_content - va ocupa doar spațiul solicitat de componentele pe care le conține (cu padding);
    • o valoare indicată explicit împreună cu unitatea de măsură.
  • layout_weight - proporția pe care o ocupă în raport cu alte componente;

  • weightSum - suma proporțiilor tuturor controalelor grafice conținute; valoarea implicită este 1;

  • layout_gravity - modul în care componenta este aliniată în cadrul grupului din care face parte (valorile posibile pe care le poate lua această proprietate sunt: top, botttom, left, right, center_vertical, center_horizontal, center (centrare pe ambele direcții), fill_vertical, fill_horizontal, fill (ocuparea spațiului pe ambele direcții), clip_vertical, clip_horizontal; aceste valori pot fi combinate prin intermediul operatorului | (pe biți);

LinearLayout (obligatoriu)

În cadrul unui grup de tip LinearLayout, componentele sunt dispuse fie pe orizontală, fie pe verticală, în funcție de proprietatea orientation (putând lua valorile horizontal - implicită sau vertical).

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="16dp">

    <Button
        android:id="@+id/btnMonday"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Monday"
        android:layout_marginBottom="8dp" />

    <Button
        android:id="@+id/btnTuesday"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Tuesday"
        android:layout_marginBottom="8dp" />

    <Button
        android:id="@+id/btnWednesday"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Wednesday"
        android:layout_marginBottom="8dp" />

    <Button
        android:id="@+id/btnThursday"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Thursday"
        android:layout_marginBottom="8dp" />

    <Button
        android:id="@+id/btnFriday"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Friday"
        android:layout_marginBottom="8dp" />

    <Button
        android:id="@+id/btnSaturday"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Saturday"
        android:layout_marginBottom="8dp" />

    <Button
        android:id="@+id/btnSunday"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Sunday" />

</LinearLayout>

GridLayout

Layout-ul de tip GridLayout este utilizat tot pentru dispunerea componentelor într-un format tabelar, folosind însă o sintaxă mult mai flexibilă. Totodată, acest mecanism este și mult mai eficient din punctul de vedere al randării.

Astfel, pentru specificarea numărului de rânduri și de coloane se vor utiliza proprietățile rowCount și columnCount, indicându-se pentru fiecare element grafic în parte poziția la care va fi plasat, prin atributele layout_row și layout_column. În cazul în care pentru o componentă grafică nu se specifică linia sau coloana din care face parte, atributul orientation (având va valori posibile horizontal sau vertical indică dacă elementul următor va fi plasat pe linia sau pe coloana succesivă).

În cazul în care se dorește extinderea unui element grafic pe mai multe rânduri sau pe mai multe coloane, se vor utiliza atributele layout_rowSpan și layout_columnSpan. Pentru controlul modului de dispunere se va folosi proprietatea layout_gravity. Precizarea layout_width și layout_height nu este neapărat necesară, valoarea lor implicită în acest caz fiind wrap_content.

<?xml version="1.0" encoding="utf-8"?>
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:columnCount="2"
    android:rowCount="3"
    android:padding="16dp">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Name:"
        android:layout_column="0"
        android:layout_row="0"
        android:layout_marginEnd="8dp" />

    <EditText
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_column="1"
        android:layout_row="0"
        android:layout_gravity="fill_horizontal" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Email:"
        android:layout_column="0"
        android:layout_row="1"
        android:layout_marginEnd="8dp" />

    <EditText
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_column="1"
        android:layout_row="1"
        android:layout_gravity="fill_horizontal" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Submit"
        android:layout_column="0"
        android:layout_row="2"
        android:layout_columnSpan="2"
        android:layout_gravity="center_horizontal" />

</GridLayout>

Adaptarea interfeței grafice în funcție de orientarea ecranului

Dispozitivele Android suportă două moduri de orientare a ecranului: portrait și landscape. În momentul în care se produce o modificare a orientării dispozitivului de afișare, activitatea care rulează în mod curent este distrusă și apoi recreată. Aplicațiile trebuie să gestioneze astfel de situații, adaptând interfața grafică în funcție de suprafața de care dispun.

În acest scop, poate fi adoptată una din următoarele abordări:

  1. ancorarea elementelor grafice (de regulă, de cele patru colțuri ale ecranului), folosind o dispunere de tip RelativeLayout împreună cu o combinație a proprietăților layout_alignParentTop, layout_alignParentBottom, layout_alignParentLeft, layout_alignParentRight, layout_centerHorizontal, layout_centerVertical
  2. redimensionarea și repoziționarea creând un fișier XML corespunzător fiecărei activități pentru fiecare orientare a ecranului, plasate în /res/layout, respectiv în /res/layout-land

De asemenea, o activitate poate fi forțată să afișeze conținutul folosind un singur tip de orientare, indiferent de poziția în care se află dispozitivul mobil:

  1. în fișierul AndroidManifest.xml, în elementul <activity> corespunzător, se specifică proprietatea android:screenOrientation cu valorile portrait, respectiv landscape;
  2. programatic, în metoda onCreate(), se apelează setRequestedOrientation() cu unul din parametrii ActivityInfo.SCREEN_ORIENTATION_PORTRAIT sau ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE.

Deși în cadrul unui ecran pot fi utilizate combinații între mai multe mecanisme de dispunere a conținutului, se recomandă ca o astfel de abordare să fie evitată întrucât se poate ajunge la situația în care interfața grafică nu scalează pe mai multe dispozitive de afișare cu rezoluții diferite sau prezintă probleme de performanță.

În situația în care se dorește spațierea controalelor grafice, poate fi utilizat un obiect de tip android.widget.Space.

Sensors

Majoritatea dispozitivelor cu sistem de operare Android au senzori încorporați care măsoară mișcarea, orientarea și diverse condiții de mediu. Acești senzori sunt capabili să furnizeze date brute cu precizie și acuratețe ridicată și sunt utili dacă doriți să monitorizați mișcarea sau poziționarea tridimensională a dispozitivului sau dacă doriți să monitorizați schimbările din mediul ambiant din apropierea unui dispozitiv. De exemplu, un joc ar putea urmări citirile de la senzorul de gravitație al unui dispozitiv pentru a deduce gesturi și mișcări complexe ale utilizatorului, cum ar fi înclinarea, scuturarea, rotația sau balansarea. De asemenea, o aplicație meteo ar putea folosi senzorul de temperatură și senzorul de umiditate ale unui dispozitiv pentru a calcula și raporta punctul de rouă, sau o aplicație de călătorie ar putea folosi senzorul de câmp geomagnetic și accelerometrul pentru a raporta o direcție de busolă.

Sensors Hardware Abstraction Layer

Cum telefoanele Android sunt facute de mai multi vendors, avem mai multe tipuri de senzori, totusi am vrea ca interactiunea dintre o aplicatie si un senzor sa fie standardizat.

Astfel a fost dezvoltata ideea de Hardware Abstraction Layer (HAL) pentru a crea o interfață standardizată între software-ul de nivel înalt (cum ar fi sistemul de operare sau aplicațiile) și hardware-ul specific al dispozitivului. Acest strat de abstractizare permite dezvoltatorilor de software să scrie cod care funcționează pe o varietate de dispozitive fără a trebui să cunoască detaliile specifice ale fiecărui hardware. În esență, HAL ascunde complexitatea și variațiile hardware-ului, oferind o interfață consistentă și simplificată pentru software. Acest lucru îmbunătățește portabilitatea software-ului, reduce timpul și costurile de dezvoltare, și facilitează actualizările și întreținererea sistemului, deoarece modificările hardware-ului pot fi gestionate în mare parte în cadrul HAL, fără a afecta semnificativ nivelurile superioare ale software-ului.

Accelerometru

In general, pentru a cititi date de la senzori, vom avea nevoie de o instanta a clasei Sensors.



private SensorManager sensorManager;
private Sensor sensor;

sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);



val sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
val sensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)

Mai jos avem un exemplu complet de utilizare al accelerometrului:



public class AccelerometerActivity extends Activity implements SensorEventListener {
    private SensorManager sensorManager;
    private Sensor accelerometer;
    private TextView xValueTextView, yValueTextView, zValueTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_accelerometer);

        xValueTextView = findViewById(R.id.xValueTextView);
        yValueTextView = findViewById(R.id.yValueTextView);
        zValueTextView = findViewById(R.id.zValueTextView);

        sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
        if (sensorManager != null) {
            accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (accelerometer != null) {
            sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL);
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (sensorManager != null) {
            sensorManager.unregisterListener(this);
        }
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
            float x = event.values[0];
            float y = event.values[1];
            float z = event.values[2];

            xValueTextView.setText("X: " + x);
            yValueTextView.setText("Y: " + y);
            zValueTextView.setText("Z: " + z);
        }
}



class AccelerometerActivity : AppCompatActivity(), SensorEventListener {
    private lateinit var sensorManager: SensorManager
    private var accelerometer: Sensor? = null
    private lateinit var xValueTextView: TextView
    private lateinit var yValueTextView: TextView
    private lateinit var zValueTextView: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_accelerometer)

        xValueTextView = findViewById(R.id.xValueTextView)
        yValueTextView = findViewById(R.id.yValueTextView)
        zValueTextView = findViewById(R.id.zValueTextView)

        sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
        accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
    }

    override fun onResume() {
        super.onResume()
        accelerometer?.let {
            sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_NORMAL)
        }
    }

    override fun onPause() {
        super.onPause()
        sensorManager.unregisterListener(this)
    }

    override fun onSensorChanged(event: SensorEvent) {
        if (event.sensor.type == Sensor.TYPE_ACCELEROMETER) {
            val x = event.values[0]
            val y = event.values[1]
            val z = event.values[2]
            xValueTextView.text = "X: $x"
            yValueTextView.text = "Y: $y"
            zValueTextView.text = "Z: $z"
        }
    }
}

La nivel conceptual, un senzor de accelerație determină accelerația care este aplicată unui dispozitiv (Ad) prin măsurarea forțelor care sunt aplicate asupra senzorului însuși (Fs) folosind următoarea relație:

Cu toate acestea, forța gravitațională influențează întotdeauna accelerația măsurată conform următoarei relații:

Din acest motiv, atunci când dispozitivul stă pe o masă (și nu accelerează), accelerometrul indică o magnitudine de g = 9,81 m/s². În mod similar, când dispozitivul este în cădere liberă și, prin urmare, accelerează rapid spre pământ cu 9,81 m/s², accelerometrul său indică o magnitudine de g = 0 m/s². Prin urmare, pentru a măsura accelerația reală a dispozitivului, contribuția forței gravitaționale trebuie eliminată din datele accelerometrului. Acest lucru poate fi realizat prin aplicarea unui filtru trece-sus. În mod invers, un filtru trece-jos poate fi utilizat pentru a izola forța gravitațională. Următorul exemplu arată cum puteți face acest lucru:



@Override
public void onSensorChanged(SensorEvent event){
    // In this example, alpha is calculated as t / (t + dT),
    // where t is the low-pass filter's time-constant and
    // dT is the event delivery rate.

    final float alpha = 0.8;

    // Isolate the force of gravity with the low-pass filter.
    gravity[0] = alpha * gravity[0] + (1 - alpha) * event.values[0];
    gravity[1] = alpha * gravity[1] + (1 - alpha) * event.values[1];
    gravity[2] = alpha * gravity[2] + (1 - alpha) * event.values[2];

    // Remove the gravity contribution with the high-pass filter.
    linear_acceleration[0] = event.values[0] - gravity[0];
    linear_acceleration[1] = event.values[1] - gravity[1];
    linear_acceleration[2] = event.values[2] - gravity[2];
}



override fun onSensorChanged(event: SensorEvent) {
    // In this example, alpha is calculated as t / (t + dT),
    // where t is the low-pass filter's time-constant and
    // dT is the event delivery rate.

    val alpha: Float = 0.8f

    // Isolate the force of gravity with the low-pass filter.
    gravity[0] = alpha * gravity[0] + (1 - alpha) * event.values[0]
    gravity[1] = alpha * gravity[1] + (1 - alpha) * event.values[1]
    gravity[2] = alpha * gravity[2] + (1 - alpha) * event.values[2]

    // Remove the gravity contribution with the high-pass filter.
    linear_acceleration[0] = event.values[0] - gravity[0]
    linear_acceleration[1] = event.values[1] - gravity[1]
    linear_acceleration[2] = event.values[2] - gravity[2]
}

Permisiuni

In general, android are implementat un set de capabilitati la nivel de aplicatie. Astfel, va trebui sa cerem permisiunea pentru a putea utiliza senzorul. Mai jos gasiti un exemplu de cum putem realiza acest lucru.



public class AccelerometerActivity extends Activity implements SensorEventListener {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_accelerometer);
        requestSensorPermission();
        // ...
    }

    private void requestSensorPermission() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.BODY_SENSORS)
                != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.BODY_SENSORS},
                    SENSOR_PERMISSION_CODE);
        } else {
            startAccelerometerListening();
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == SENSOR_PERMISSION_CODE) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                startAccelerometerListening();
            } else {
                Toast.makeText(this, "Sensor permission denied", Toast.LENGTH_SHORT).show();
            }
        }
    }
}



class AccelerometerActivity : AppCompatActivity(), SensorEventListener {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_accelerometer)
        requestSensorPermission()
        // ...
    }

    private fun requestSensorPermission() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.BODY_SENSORS)
            != PackageManager.PERMISSION_GRANTED
        ) {
            ActivityCompat.requestPermissions(
                this,
                arrayOf(Manifest.permission.BODY_SENSORS),
                SENSOR_PERMISSION_CODE
            )
        } else {
            startAccelerometerListening()
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == SENSOR_PERMISSION_CODE) {
            if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                startAccelerometerListening()
            } else {
                Toast.makeText(this, "Sensor permission denied", Toast.LENGTH_SHORT).show()
            }
        }
    }

    companion object {
        private const val SENSOR_PERMISSION_CODE = 1 // You need to define this constant
    }

De asemenea, in AndroidManifest.xml va trebui sa actualizam cu o linie care anunta sistemul de operare ca vom avea nevoie de aceasta permisiune.

<uses-permission android:name="android.permission.BODY_SENSORS" />

Activitate de Laborator

Se dorește implementarea unei aplicații Android, conținând o singură activitate, care să ofere utilizatorilor funcționalitatea necesară pentru formarea unui număr de telefon (PhoneDialer).

1. Să se creeze un proiect Android Studio denumit PhoneDialer.

Se indică detaliile proiectului:

  • Application name - Phone Dialer
  • Company domain - lab03.eim.systems.cs.pub.ro (se va genera în mod automat Package name cu valoarea ro.pub.cs.systems.eim.lab03.phonedialer)
  • Project location - locația directorului de pe discul local unde a fost descărcat depozitul la distanță Laborator03

Se indică platforma pentru care se dezvoltă aplicația Android (se bifează doar Phone and Tablet), iar SDK-ul Android (minim) pentru care se garantează funcționarea este API 24 (Nougat, 7.0).

Vom folosi template-ul Empty View Activity

pentru care se precizează:

  • Activity Name (denumirea activității) - PhoneDialerActivity;
  • Layout Name (denumirea fișierului XML din res/layout în care va fi construită interfața grafică) - activity_phone_dialer.xml.

2. În fișierul activity_phone_dialer din directorul res/layout se construiește interfața grafică folosind:

  • editorul vizual (Design)
  • editorul XML (Text)

Aceasta va conține:

  • un obiect de tip EditText, care nu poate fi editat manual de utilizator, în care se va afișa numărul de telefon;
  • 10 obiecte de tip Button corespunzătoare celor 10 cifre (0..9);
  • 2 obiecte de tip Button corespunzătoare caracterelor speciale * și #;
  • un obiect de tip ImageButton pentru operaţia de editare a numărului de telefon, prin revenirea la caracterul anterior, în cazul în care s-a greșit;
  • 2 obiecte de tip ImageButton pentru operaţiile de formare a numărului de telefon, respectiv de închidere.

Pentru iconite, va recomandam flaticon.

Pentru dispunerea controalelor în cadrul interfeței grafice se va folosi un mecanism de tip LinearLayout cu orientare verticală, iar tastatura virtuală va fi realizată printr-un obiect de tip GridLayout cu 6 linii și 3 coloane.

3. În clasa PhoneDialerActivity din pachetul ro.pub.cs.systems.eim.lab03.phonedialer, să se implementeze o clasă Listener pentru tratarea evenimentelor de tip Click.

  • pentru butoanele ce conțin cifre sau caracterele * / #, se va adăuga simbolul corespunzător la numărul de telefon care se dorește format;
  • pentru butonul de corecție, se va șterge ultimul caracter (în cazul în care numărul de telefon nu este vid);
  • pentru butonul de oprire, se va închide activitatea (se va apela metoda finish());
  • pentru butonul de apel, se va invoca intenția care realizează legătura telefonică; întrucât se compilează proiectul Android folosind o versiune mai mare decât Marshmelow (6.0), este necesar să fie solicitată permisiunea de efectuare a apelului telefonic la momentul rulării:

    if (ContextCompat.checkSelfPermission(PhoneDialerActivity.this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
      ActivityCompat.requestPermissions(
        PhoneDialerActivity.this,
        new String[]{Manifest.permission.CALL_PHONE},
        Constants.PERMISSION_REQUEST_CALL_PHONE);
    } else {
      Intent intent = new Intent(Intent.ACTION_CALL);
      intent.setData(Uri.parse("tel:" + phoneNumberEditText.getText().toString()));
      startActivity(intent);
    }


    if (ContextCompat.checkSelfPermission(
            this@PhoneDialerActivity,
            Manifest.permission.CALL_PHONE
        ) != PackageManager.PERMISSION_GRANTED
    ) {
        ActivityCompat.requestPermissions(
            this@PhoneDialerActivity,
            arrayOf(Manifest.permission.CALL_PHONE),
            Constants.PERMISSION_REQUEST_CALL_PHONE
        )
    } else {
        val intent = Intent(Intent.ACTION_CALL)
        intent.setData(Uri.parse("tel:" + phoneNumberEditText!!.text.toString()))
        startActivity(intent)
    }

Se va defini o clasă internă cu nume, ce implementează interfața View.OnClickListener (implementează metoda public void onClick(View view). Instanța acesteia va fi utilizată pentru toate obiectele de tip buton din cadrul interfeței grafice.

Pentru a putea apela, în fișierul AndroidManifest.xml trebuie să se specifice o permisiune explicită în acest sens:
<uses-permission android:name="android.permission.CALL_PHONE" />

4. Pentru a fi inovativi, in aplicatia noastra vom aduce optiunea sa poti vedea acceleratia pe axa Ox in timp ce tastezi. Pentru acest lucru, vom adauga un nou camp de tip TextView in care vom afisa aceasta acceleratie.

5. Să se gestioneze corespunzător evenimentul de tip rotire a ecranului

  1. să se blocheze tranziția între modurile portrait și landscape:
    1. în fișierul AndroidManifest.xml

      <manifest ...>
        <application ... >
          <activity ...
            android:screenOrientation="portrait" 
            ... />
            
          <!-- ... -->
        </application>
      </manifest>
      
    2. programatic

      @Override
      protected void onCreate(Bundle onCreateInstanceState) {
        super.onCreate(onCreateInstanceState);
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
        * ..
      }
      
  2. Să se definească, în directorul res/layout-land o interfață grafică adecvată acestei configurații a dispozitivului de afișare:

În Android Studio, se creează un nou subdirector layout-land în directorul res (din meniul contextual afișat cu right click, se selectează NewAndroid resource directory, indicându-se denumirea sa (layout-land), tipul de resursă (layout) și setul codului sursă pentru care se definește resursa respectivă (main)):

Se trece în modul de vizualizare Project și în directorul res/layout-land se copiază conținutul fișierului activity_phone.dialer.xml prin operații de tip copy-paste și se realizează modificările necesare SAU se poate preciza un fișier nou cu aceeași denumire.

Laborator 04. Structura unei Aplicații (II)

Intenții

Aplicațiile pe Android sunt izolate, și comunicația între aplicație și sistem se face prin Inter-Process Communication (IPC). În Android, pentru a porni alte activități sau aplicații vom folosi forma de IPC numită Intents.

Conceptul de intenție în Android este destul de complex (și unic), putând fi definit ca o acțiune având asociată o serie de informații, transmisă sistemului de operare Android pentru a fi executată sub forma unui mesaj asincron. În acest fel, intenția asigură interacțiunea între toate aplicațiile instalate pe dispozitivul mobil, chiar dacă fiecare în parte are o existență autonomă. Din această perspectivă, sistemul de operare Android poate fi privit ca o colecție de componente funcționale, independente și interconectate.

De regulă, o intenție poate fi utilizată pentru:

  • a invoca activități din cadrul aceleiași aplicații Android;
  • a invoca alte activități, existente în contextul altor aplicații Android;
  • a transmite mesaje cu difuzare (eng. broadcast messages), care sunt propagate la nivelul întregului sistem de operare Android și pe care unele aplicații Android le pot prelucra, prin definirea unor clase ascultător specifice; un astfel de comportament este util pentru a implementa aplicații bazate pe evenimente.

O intenție reprezintă o instanță a clasei android.content.Intent. Aceasta este transmisă ca parametru unor metode (de tipul startActivity() sau startService(), definite în clasa abstractă android.content.Context), pentru a invoca anumite componente (activități sau servicii). Un astfel de obiect poate încapsula anumite date (împachetate sub forma unui android.os.Bundle), care pot fi utilizate de componenta ce se dorește a fi executată prin intermediul intenției.

Constructia unui Intent

Intent explicit

Un intent explicit este unul pe care îl folosești pentru a lansa un component de aplicație specific, cum ar fi o activitate sau un serviciu particular în aplicația ta. Pentru a crea un intent explicit, definește numele componentei pentru obiectul Intent - toate celelalte proprietăți ale intentului sunt opționale.

De exemplu, dacă vrem sa pornim o activitate SecondActivity la apasarea unui buton vom folosi urmatorul cod:



// Vom porni o a doua activitate la apasarea unui buton
// A doua activitatea a fost creata folosind Click Dreapta pe directorul cu activitati ->
// New View Empty Activity
Button btn = (Button)findViewById(R.id.open_activity_button);    

btn.setOnClickListener(new View.OnClickListener() {         
        @Override
        public void onClick(View v) {
            startActivity(new Intent(MainActivity.this, SecondActivity.class));
        }
});



val btn = findViewById

Intent implicit

Un intent implicit specifică o acțiune care poate invoca orice aplicație de pe dispozitiv capabilă să efectueze acțiunea respectivă. Utilizarea unui intent implicit este utilă atunci când aplicația ta nu poate efectua acțiunea, dar alte aplicații probabil pot și ai vrea ca utilizatorul să aleagă care aplicație să folosească.

De exemplu, dacă ai conținut pe care vrei să-l partajezi cu alte persoane, creează un intent cu acțiunea ACTION_SEND și adaugă extra-uri care specifică conținutul de partajat. Atunci când apelezi `startActivity()`` cu acest intent, utilizatorul poate selecta o aplicație prin intermediul căreia să partajeze conținutul.

Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND)
// Date pe care vrem sa le trimitem catre activitatea pe care urmeaza sa o pornim
sendIntent.putExtra(Intent.EXTRA_TEXT, textMessage);
sendIntent.setType("text/plain");

// Try to invoke the intent.
try {
    startActivity(sendIntent);
} catch (ActivityNotFoundException e) {
    // Define what your app should do if no activity can handle the intent.
}

Kotlin

val sendIntent = Intent().apply {
    action = Intent.ACTION_SEND
    // Date pe care vrem sa le trimitem catre activitatea pe care urmeaza sa o pornim
    putExtra(Intent.EXTRA_TEXT, textMessage)
    type = "text/plain"
}

// Try to invoke the intent.
try {
    startActivity(sendIntent)
} catch (e: ActivityNotFoundException) {
    // Define what your app should do if no activity can handle the intent.
}

Transmiterea de informații între componente prin intermediul intențiilor

Intențiile pot încapsula anumite informații care pot fi partajate de componentele între care fac legătura (însă unidirecțional, de la componenta care invocă spre componenta invocată!) prin intermediul secțiunii extra care conține un obiect de tip Bundle. Obținerea valorii secțiunii extra corespunzătoare unei intenții poate fi obținute folosind metoda getExtras(), în timp ce specificarea unor informații care vor fi asociate unei intenții poate fi realizată printr-un apel al metodei putExtras().

O activitate copil, lansată în execuție prin intermediul metodei startActivity(), este independentă de activitatea părinte, astfel încât aceasta nu va fi notificată cu privire la terminarea sa. În situațiile în care un astfel de comportament este necesar, activitatea copil va fi pornită de activitatea părinte ca subactivitate care transmite înapoi un rezultat. Acest lucru se realizează prin lansarea în execuție a activității copil prin intermediul metodei startActivityForResult(). În momentul în care este finalizată, va fi invocată automat metoda onActivityResult() de la nivelul activității părinte.

final private static int ANOTHER_ACTIVITY_REQUEST_CODE = 2017;

@Override
protected void onCreate(Bundle state) {
    super.onCreate(state);
    setContentView(R.layout.activity_main);
    Button btn = (Button)findViewById(R.id.open_activity_button);    

    btn.setOnClickListener(new View.OnClickListener() {         
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, SecondActivity.class);
                intent.putExtra("some_key", someValue);
                // Pornim intent, adaugam un int pe care il vom folosi ca filtru
                // in onActivityResult
                startActivityForResult(intent, ANOTHER_ACTIVITY_REQUEST_CODE);
            }
    });
}
// Aici vom extrage un bundle ce contine datele intoarse de `SecondActivity`
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
  switch(requestCode) {
    case ANOTHER_ACTIVITY_REQUEST_CODE:
      if (resultCode == Activity.RESULT_OK) {
        Bundle data = intent.getExtras();
      }
      break;
      // process other request codes
  }
}

În activitatea copil, înainte de apelul metodei finish(), va trebui transmis activității părinte codul de rezultat (Activity.RESULT_OK, Activity.RESULT_CANCELED sau orice fel de rezultat de tip întreg) și obiectul de tip intenție care conține datele (opțional, în situația în care trebuie întoarse rezultate explicit), ca parametrii ai metodei setResult(). La polul opus, activitatea SecondActivity va prelua datele astfel:

@Override
protected void onCreate(Bundle state) {
  super.onCreate(state);
  setContentView(R.layout.second_activity);

  /* Get intent from parent */
  Intent intentFromParent = getIntent();
  Bundle data = intentFromParent.getExtras();
  /* data has now some_key which we can use */
  
  /* Return some data to the calling Intent */
  Intent intentToParent = new Intent();
  intent.putExtra("another_key", anotherValue);
  setResult(RESULT_OK, intentToParent);
  finish();

ResultsLauncher API

onActivityResult e deprecated. Google recomandă folosirea ActivityResultsLauncherAPI. Aceasta nouă abordare este mai sigură și mai ușor de gestionat, deoarece elimină necesitatea de a utiliza coduri de cerere hardcoded și simplifică gestionarea callback-urilor pentru rezultatele activității.

Activitatea părinte (MainActivity) În activitatea părinte, înlocuiește startActivityForResult() și onActivityResult() cu utilizarea unui ActivityResultLauncher.

class MainActivity : AppCompatActivity() {
    // Definește un ActivityResultLauncher
    private lateinit var startForResult: ActivityResultLauncher<Intent>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Inițializează launcher-ul cu un callback pentru rezultat
        startForResult = registerForActivityResult(StartActivityForResult()) { result ->
            if (result.resultCode == Activity.RESULT_OK) {
                // Procesează rezultatul aici
                val data: Intent? = result.data
                val someData = data?.getStringExtra("another_key")
                // Folosește `someData` cum este necesar
            }
        }

        val btn = findViewById<Button>(R.id.open_activity_button)
        btn.setOnClickListener {
            val intent = Intent(this, SecondActivity::class.java).apply {
                putExtra("some_key", "someValue")
            }
            // Lansează activitatea copil folosind launcher-ul
            startForResult.launch(intent)
        }
    }
}

Activitatea Copil (SecondActivity)

class SecondActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.second_activity)

        // Presupunând că vrei să returnezi rezultatul la un anumit eveniment, de exemplu, la apăsarea unui buton
        val someButton: Button = findViewById(R.id.some_button)
        someButton.setOnClickListener {
            val returnIntent = Intent().apply {
                putExtra("another_key", "anotherValue")
            }
            setResult(RESULT_OK, returnIntent)
            finish()
        }

        // Dacă activitatea se încheie fără a seta explicit un rezultat, poți să nu faci nimic sau să setezi RESULT_CANCELED
    }
}

Broadcast

Deseori, vom fi nevoiți să trimitem date fie de la o aplicație la alta. Pentru acest lucru vom folosi noțiunea de Broadcast. Acestea reprezintă o altă formă de IPC (Inter-Process Communication).

Aplicațiile Android pot trimite sau primi mesaje de emisie atât de la sistemul Android, cât și de la alte aplicații Android, similar cu modelul de proiectare publicare-abonare. Aceste emisiuni sunt trimise atunci când are loc un eveniment de interes. De exemplu, sistemul Android trimite emisiuni când apar diverse evenimente de sistem, cum ar fi atunci când sistemul pornește sau dispozitivul începe să se încarce. Aplicațiile pot trimite, de asemenea, emisiuni personalizate, de exemplu, pentru a notifica alte aplicații despre ceva care ar putea fi de interes pentru ele (de exemplu, s-au descărcat date noi).

Sistemul optimizează livrarea emisiunilor pentru a menține sănătatea optimă a sistemului. Prin urmare, nu se garantează timpurile de livrare a emisiunilor. Aplicațiile care necesită comunicare între procese cu latență scăzută ar trebui să ia în considerare utilizarea serviciilor legate.

Aplicațiile se pot înregistra pentru a primi emisiuni specifice. Când este trimisă o emisiune, sistemul direcționează automat emisiunile către aplicațiile care s-au abonat să primească acel tip particular de emisiune.

În general, emisiunile pot fi folosite ca un sistem de mesagerie între aplicații și în afara fluxului normal de utilizare. Cu toate acestea, trebuie să fii atent să nu abuzezi de oportunitatea de a răspunde la emisiuni și de a rula sarcini în fundal care pot contribui la o performanță lentă a sistemului.

Trimiterea unei intenții de tip broadcast

Construirea unei intenții care urmează să fie difuzată la nivelul sistemului de operare Android poate fi realizată prin definirea unui obiect de tipul Intent, pentru care se vor specifica acțiunea, datele și categoria, astfel încât obiectele de tip ascultător să îl poată identifica cât mai exact. Ulterior, acesta va fi trimis tuturor proceselor aferente aplicațiilor instalate pe dispozitivul mobil prin intermediul metodei sendBroadcast(), căreia îi este atașat ca parametru.


Note

Pot fi utilizate atât acțiuni predefinite (care vor fi procesate atât de aplicațiile Android native cât și de eventuale aplicații instalate din alte surse) cât și acțiuni definite de utilizator, pentru care trebuie implementate aplicații dedicate, responsabile cu procesarea acestora.




final public static String SOME_ACTION = "ro.pub.cs.systems.eim.lab04.SomeAction.SOME_ACTION";

Intent intent = new Intent(SOME_ACTION);
intent.putExtra("someKey", someValue);
sendBroadcast(intent);



companion object {
    const val SOME_ACTION = "ro.pub.cs.systems.eim.lab04.SomeAction.SOME_ACTION"
}

val intent = Intent(SOME_ACTION).apply {
    putExtra("someKey", someValue)
}
sendBroadcast(intent)

Primirea unui intenții cu difuzare

Pentru a putea primi o intenție cu difuzare, o componentă trebuie să fie înregistrată în acest sens, definind un filtru de intenții pentru a specifica ce tipuri de acțiuni și ce tipuri de date asociate intenției poate procesa.

Acesta poate fi precizat:

  • în fișierul AndroidManifest.xml (caz în care nu este necesar ca aplicația să ruleze la momentul în care se produce evenimentul cu difuzare pentru a-l putea procesa); elementul <receiver> trebuie să conțină în mod obligatoriu filtrul de intenții prin care se indică acțiunea care poate fi procesată:
    <manifest ... >
      <application ... >
        <receiver
          android:name=".SomeEventBroadcastReceiver">
          <intent-filter>
            <action android:name="ro.pub.cs.systems.eim.lab04.SomeAction.SOME_ACTION" />
          </intent-filter> 
        </receiver>
      </application>
    </manifest>
    
  • programatic, în codul sursă (caz în care aplicația trebuie să fie în execuție la momentul în care se produce evenimentul cu difuzare pentru a-l putea procesa); o astfel de abordare este utilă când procesarea intenției cu difuzare implică actualizarea unor componente din cadrul interfeței grafice asociate activității: `private SomeEventBroadcastReceiver someEventBroadcastReceiver = new SomeEventBroadcastReceiver(); private IntentFilter intentFilter = new IntentFilter(SOME_ACTION);
<pre><code class="language-java">

@Override
protected void onResume() {
  super.onResume();
  
  registerReceiver(someEventBroadcastReceiver, intentFilter);
}

@Override
protected void onPause() {
  super.onPause();
  
  unregisterReceiver(someEventBroadcastReceiver);
}



override fun onResume() {
    super.onResume()
    
    registerReceiver(someEventBroadcastReceiver, intentFilter)
}

override fun onPause() {
    super.onPause()
    
    unregisterReceiver(someEventBroadcastReceiver)
}


O clasă capabilă să proceseze intenții cu difuzare este derivată din android.content.BroadcastReceiver, implementând metoda onReceive() pe care realizează rutina de tratare propriu-zisă:



import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

public class SomeEventBroadcastReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent intent) {
    // ...
  }
}



import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent

class SomeEventBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        // ...
    }
}

Metoda onReceive() va fi invocată în mod automat în momentul în care este primită o intenție cu difuzare, fiind executată pe firul de execuție principal al aplicației. De regulă, în cadrul acestei metode utilizatorul este anunțat asupra producerii evenimentului prin intermediul serviciului de notificare (Notification Manager), este lansat în execuție un serviciu sau sunt actualizate componente din cadrul interfeței grafice.

Activitate de Laborator

Se dorește implementarea unei aplicații Android, conținând o activitate care să ofere utilizatorilor funcționalitatea necesară pentru a stoca un număr de telefon în agenda de contacte, specificând pentru acesta mai multe informații. Pe langa documentatia din laborator, vom folosi documentatia oficiala Android despre Intents si Broadcasts.

Small Contacts Manager Large Contacts Manager

1. Să se creeze un proiect Android Studio denumit ContactsManager (se selectează Start a new Android Studio project -> Empty View Activity -> pentru proiect XML).

2. În fișierul activity_contacts_manager din directorul res/layout să se construiască interfața grafică folosind:

  • editorul vizual (Graphical Layout)
  • editorul XML (Text)

Acesta va fi format din două containere după cum urmează:

  • primul conține mai multe elemente dispuse vertical și ocupând pe lățime întregul spațiu avut la dispoziție:
    • un buton (Button) având mesajul Show Additional Fields în cazul în care celălalt container nu este afișat, respectiv mesajul Hide Additional Fields în cazul în care celălalt container este afișat, determinând atașarea / detașarea acestuia la activitate;
    • patru controale de tip câmpuri text (EditText) prin care se introduc:
      • numele;
      • numărul de telefon - acest câmp este dezactivat (are proprietatea android:enabled="false"), urmând ca valoarea sa să fie preluată din câmpul extra al unei intenții;
      • adresa electronică;
      • adresa poștală.
  • cel de-al doilea container (care nu este vizibil inițial) conține patru controale de tip câmpuri text dispuse vertical și ocupând pe lățime întregul spațiu avut la dispoziție, prin care se introduc:
    • poziția ocupată;
    • denumirea companiei;
    • site-ul web;
    • identificatorul pentru mesagerie instantanee.

Fiecare container poate fi inclus într-un mecanism de dispunere a conținutului de tip LinearLayout cu dispunere verticală. Acestea vor fi incluse într-un container de tip ScrollView, pentru a preîntâmpina situația în care interfața grafică nu poate fi afișată pe ecranul dispozitivului mobil.

Să se implementeaze interacțiunea cu utilizatorul a aplicației.

  • în metoda onCreate() a activității se obțin referințe către butoanele Show Additional Details / Hide Additional Details, respectiv Save și Cancel prin intermediul metodei findViewById(R.id....);
  • se implementează o clasă ascultător pentru butoane, care implementează View.OnClickListener și implementează metoda onClick(View v); în funcție de id-ul butonului care este transmis argument metodei, sunt realizate următoarele acțiuni:
    • butonul Show Additional Details / Hide Additional Details - afișează / ascunde al doilea container în funcție de starea curentă , modificând corespunzător textul afișat pe buton. (Hint: atributul visibility al containerului, resepctiv metoda setVisibility() a clasei Java împreună cu constantele View.VISIBLE, View.GONE).
    • butonul Save - lansează în execuție aplicația Android nativă pentru stocarea unui contact în agenda telefonică, după ce în prealabil au fost preluate informațiile din controalele grafice:

        Intent intent = new Intent(ContactsContract.Intents.Insert.ACTION);
        intent.setType(ContactsContract.RawContacts.CONTENT_TYPE);
        if (name != null) {
        intent.putExtra(ContactsContract.Intents.Insert.NAME, name);
        }
        if (phone != null) {
        intent.putExtra(ContactsContract.Intents.Insert.PHONE, phone);
        }
        if (email != null) {
        intent.putExtra(ContactsContract.Intents.Insert.EMAIL, email);
        }
        if (address != null) {
        intent.putExtra(ContactsContract.Intents.Insert.POSTAL, address);
        }
        if (jobTitle != null) {
        intent.putExtra(ContactsContract.Intents.Insert.JOB_TITLE, jobTitle);
        }
        if (company != null) {
        intent.putExtra(ContactsContract.Intents.Insert.COMPANY, company);
        }
        ArrayList<ContentValues> contactData = new ArrayList<ContentValues>();
        if (website != null) {
        ContentValues websiteRow = new ContentValues();
        websiteRow.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE);
        websiteRow.put(ContactsContract.CommonDataKinds.Website.URL, website);
        contactData.add(websiteRow);
        }
        if (im != null) {
        ContentValues imRow = new ContentValues();
        imRow.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE);
        imRow.put(ContactsContract.CommonDataKinds.Im.DATA, im);
        contactData.add(imRow);
        }
        intent.putParcelableArrayListExtra(ContactsContract.Intents.Insert.DATA, contactData);
        startActivity(intent);



                val intent = Intent(ContactsContract.Intents.Insert.ACTION).apply {
            setType(ContactsContract.RawContacts.CONTENT_TYPE)
            
            name?.let {
                putExtra(ContactsContract.Intents.Insert.NAME, it)
            }
            
            phone?.let {
                putExtra(ContactsContract.Intents.Insert.PHONE, it)
            }
            
            email?.let {
                putExtra(ContactsContract.Intents.Insert.EMAIL, it)
            }
            
            address?.let {
                putExtra(ContactsContract.Intents.Insert.POSTAL, it)
            }
            
            jobTitle?.let {
                putExtra(ContactsContract.Intents.Insert.JOB_TITLE, it)
            }
            
            company?.let {
                putExtra(ContactsContract.Intents.Insert.COMPANY, it)
            }
        }

        val contactData = ArrayList().apply {
            website?.let {
                add(ContentValues().apply {
                    put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE)
                    put(ContactsContract.CommonDataKinds.Website.URL, it)
                })
            }
            
            im?.let {
                add(ContentValues().apply {
                    put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE)
                    put(ContactsContract.CommonDataKinds.Im.DATA, it)
                })
            }
        }

        intent.putParcelableArrayListExtra(ContactsContract.Intents.Insert.DATA, contactData)
        startActivity(intent)

    - Intenția pentru realizarea acestei operații are asociată
    acțiunea `ContactsContract.Intents.Insert.ACTION` și tipul
    `ContactsContract.RawContacts.CONTENT_TYPE`. Informațiile care
    se doresc a fi completate sunt atașate în câmpul `extra` al
    acesteia, având cheile:  
     - ✔ `ContactsContract.Intents.Insert.NAME`;  
     - ✔ `ContactsContract.Intents.Insert.PHONE`;  
     - ✔ `ContactsContract.Intents.Insert.EMAIL`;  
     - `ContactsContract.Intents.Insert.POSTAL`;  
     - ✔ `ContactsContract.Intents.Insert.JOB_TITLE`;  
     - ✔ `ContactsContract.Intents.Insert.COMPANY`;  
    Pentru site-ul web și identificatorul de mesagerie instantanee,
    se folosește un tablou de elemente `ContentValues` în care se
    specifică înregistrări de tipul `CommonDataKinds.Website.URL`,
    respectiv `CommonDataKinds.Im.DATA`;  
    Pentru a putea gestiona agenda telefonică, este necesar ca în
    fișierul `AndroidManifest.xml` să fie specificate următoarele
    permisiuni: `
    ```xml
    <uses-permission
      android:name="android.permission.READ_CONTACTS" />
    <uses-permission
      android:name="android.permission.WRITE_CONTACTS" />
    ```
    `
-   butonul *Cancel* - termină aplicația Android: `finish();`
  • se înregistrează o instanță a clasei ascultător ca mecanism de tratare a evenimentelor de tip accesare a butoanelor din cadrul interfeței grafice, prin apelul metodei setOnClickListener().

5. Să se modifice aplicația Android Phone Dialer astfel încât să conțină un buton suplimentar prin care este invocată aplicația Contacts Manager căreia îi transmite numărul de telefon format și așteptând un rezultat cu privire la stocarea contactului în agenda telefonică.

Ca imagine pentru butonul care invocă aplicația Contacts Manager se poate folosi această resursă.

Metoda de tratare a evenimentului de tip accesare a butonului de stocare a numărului de telefon în agenda telefonică invocă o intenție asociată aplicației Contacts Manager, transmițând și numărul de telefon în câmpul extra asociat acesteia, identificabil prin intermediul unei chei.

String phoneNumber = phoneNumberEditText.getText().toString();
if (phoneNumber.length() > 0) {
  Intent intent = new Intent("ro.pub.cs.systems.eim.lab04.contactsmanager.intent.action.ContactsManagerActivity");
  intent.putExtra("ro.pub.cs.systems.eim.lab04.contactsmanager.PHONE_NUMBER_KEY", phoneNumber);
  startActivityForResult(intent, Constants.CONTACTS_MANAGER_REQUEST_CODE);
} else {
  Toast.makeText(getApplication(), getResources().getString(R.string.phone_error), Toast.LENGTH_LONG).show();
}

Definiți în prealabil constanta CONTACTS_MANAGER_REQUEST_CODE și valoarea string phone_error în fișierele corespunzătoare din folderul de resurse statice res.

6. Să se modifice aplicația Android Contacts Manager astfel încât să poată fi lansată în execuție doar din contextul altei activități, prin intermediul unei intenții care conține în câmpul extra un număr de telefon, identificabil prin cheia ro.pub.cs.systems.eim.lab04.contactsmanager.PHONE_NUMBER_KEY, acesta fiind plasat în câmpul text needitabil corespunzător. Totodată, va transmite înapoi rezultatul operației de stocare (Activity.RESULT_OK sau Activity.RESULT_CANCELED).

  • în fișierul AndroidManifest.xml se modifică filtrul de intenții (acțiunea și categoria), astfel încât activitatea să poată fi rulată doar prin intermediul unei intenții

    <manifest ...>
      <application ...>
        <activity
          android:name=".graphicuserinterface.ContactsManagerActivity"
          android:label="@string/app_name" >
          <intent-filter>
            <action android:name="ro.pub.cs.systems.eim.lab04.contactsmanager.intent.action.ContactsManagerActivity" />
            <category android:name="android.intent.category.DEFAULT" />
          </intent-filter>
        </activity>
      </application>
    </manifest>
    

    `

  • în metoda onCreate() a activității aplicației ContactsManager este verificată intenția cu care este pornită, și în cazul în care aceasta nu este nulă, este preluată informația din secțiunea extra, identificată prin cheia ro.pub.cs.systems.eim.lab04.contactsmanager.PHONE_NUMBER_KEY, conținutul său fiind plasat în cadrul câmpului text corespunzător:

    Intent intent = getIntent();
    if (intent != null) {
      String phone = intent.getStringExtra("ro.pub.cs.systems.eim.lab04.contactsmanager.PHONE_NUMBER_KEY");
      if (phone != null) {
        phoneEditText.setText(phone);
      } else {
        Toast.makeText(this, getResources().getString(R.string.phone_error), Toast.LENGTH_LONG).show();
      }
    }
    
  • pe metodele de tratare a evenimentelor de accesare a butoanelor:

    • Save - este lansată în execuție aplicația nativă pentru gestiunea agendei telefonice, folosind un cod de cerere prin intermediul căruia se va verifica rezultatul furnizat: startActivityForResult(intent, Constants.CONTACTS_MANAGER_REQUEST_CODE);
    • Cancel - se transmite înapoi rezultatul setResult(Activity.RESULT_CANCELED, new Intent());
  • în metoda onActivityResult() asociată activității aplicației ContactsManager, în momentul în care s-a părăsit aplicația nativă pentru gestiunea agendei telefonice, se verifică codul de cerere și se transmite înapoi un rezultat:
    `public void onActivityResult(int requestCode, int resultCode, Intent intent) {
    switch(requestCode) {
      case Constants.CONTACTS_MANAGER_REQUEST_CODE:
        setResult(resultCode, new Intent());
        finish();
        break;
      }
    }
    
    `

Datorită faptului că aplicația Android Contacts Manager nu dispune de o activitate principală (implicită), aceasta nu va mai putea fi lansată în execuție folosind mediul integrat de dezvoltare Android Studio. Pentru ca aceasta să fie doar instalată pe dispozitivul mobil, se accesează meniul RunEdit Configurations..., iar în secțiunea Launch Options de pe panoul General, se selectează opțiunea Launch: Nothing.

12android_studio_run_menu.png

  • Să se verifice în emulator/telefon faptul că ContactsManager nu este o aplicație care poate fi lansată din launcher. Dacă aveți utilitarul MyAndroidTools, acesta va lista aplicația cu Activitatea definită.
  • Să se verifice în emulator/telefon pachetele instalate
student@eg106:~$ aapt l -a ./app/build/outputs/apk/debug/app-debug.apk | sed -n -e '/manifest/,$p'  
student@eg106:~$ adb shell 
vbox86p:/ # pm list packages -f
vbox86p:/ # dumpsys package | grep  'eim'| grep Activity
vbox86p:/ # 

Servicii

Un Service este o componentă a aplicației care poate efectua operațiuni de lungă durată în fundal. Nu oferă o interfață pentru utilizator. Odată pornit, un service poate continua să ruleze pentru o anumită perioadă de timp, chiar și după ce utilizatorul trece la o altă aplicație. În plus, o componentă se poate lega de un service pentru a interacționa cu acesta și chiar pentru a efectua comunicații între procese (IPC). De exemplu, un service poate gestiona tranzacții de rețea, poate reda muzică, poate efectua operațiuni de fișier I/O sau poate interacționa cu un furnizor de conținut, toate din fundal.

Un serviciu nu trece prin evenimentele ce fac parte din ciclul de viață al unei activități. Totuși, un serviciu poate fi controlat (pornit, oprit) din contextul altor componente ale unei aplicații Android (activități, ascultători de intenții cu difuzare, alte servicii).

Serviciul este rulat pe firul de execuție principal al aplicației Android. De aceea, în situația în care operațiile pe care le realizează poate influența experiența utilizatorului, acestea trebuie transferate pe alte fire de execuție din fundal (folosind clasele HandlerThread sau AsyncTask). Ar trebui să rulați orice operațiuni blocante pe un thread separat în cadrul service-ului pentru a evita erorile Application Not Responding (ANR).

Un serviciu continuă să se ruleze chiar și în situația în care componenta aplicației Android care l-a invocat prin intermediul unei intenții devine inactivă (nu mai este vizibilă) sau chiar este distrusă. Acest comportament este adecvat în situația în care serviciul realizează operații de intrare/ieșire intensive, interacționează cu diverse servicii accesibile prin intermediul rețelei sau cu furnizori de conținut.

Tipuri de Servicii

În programarea Android, există trei tipuri de servicii:

  1. de tip Foreground - Un serviciu foreground efectuează operații care sunt vizibile pentru utilizator. De exemplu, o aplicație audio ar folosi un serviciu foreground pentru a reda o piesă audio. Serviciile foreground trebuie să afișeze o Notification. Serviciile foreground continuă să ruleze chiar și când utilizatorul nu interacționează cu aplicația. Când folosești un serviciu foreground, trebuie să afișezi o notificare pentru ca utilizatorii să fie conștienți că serviciul rulează. Această notificare nu poate fi închisă decât dacă serviciul este oprit sau eliminat din foreground.

  2. de tip Background - Un serviciu background efectuează o operație care nu este observată direct de utilizator. De exemplu, dacă o aplicație folosește un serviciu pentru a-și compacta spațiul de stocare, acesta ar fi de obicei un serviciu background.

  3. de tip Bound - Un serviciu este bound când o componentă a aplicației se leagă de acesta prin apelarea bindService(). Un serviciu bound oferă o interfață client-server care permite componentelor să interacționeze cu serviciul, să trimită cereri, să primească rezultate și chiar să facă acest lucru între procese prin comunicare între procese (IPC). Un serviciu bound rulează doar atât timp cât o altă componentă a aplicației este legată de acesta. Mai multe componente se pot lega de serviciu simultan, dar când toate se dezleagă, serviciul este distrus.

Gestiunea unui Serviciu

Pentru a crea un serviciu, trebuie să creezi o subclasă a Service sau să folosești una dintre subclasele existente ale acesteia. În implementare, trebuie să suprascrii câteva metode callback care gestionează aspecte cheie ale ciclului de viață al serviciului și să oferi un mecanism care permite componentelor să se lege de serviciu, dacă este cazul.

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;

public class SomeStartedService extends Service {

  private int startMode;

  @Override
  public void onCreate() {
    super.onCreate();
    // ...
  }

  @Override
  public int onStartCommand(Intent intent, 
                            int flags,
                            int startId) {
    // start a new thread here and run your code
    // from here you can send broadcasts to update the UI in a interface
    return startMode;
  }

  @Override
  public IBinder onBind(Intent intent) {
    // ...
    return null;
  }
  
  @Override
  public void onDestroy() {
    super.onDestroy();
    // ...
  }
}

Acestea sunt cele mai importante metode callback pe care ar trebui să le suprascrii:

  • onCreate() - Sistemul invocă această metodă pentru a efectua proceduri de configurare care au loc o singură dată când serviciul este creat inițial (înainte de a apela fie onStartCommand() sau onBind()). Dacă serviciul rulează deja, această metodă nu este apelată.

  • onStartCommand() - Sistemul invocă această metodă prin apelarea startService() când o altă componentă (cum ar fi o activitate) solicită pornirea serviciului. Când această metodă se execută, serviciul este pornit și poate rula în background pe termen nedefinit. Dacă implementezi această metodă, este responsabilitatea ta să oprești serviciul când munca sa este completă prin apelarea stopSelf() sau stopService(). Dacă dorești doar să oferi binding, nu trebuie să implementezi această metodă.

  • onBind() - Sistemul invocă această metodă prin apelarea bindService() când o altă componentă dorește să se lege de serviciu (cum ar fi pentru a efectua Remote Procedure Calls (RPC)). În implementarea acestei metode, trebuie să oferi o interfață pe care clienții o folosesc pentru a comunica cu serviciul prin returnarea unui IBinder. Trebuie să implementezi întotdeauna această metodă; totuși, dacă nu dorești să permiți binding-ul, ar trebui să returnezi null.

  • onDestroy() - Sistemul invocă această metodă când serviciul nu mai este folosit și urmează să fie distrus. Serviciul tău ar trebui să implementeze această metodă pentru a elibera orice resurse precum thread-uri, listeners înregistrați sau receivers. Aceasta este ultima apelare pe care serviciul o primește.

Declararea unui serviciu în manifest

Pentru a putea fi utilizat, orice serviciu trebuie să fie declarat în cadrul fișierului AndroidManifest.xml, prin intermediul etichetei <service> în cadrul elementului <application>. Eventual, se poate indica o permisiune necesară pentru pornirea și oprirea serviciului, astfel încât aceste operații să poată fi realizate numai de anumite aplicații Android.

<manifest ...>
  <application ...>
    <service
      android:name="ro.pub.cs.systems.eim.lab05.SomeService"
      android:enabled="true"
      android:exported="true"
      android:permission="ro.pub.cs.systems.eim.lab05.SOME_SERVICE_PERMISSION" />
  </application>
</manifest>

Intalnim mai multe campuri de configurare posibile:

  • android:name este singurul obligatoriu în cadrul elementului <service>, desemnând clasa care gestionează operațiile specifice serviciului respectiv. Din momentul în care aplicația Android este publicată, această valoare nu mai poate fi modificată, întrucât poate avea un efect asupra componentelor care utilizează acest serviciu prin intermediul unei intenții explicite folosită la pornirea serviciului sau la asocierea componentei cu serviciul respectiv.

  • android:enabled indică dacă serviciul poate fi instanțiat de către sistemul de operare.

  • android:exported specifică posibilitatea ca alte componente (aparținând altor aplicații) să poată interacționa cu serviciul. În situația în care serviciul nu conține filtre de intenții, acesta poate fi invocat numai prin precizarea explicită a numelui clasei care îl gestionează (calificată complet), valoarea sa fiind false, fiind destinat invocării din contextul unor componente aparținând aceleiași aplicații Android ca și el. În cazul în care serviciul definește cel puțin un filtru de intenții, valoarea sa este true, fiind destinat invocării din contextul altor aplicații Android.

  • android:permission precizează denumirea unei permisiuni pe care entitatea trebuie să o dețină pentru a putea lansa în execuție un serviciu sau pentru a i se putea asocia. Aceasta trebuie indicată în cadrul intențiilor transmise ca argumente metodelor utilizate pentru a invoca serviciul respectiv (startService(), respectiv bindService()), altfel intenția respectivă nu va fi livrată către serviciu.

Exemplu de serviciu

Un serviciu started este unul pe care o altă componentă îl pornește prin apelarea startService(), care rezultă într-o apelare a metodei onStartCommand() a serviciului.

Când un serviciu este pornit, acesta are un ciclu de viață independent de componenta care l-a pornit. Serviciul poate rula în background pe termen nedefinit, chiar dacă componenta care l-a pornit este distrusă. Ca atare, serviciul ar trebui să se oprească singur când sarcina sa este completă prin apelarea stopSelf(), sau o altă componentă îl poate opri prin apelarea stopService().

O componentă a aplicației, cum ar fi o activitate, poate porni serviciul prin apelarea startService() și transmiterea unui Intent care specifică serviciul și include orice date pe care serviciul să le folosească. Serviciul primește acest Intent în metoda onStartCommand().

De exemplu, să presupunem că o activitate trebuie să salveze niște date într-o bază de date online. Activitatea poate porni un serviciu însoțitor și să-i livreze datele de salvat prin transmiterea unui intent către startService(). Serviciul primește intent-ul în onStartCommand(), se conectează la Internet și efectuează tranzacția în baza de date. Când tranzacția este completă, serviciul se oprește singur și este distrus.

public class HelloService extends Service {
  private Looper serviceLooper;
  private ServiceHandler serviceHandler;

  // A handler is a cool class that we can use to send and receive
  // messages between objects. For example, this is a 
  // Handler that receives messages from the thread
  // Combined with a Looper it's a easy way to run some work on a thread and
  // get the results back via the handler.
  private final class ServiceHandler extends Handler {
      public ServiceHandler(Looper looper) {
          super(looper);
      }
      @Override
      public void handleMessage(Message msg) {
          // Normally we would do some work here, like download a file.
          // For our sample, we just sleep for 5 seconds.
          try {
              Thread.sleep(5000);
          } catch (InterruptedException e) {
              // Restore interrupt status.
              Thread.currentThread().interrupt();
          }
          // Stop the service using the startId, so that we don't stop
          // the service in the middle of handling another job
          stopSelf(msg.arg1);
      }
  }

  @Override
  public void onCreate() {
    // Start up the thread running the service. Note that we create a
    // separate thread because the service normally runs in the process's
    // main thread, which we don't want to block. We also make it
    // background priority so CPU-intensive work doesn't disrupt our UI.
    HandlerThread thread = new HandlerThread("ServiceStartArguments",
            Process.THREAD_PRIORITY_BACKGROUND);
    thread.start();

    // Get the HandlerThread's Looper and use it for our Handler
    // The looper is basically the "code" that loops for the thread,
    serviceLooper = thread.getLooper();
    serviceHandler = new ServiceHandler(serviceLooper);
  }

  @Override
  public int onStartCommand(Intent intent, int flags, int startId) {
      Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show();

      // For each start request, send a message to start a job and deliver the
      // start ID so we know which request we're stopping when we finish the job
      Message msg = serviceHandler.obtainMessage();
      msg.arg1 = startId;
      serviceHandler.sendMessage(msg);

      // If we get killed, after returning from here, restart
      return START_STICKY;
  }

  @Override
  public IBinder onBind(Intent intent) {
      // We don't provide binding, so return null
      return null;
  }

  @Override
  public void onDestroy() {
    Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show();
  }
}

LifeCycle

Se observă faptul că ciclul de viață al unui serviciu are loc între metodele onCreate() și onDestroy(). De cele mai multe ori, este necesar ca un serviciu să folosească unul sau mai multe fire de execuție (dedicate) astfel încât să nu influențeze responsivitatea aplicației Android. Într-o astfel de situație, firul de execuție va fi pornit pe metoda onCreate() și va fi oprit pe metoda onDestroy(). De asemenea, în cadrul metodei onCreate() au loc diferite operații de configurare (inițializări), în timp ce în cadrul metodei onDestroy() realizează eliberarea resurselor folosite. Metodele onCreate() și onDestroy() sunt invocate pentru toate tipurile de servicii, atât pentru cele de tip started, cât și pentru cele de tip bounded.

În cazul unui serviciu de tip started, perioada activă din ciclul de viață este cuprinsă între apelul metodei onStartCommand() (apelată în mod automat atunci când o componentă apelează metoda startService(), primind ca argument obiectul de tip Intent care a fost folosit la invocarea sa) și apelul metodei onDestroy() (apelat atunci când serviciul este oprit, prin intermediul uneia dintre metodele stopSelf() sau stopService()).

În cazul unui serviciu de tip bounded, perioada activă din ciclul de viață este cuprinsă între apelul metodei onBind() (apelată în mod automat atunci când o componentă apelează metoda bindService(), primind ca argument obiectul de tip Intent care a fost folosit la invocarea sa) și apelul metodei onUnbind() (apelată în mod automat atunci când toate componentele asociate serviciului au apelat metoda unbindService()).

Pornirea unui Serviciu

Un serviciu este pornit printr-un apel al metodei startService(). Aceasta primește ca parametru un obiect de tip Intent care poate fi creat:

  • explicit, pe baza denumirii clasei care implementează serviciul respectiv;
    Intent intent = new Intent(this, SomeService.class);
    startService(intent);
    
  • implicit, indicând componenta care gestionează serviciul respectiv (se va indica atât denumirea pachetului cât și denumirea clasei ca argument al metodei setComponent() asociat intenției respective):
    Intent intent = new Intent();
    intent.setComponent(new ComponentName("SomePackage", "SomeService"));
    startService(intent);
    

Transmiterea de informații suplimentare către serviciu poate fi realizată prin intermediul metodelor putExtras(Bundle), putExtra(String, Parcelable) sau put<type>Extra(String, <type>).

Metoda startService() presupune livarea unui mesaj asincron la nivelul sistemului de operare Android, astfel încât aceasta se execută instantaneu. Fiecare apel al acestei metode invocarea, în mod automat, al metodei onStartCommand().

De regulă, comunicația dintre o componentă și un serviciu este unidirecțională, dintre componenta care invocă către serviciul invocat, prin intermediul datelor încapsulate în intenție. În situația în care se dorește ca serviciul să transmită date către componentă, se utilizează un obiect de tip PendingIntent, prin intermediul căruia pot fi transmise mesaje cu difuzare.

Threads

Când o aplicație este lansată, sistemul creează un thread de execuție pentru aplicație, numit thread principal. Acest thread este foarte important, deoarece este responsabil cu dispecerizarea evenimentelor către widget-urile corespunzătoare ale interfeței utilizator, inclusiv evenimentele de desenare. De asemenea, este aproape întotdeauna thread-ul în care aplicația ta interacționează cu componentele din pachetele android.widget și android.view ale kit-ului UI Android. Din acest motiv, thread-ul principal este numit uneori thread-ul UI. Totuși, în circumstanțe speciale, thread-ul principal al unei aplicații ar putea să nu fie thread-ul său UI.

Din cauza acestui model cu un singur thread, este vital pentru receptivitatea interfeței utilizator a aplicației tale să nu blochezi thread-ul UI. Dacă ai operații de efectuat care nu sunt instantanee, asigură-te că le faci în thread-uri background sau worker separate. Doar ține minte că nu poți actualiza interfața utilizator din niciun alt thread în afară de thread-ul UI sau thread-ul principal.

Acesta este un exemplu simplu de cum putem folosi declara un thread.

public class ProcessingThread extends Thread {
  // we can pass data to the thread through the constructor
  public ProcessingThread() {
  }

  @Override
  public void run() {
    while(true){ 
        // we perform some work on the thread
    }
  }
}

Pentru a il folosi dintr-un serviciu vom face astfel:

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    Log.d(Constants.TAG, "onStartCommand() method was invoked");
    ProcessingThread processingThread = new ProcessingThread();
    processingThread.start();
}

Prioritatea unui Serviciu

În situația în care este necesar, sistemul de operare Android poate solicita memoria utilizată de un anumit serviciu, în funcție de prioritatea pe care acesta o are.

Un serviciu de tip started are o prioritate mai mare decât o activitate inactivă, dar mai mică decât o activitate activă. Din acest motiv, este necesar ca programatorul să trateze cazurile în care serviciul este repornit, ca urmare a distrugerii sale de către sistemul de operare Android, în vederea satisfacerii necesarului de memorie.

Un serviciu de tip bounded are de obicei o prioritate similară cu a activității cu care interacționează, deși aceasta poate fi controlată prin intermediul unor parametrii transmiși în momentul în care este realizată legătura. Este puțin probabil ca un serviciu atașat unei activități active să fie distrus la fel cum este destul de probabil ca un serviciu pentru care toate activitățile atașate sunt inactive să fie distrus.

Un serviciu care rulează în prim-plan nu este aproape niciodată distrus.

Activitate de Laborator

1. Vom porni de la urmatorul schelet.

2. Astazi vom lucra cu doua proiecte in paralel, proiectel StartedService, respectiv StartedServiceActivity din directorul labtasks/StartedService.

  • Proiectul StartedService conține codul sursă pentru un serviciu de tip started care transmite mai multe valori, de diferite tipuri (șir de caractere, întreg, vector), temporizate la un anumit interval (dată de valoarea SLEEP_TIME din interfața Constants). Aceste valori sunt transmise prin intermediul unor broadcast intents, la nivelul întregului sistem de operare Android.

Aplicația StartedService nu are o activitate atașată. Astfel, aplicația pornită nu va avea o interfață grafică. Când o pornim, nu vom observa nimic pe partea de GUI.

  • Proiectul StartedServiceActivity conține codul sursă pentru o aplicație Android care utilizează un ascultător pentru intenții cu difuzare (eng. BroadcastReceiver), pentru tipurile de mesaje propagate la nivelul sistemului de operare de către serviciu, pe care le afișează în interfața grafică, prin intermediul unui câmp text.

3. În proiectul StartedService, în clasa StartedService din pachetul ro.pub.cs.systems.eim.lab05.startedservice.service, să se completeze metoda onStartCommand() astfel încât aceasta să pornească un thrad în cadrul căruia să fie trimise 3 broadcast intents la nivelul sistemului de operare Android.

Pentru fiecare broadcast intent, se vor specifica:

  • acțiunea, care va avea valorile definite în interfața Constants (Constants.ACTION_STRING, Constants.ACTION_INTEGER, Constants.ACTION_ARRAY_LIST); se va utiliza metoda setAction();
  • informațiile transmise, plasate în câmpul extra (având cheia Constants.DATA și valoarea dată de Constants.STRING_DATA, Constants.INTEGER_DATA, Constants.ARRAY_LIST_DATA); se va utiliza metoda putExtra() care primește ca argumente cheia și valoarea.

Transmiterea propriu-zisă a intenției se face prin intermediul metodeih sendBroadcast().

Intent intent = new Intent();
intent.setAction("com.example.broadcast.MY_NOTIFICATION");
intent.putExtra("data", "Nothing to see here, move along.");
sendBroadcast(intent);

Cele trei mesaje vor fi temporizate la intervalul indicat de valoarea Constants.SLEEP_TIME (propagarea mesajelor va fi intercalată de apeluri Thread.sleep().

a) De ce este necesar ca serviciul să realizeze operațiile pe un fir de execuție dedicat?

b) Ce alternativă s-ar fi putut folosi pentru a se evita o astfel de abordare? Ce avantaj și ce dezavantaj prezintă această alternativă?

Se implementează o clasă derivată din Thread pentru care se va suprascrie metoda run(). Pe firul de execuție dedicat, se vor propaga intențiile cu difuzare la nivelul sistemului de operare Android, după care acesta își va încheia activitatea.

package ro.pub.cs.systems.eim.lab05.startedservice.service;

import android.content.Context;
import android.content.Intent;

import ro.pub.cs.systems.eim.lab05.startedservice.general.Constants;

public class ProcessingThread extends Thread {

  private Context context;

  public ProcessingThread(Context context) {
    this.context = context;
  }

  @Override
  public void run() {
    while(true){ 
      sendMessage(Constants.MESSAGE_STRING);
      sleep();
      * ...
    }
  }

  private void sleep() {
    try {
      Thread.sleep(Constants.SLEEP_TIME);
    } catch (InterruptedException interruptedException) {
      interruptedException.printStackTrace();
    }
  }

  private void sendMessage(int messageType) {
    Intent intent = new Intent();
    switch(messageType) {
      case Constants.MESSAGE_STRING:
         intent.setAction(Constants.ACTION_STRING);
         intent.putExtra(Constants.DATA, Constants.STRING_DATA);
         break;
      * ...
    }
    context.sendBroadcast(intent);
  }
}

Monitorizați ciclurile din Thread.run() in logcat:

Log.d(Constants.TAG, "Thread.run() was invoked, PID: " + android.os.Process.myPid() + " TID: " + android.os.Process.myTid());

4. În proiectul StartedServiceActivity, să se pornească serviciul, printr-un apel al metodei startService() sau startForegroundService dupa versiunea Oreo; intenția care va fi transmisă ca argument metodei startService() trebuie să refere explicit serviciul care urmează a fi pornit, din motive de securitate (se folosește metoda setComponent(), care indică atât pachetul corespunzător aplicației Android care conține serviciul, cât și clasa corespunzătoare acestuia - calificată complet).

Intent intent = new Intent();
intent.setComponent(new ComponentName("ro.pub.cs.systems.eim.lab05.startedservice", "ro.pub.cs.systems.eim.lab05.startedservice.service.StartedService"));
// De la oreo in sus se foloseste startForegroundService
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    startForegroundService(intent);
}
else {
    startService(intent);
}

In serviciu, vom face urmatoarea actualizare pentru ca serviciul sa anunte pornirea catre activitate:

  private static final String TAG = "ForegroundService";
  private static final String CHANNEL_ID = "11";
  private static final String CHANNEL_NAME = "ForegroundServiceChannel";
  private void dummyNotification() {
    NotificationChannel channel = null;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      channel = new NotificationChannel(CHANNEL_ID,CHANNEL_NAME,
        NotificationManager.IMPORTANCE_HIGH);
    }
    NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      manager.createNotificationChannel(channel);
    }
    Notification notification = null;
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
      notification = new Notification.Builder(getApplicationContext(),CHANNEL_ID).build();
    }
    startForeground(1, notification);
  }
 
   @Override
  public int onStartCommand(Intent intent, int flags, int startId) {
    /* ... */
    // Am adaugat aceasta linie
    dummyNotification();
    /* ... */
  }

a) Să se ruleze aplicațiile. Se va rula aplicația StartedService care instalează serviciul pe dispozitivul mobil. Ulterior se va rula aplicația StartedServiceActivity. Verificați faptul că serviciul a fost pornit și oprit corespunzător prin mesajele afișate în consolă.

b) Monitorizați în DDMS procesele asociate activității și serviciului. Ce se întâmplă dacă activitatea este eliminată (onDestroy())?

c) Explicați ce se întâmplă dacă repornim activitatea (monitorizați în DDMS si logcat).

5. În proiectul StartedServiceActivity, să se implementeze un ascultător pentru intenții cu difuzare, în clasa StartedServiceBroadcastReceiver din pachetul ro.pub.cs.systems.eim.lab05.startedserviceactivity.view. Acesta extinde clasa BroadcastReceiver și implementează metoda onReceive(), având ca argumente contextul din care a fost invocată și intenția prin intermediul căreia a fost transmis mesajul respectiv. Astfel, datele extrase din intenție (având cheia indicată de Constants.DATA) vor fi afișate într-un câmp text (messageTextView) din cadrul interfeței grafice.

onReceive(Context, Intent) din clasa StartedServiceBroadcastReceiver, se verifică:

Puteți testa recepția mesajelor de broadcast folosind comanda shell

adb shell 'am broadcast -a "ro.pub.cs.systems.eim.lab05.startedservice.string" --es  "ro.pub.cs.systems.eim.lab05.startedservice.data" "******** hello world!"'

Acestea vor fi afișate în cadrul câmpului text din cadrul interfeței grafice (messageTextView), transmis ca argument la instanțierea ascultătorului pentru intenții cu difuzare.

package ro.pub.cs.systems.eim.lab05.startedserviceclient.view;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.TextView;

import ro.pub.cs.systems.eim.lab05.startedserviceclient.general.Constants;

public class StartedServiceBroadcastReceiver extends BroadcastReceiver {

  private TextView messageTextView;

  public StartedServiceBroadcastReceiver(TextView messageTextView) {
    this.messageTextView = messageTextView;
  }

  @Override
  public void onReceive(Context context, Intent intent) {
    String action = intent.getAction();
    String data = null;
    if (Constants.ACTION_STRING.equals(action)) {
      data = intent.getStringExtra(Constants.DATA);
    }
    * ...
    if (messageTextView != null) {
      messageTextView.setText(messageTextView.getText().toString() + "\n" + data);
    }
  }
}

6. În proiectul StartedServiceActivity, în cadrul metodei onCreate() a activității StartedServiceActivity (din pachetul ro.pub.cs.systems.eim.lab05.startedserviceactivity), să se realizeze următoarele operații:

a) să se creeze o instanță a ascultătorului pentru intenții cu difuzare;

startedServiceBroadcastReceiver = new StartedServiceBroadcastReceiver(messageTextView);

b) să se creeze o instanță a unui obiect de tipul IntentFilter, la care să se adauge toate acțiunile corespunzătoare intențiilor cu difuzare propagate de serviciu; se va folosi metoda addAction();

startedServiceIntentFilter = new IntentFilter();
startedServiceIntentFilter.addAction(Constants.ACTION_STRING);
* ...

c) să se atașeze, respectiv să se detașeze ascultătorul de intenții cu difuzare, astfel încât acesta să proceseze mesajele primite de la serviciu doar în situația în care activitatea este vizibilă pe suprafața de afișare; în acest sens, vor fi utilizate metodele registerReceiver(), respectiv unregisterReceiver(), apelate pe metodele de callback ale activității corespunzătoare stării în care aceasta este vizibilă pe suprafața de afișare (onResume(), respectiv onPause()).

Este necesar ca activarea ascultătorului să se realizeze pe metoda de callback onResume(), iar dezactivarea sa să fie realizată pe metoda de callback onPause().

@Override
protected void onResume() {
  super.onResume();
  registerReceiver(startedServiceBroadcastReceiver, startedServiceIntentFilter);
}

@Override
protected void onPause() {
  unregisterReceiver(startedServiceBroadcastReceiver);
  super.onPause();
}

d) Să se oprească serviciul printr-un apel al metodei stopService(). Unde ar putea fi plasată aceasta? Care sunt avantajele și dezavantajele unei astfel de abordări?

7. Rulați din nou aplicația, întrerupând temporar activitatea (printr-o apăsare a tastei Home) în timp ce sunt procesate intențiile cu difuzare transmise de serviciu. Ce observați la revenirea în activitate?

Modificați modul de implementare al ascultătorului de intenții cu difuzare astfel încât în momentul în care se primește un mesaj, să repornească activitatea (dacă este cazul), asigurându-se astfel faptul că nu se mai pierde nici o informație transmisă de serviciu dacă aceasta nu este vizibilă pe suprafața de afișare.

<spoiler Indicații de Rezolvare> Pentru ca ascultătorul de intenții cu difuzare să poată procesa mesaje chiar și în situația în care activitatea nu este vizibilă pe suprafața de afișare, el trebuie declarat, împreună cu filtrul de intenții, în fișierul AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
  <application ...>
    <receiver
      android:name=".view.StartedServiceBroadcastReceiver">
      <intent-filter>
        <action android:name="ro.pub.cs.systems.eim.lab05.startedservice.string" />
      </intent-filter>
      <!-- other intent filters for other activities -->
    </receiver>
  </application>
</manifest>

În acest scop, va trebui declarat și un constructor implicit care va fi folosit pentru instanțierea filtrului de intenții. Astfel, nu se va mai putea obține o referință către câmpul text în care să se realizeze afișarea informațiilor obținute în urma procesării intenției cu difuzare. Aceasta va fi realizată de către activitate, care va fi invocată de ascultătorul de mesaje cu difuzare, în cadrul metodei onReceive(), prin intermediul unei intenții (datele fiind plasate în câmpul extra):

@Override
public void onReceive(Context context, Intent intent) {
  String action = intent.getAction();
  String data = null;
  if (Constants.ACTION_STRING.equals(action)) {
    data = intent.getStringExtra(Constants.DATA);
  }
  * ...
  Intent startedServiceActivityIntent = new Intent(context, StartedServiceActivity.class);
  startedServiceActivityIntent.putExtra(Constants.MESSAGE, data);
  startedServiceActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_SINGLE_TOP);
  context.startActivity(startedServiceActivityIntent);
}

De remarcat faptul că activitatea poate fi pornită din contextul ascultătorului pentru mesaje cu difuzare, folosind următorii parametri:

  • Intent.FLAG_ACTIVITY_NEW_TASK - prin care activitatea este plasată într-un nou set de sarcini pe stivă, pe care utilizatorul le poate gestiona independent;
  • Intent.FLAG_ACTIVITY_SINGLE_TOP - prin care activitatea nu este repornită dacă se găsește deja la baza stivei care reține istoricul operațiunilor realizate de utilizator.

Astfel, activitatea va fi invocată prin intermediul intenției, în condițiile în care aceasta este activă (se găsește în memorie, fiind plasată pe stiva de activități, fără a fi vizibilă). De aceea, aceasta va invoca metoda de callback onNewIntent() pe care trebuie realizată afișarea informațiilor transmise în câmpul extra al intenției.

@Override
protected void onNewIntent(Intent intent) {
  super.onNewIntent(intent);
  String message = intent.getStringExtra(Constants.MESSAGE);
  if (message != null) {
    messageTextView.setText(messageTextView.getText().toString() + "\n" + message);
  }
}

Laborator 06. Comunicația prin Sockeți în Android

In cadrul laboratorului trecut am vazut cum putem interactiona cu diferite servicii cu ajutorul protocolului HTTP. Totusi, exista cazuri in care avem nevoie sa lucram mai jos, direct peste stockets.

Retea locala

De obicei, servicile pe care le accesam in internet sunt accesibil din internet. Totusi, apar cazuri cand atat dispozitivul mobil cat si server-ul se afla in aceasi retea privata. Cel mai des intalnit caz fiind atunci cand dezvoltam o aplicatie, sau avem o aplicatie de telefon pentru un dispozitiv ce ruleaza local.

In acest caz in care dispozitivele se gasesc in aceasi retea, va fi nevoie sa realizam cateva lucruri in functie de circumstante (e.g. NAT).

Dispozitiv Fizic

O mașină fizică și un dispozitiv fizic pot comunica:

  1. prin plasarea lor în aceeași rețea fără fir (WiFi), având adresele IP furnizate de un server DHCP ce rulează pe un router;
  2. prin stabilirea unei legături punct la punct, folosind Bluetooth;
  3. printr-o conexiune de date realizată prin intermediul portului USB.

Pentru tethering, pe telefon, se accesează SettingsWireless & NetworksTethering & portable hotspot și se selectează opțiunea USB Tethering

Astfel, se va activa (în mod automat) interfața rndis0, pentru care se poate determina adresa Internet:

student@eim-lab:/opt/android-sdk-linux/platform-tools$ ./adb devices
List of devices attached
0019531d59461f    device
student@eim-lab:/opt/android-sdk-linux/platform-tools$ ./adb -s 0019531d59461f shell
shell@n7000:/ $ su
su
shell@n7000:/ # ifconfig rndis0
ifconfig rndis0
rndis0: ip 192.168.42.129 mask 255.255.255.0 flags [up broadcast running multicast]
shell@n7000:/ #

Ulterior, se determină adresa Internet a mașinii fizice, asociată interfeței usb0 (Linux), respectiv Ethernet (Windows).

Linux

student@eim-lab:~$ sudo ifconfig usb0
usb0      Link encap:Ethernet  HWaddr 32:ca:4b:1c:ff:7b 
          inet addr:192.168.42.170  Bcast:192.168.42.255  Mask:255.255.255.0
          inet6 addr: fe80::30ca:4bff:fe1c:ff7b/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:109 errors:0 dropped:0 overruns:0 frame:0
          TX packets:319 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:24103 (23.5 KiB)  TX bytes:64369 (62.8 KiB)

Windows

C:\Program Files (x86)\Android\android-sdk\platform-tools>ipconfig

Windows IP Configuration


Ethernet adapter Local Area Connection:

   Connection-specific DNS Suffix  . :
   Link-local IPv6 Address . . . . . : fe80::18bf:d0be:3625:6b1%44
   IPv4 Address. . . . . . . . . . . : 192.168.42.81
   Subnet Mask . . . . . . . . . . . : 255.255.255.0
   Default Gateway . . . . . . . . . : 192.168.42.129

Dispozitiv Virtual (Emulator)

Android Virtual Device

Fiecare instanță a unui dispozitiv virtual Android oferă o pereche de porturi pentru diferite conexiuni:

  • un port de consolă, prin intermediul căruia este permis accesul prin telnet pentru execuția de diverse comenzi;
  • un port pentru adb.

Numerele folosite pentru aceste porturi sunt succesive. Implicit, numerotarea porturilor începe de la 5554 (portul de consolă) / 5555 (portul adb). Determinarea portului poate fi realizată:

  • prin inspectarea ferestrei în care este afișat emulatorul, având forma Android Emulator (55nr), unde nr poate lua valori cuprinse între 54 și 87 (doar valori impare - pentru portul de consolă, valorile pare fiind rezervate pentru portul adb); astfel, sunt suportate maxim 16 instanțe de dispozitive virtuale Android simultan;
android_virtual_device_port.png
  • prin rularea comenzii adb devices
    student@eim-lab:/opt/android-sdk-linux/platform-tools$ ./adb devices
    List of devices attached
    emulator-5554   device
    

Conectarea la consola dispozitivului virtual Android se face prin comanda:

student@eim-lab:~$ telnet localhost 55nr

specificându-se portul pe care rulează emulatorul.

Este necesară și autentificarea pe dispozitivul virtual respectiv, folosind o cheie care a fost instalată odată cu acesta, a cărei locație este indicată. În acest sens, se folosește comanda auth, urmată de cheia dispozitivului virtual. Cheia se află în ~/.emulator_console_auth_token

În consolă, realizarea unei legături între mașina fizică și dispozitivul virtual Android se face prin port forwarding, folosind comanda redir, aceasta suportând mai multe opțiuni:

  • list;
  • add;
  • del.
OPȚIUNE DESCRIERE
list afișează toate redirectările de port folosite la momentul respectiv
add <protocol>:<port_masina_fizica>:<port_dispozitiv_virtual> adaugă o redirectare de port
<protocol> poate avea doar valorile tcp sau udp
<port_masina_fizica> reprezintă numărul portului utilizat pe mașina fizică
<port_dizpozitiv_virtual> reprezintă numărul portului de pe dispozitivul virtual spre care vor fi redirecționate datele
del <protocol>:<port_masina_fizica> șterge o redirectare de port

De exemplu:

student@eim-lab:~$ telnet localhost 5554
Android Console: type 'help' for a list of commands
OK
redir add tcp:2000:4000
OK
redir list
tcp:2000  => 4000
OK
redir del tcp:2000
OK
redir list
no active redirections
OK
exit
Connection to host lost.
student@eim-lab:~$

Comenzi utile

Mai jos gasiti o serie de comenzi utile pentru a putea afisa adresele IP alocate pe un dispozitiv.

Interfețele, adresele, rutele (mașina de dezvoltare Linux)

student@eg106:~$ ip ro
    default via 172.16.7.254 dev eno1 proto dhcp metric 100 
    169.254.0.0/16 dev eno1 scope link metric 1000 
    172.16.4.0/22 dev eno1 proto kernel scope link src 172.16.7.36 metric 100 
    172.16.101.0/24 dev vmnet1 proto kernel scope link src 172.16.101.1 
    192.168.57.0/24 dev vboxnet1 proto kernel scope link src 192.168.57.1 

OSX

netstat -rn

In telefon sau emulator:

ipconfig

Sockets

Folosirea unui socket TCP presupune comunicația între două entități:

  1. un client care se conectează la o anumită adresă, pe un anumit port, pe care le cunoaște în prealabil;
  2. un server care așteaptă să fie invocat, la o adresă și la un port.

În Android (ca și în cazul platformei Java), clasa de bază pentru comunicația dintre client și server este Socket. Aceasta pune la dispoziție un flux de intrare și un flux de ieșire prin intermediul cărora diferite entități, ale căror adrese IP sunt vizibile între ele, pot transmite diferite date, folosind protocolul TCP. Un socket este reprezentat de o asociere dintre o adresă și un port.

"Ascultarea" invocărilor este realizată prin intermediul clasei ServerSocket. În momentul în care este detectată o astfel de solicitare, este creată o nouă conexiune, reprezentată de un obiect de tip Socket prin care se va realiza comunicarea. Astfel, la nivelul serverului, fiecare client este identificat printr-o instanță proprie a unui obiect Socket.

De regulă, o aplicație Android nu are acces la comunicația prin rețea decât prin intermediul unei permisiuni speciale:

<manifest ...>
  <!-- other application properties or components -->
  <uses-permission
    android:name="android.permission.INTERNET" />
  <!-- other application properties or components --> 
</manifest>

Clientul

Conexiunea unui client la un server se poate realiza numai în situația în care sunt cunoscute adresa IP (sau denumirea, rezolvată apoi prin intermediul DNS) și portul la care acesta așteaptă să fie invocat. Aceste valori sunt transmise ca parametrii în constructorul obiectului Socket. Ulterior, operațiile de comunicație (citire / scriere) sunt realizate prin operații pe fluxuri de intrare (InputStream) și pe fluxuri de ieșire (OutputStream).

Așadar, comunicația prin intermediul unui obiect de tip Socket presupune următorii pași:

1. deschiderea unui socket, prin transmiterea parametrilor de identificare a gazdei (adresă IP / denumire și port) ca parametrii ai constructorului unui obiect de tip Socket

String hostname = "localhost";
int port = 2000;
Socket socket = new Socket(hostname, port);

2. crearea unui output stream pentru a trimite date prin intermediul socket-ului TCP și trimiterea efectivă a datelor:

BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(socket.getOutputStream());
PrintWriter printWriter = new PrintWriter(bufferedOutputStrea, true);

De remarcat faptul că obiectul PrintWriter primește în constructor un parametru de tip boolean prin care se indică dacă transmiterea efectivă a datelor este realizată sau nu în mod automat atunci când se întâlnește caracterul \n (newline).

Metodele implementate de clasa PrintWriter sunt similare cu cele oferite de PrintStream (clasa folosită de metodele din System.out), diferența fiind faptul că pot fi create mai multe instanțe pentru seturi de caractere Unicode diferite:

Trimiterea efectivă a datelor este realizată atunci când se apelează metoda flush() sau automat la întâlnirea caracterului '\n', în funcție de argumentele cu care a fost invocat constructorul.

3. crearea unui flux de intrare pentru a primi date prin intermediul socket-ului TCP și primirea efectivă a datelor:

InputStreamReader inputStreamReader = new InputStreamReader(socket.getInputStream());
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

Metodele implementate de clasa BufferedReader sunt:

  • read() - pentru a primi un singur caracter (dacă este apelată fără parametrii) sau un tablou de caractere (de o anumită dimensiune, acestea fiind stocate într-un vector furnizat ca parametru, începând cu o anumită poziție);
  • readLine() - pentru a primi o linie.

Metoda readLine() este blocantă, așteptând un mesaj terminat prin \n (newline). În situația în care conexiunea este terminată, se transmite EOF, iar metoda întoarce valoarea null.


Note

Pot fi utilizate obiecte de tipul ObjectOutputStream și ObjectInputStream pentru transmiterea de obiecte Java, atunci când entitățile care comunică rulează în contextul unei mașini virtuale de acest tip (JVM).


Se recomandă ca obținerea de referințe către fluxul de intrare respectiv fluxul de ieșire asociate unui obiect de tip Socket să fie realizată prin intermediul unor metode statice definite în cadrul unor clase ajutător:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class Utilities {

  public static BufferedReader getReader(Socket socket) throws IOException {
    return new BufferedReader(new InputStreamReader(socket.getInputStream()));
  }
  
  public static PrintWriter getWriter(Socket socket) throws IOException {
    return new PrintWriter(socket.getOutputStream(), true);
  }

}

4. închiderea obiectului Socket, atunci când acesta nu mai este necesar

socket.close();

În momentul în care se apelează metoda close(), sunt transmise și datele care erau stocate în zonele de memorie tampon.

Rularea pe un thread separat

Stim deja ca nu putem executa operatii blocante pe thread-ul principal. Din acest motiv, orice operație ce presupune folosirea unor sockets trebuie realizată pe un thread dedicat.

Trebuie avut în vedere faptul că pe firul de execuție dedicat comunicației prin rețea NU pot fi actualizate informațiile asociate controalelor grafice, excepția generată în această situație fiind CalledFromWrongThreadException. Explicația este dată în mesajul care însoțește această excepție: numai firul de execuție în care a fost instanțiat un control grafic (obiect de tip android.view.View) are dreptul de a realiza modificări asupra acestuia (Only the original thread that created a view hierarchy can touch its views).

Ca si in alte laboratoare, vom folosi creea un thread separat si un handler pentru a actualiza interfata.

// In MainActivity

// Create handler as a class field
private Handler mainHandler = new Handler(Looper.getMainLooper()) {
    @Override
    public void handleMessage(Message msg) {
        // This runs on GUI thread. This is called
        // When the thread used for communications is finished
        String result = (String) msg.obj;
        daytimeProtocolTextView.setText(result);
    }
};

// Create and start the thread
Thread networkThread = new Thread(new Runnable() {
    @Override
    public void run() {
        
        socket = new Socket();
        ...
        result = "whatever the result is";

        // Send result to GUI thread
        Message message = mainHandler.obtainMessage();
        message.obj = result;
        mainHandler.sendMessage(message);
    }
});

// This should be put on a button click listener
networkThread.start();

Exemplu

Se dorește interogarea serverului National Institute of Standards & Technology, care oferă un serviciu de interogare a datei și orei curente, cu o precizie ridicată, conform Daytime Protocol (RFC-867).

În acest sens, se va deschide un socket TCP, prin interogarea serverului disponibil la adresa , pe portul 13, în cadrul unui fir de execuție separat (clasa NISTCommunicationThread). Întrucât nu este necesară decât operația de primire a unor date, se va crea doar un obiect de tip BufferedReader, citindu-se două linii (una fiind vidă - așadar ignorată, cealaltă conținând informațiile necesare, care se doresc a fi afișate). Întrucât modificarea conținutului unui control grafic nu poate fi realizată decât din contextul firului de execuție în care a fost creat, acesta va fi obținut prin parametrul metodei post() a obiectului respectiv, doar aici fiind permisă asocierea conținutului solicitat. În momentul în care datele au fost preluate, socket-ul TCP poate fi închis. Pe fiecare eveniment de tip apăsare a butonului se va crea un fir de execuție dedicat în care se va instanția un obiect de tip Socket.

public class DayTimeProtocolActivity extends AppCompatActivity {
    private Button getInformationButton;
    private TextView daytimeProtocolTextView;
    private Handler mainHandler;

    private class NISTCommunicationThread extends Thread {
        @Override
        public void run() {
            String dayTimeProtocol = null;
            try {
                Socket socket = new Socket(Constants.NIST_SERVER_HOST, Constants.NIST_SERVER_PORT);
                BufferedReader bufferedReader = Utilities.getReader(socket);
                bufferedReader.readLine();
                dayTimeProtocol = bufferedReader.readLine();
                Log.d(Constants.TAG, "The server returned: " + dayTimeProtocol);
            } catch (UnknownHostException unknownHostException) {
                Log.d(Constants.TAG, unknownHostException.getMessage());
                if (Constants.DEBUG) {
                    unknownHostException.printStackTrace();
                }
            } catch (IOException ioException) {
                Log.d(Constants.TAG, ioException.getMessage());
                if (Constants.DEBUG) {
                    ioException.printStackTrace();
                }
            }

            // Send result back to UI thread
            Message message = mainHandler.obtainMessage();
            message.obj = dayTimeProtocol;
            mainHandler.sendMessage(message);
        }
    }

    private class ButtonClickListener implements Button.OnClickListener {
        @Override
        public void onClick(View view) {
            NISTCommunicationThread nistThread = new NISTCommunicationThread();
            nistThread.start();
        }
    }

    private ButtonClickListener buttonClickListener = new ButtonClickListener();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_day_time_protocol);
        
        daytimeProtocolTextView = (TextView)findViewById(R.id.daytime_protocol_text_view);
        getInformationButton = (Button)findViewById(R.id.get_information_button);
        getInformationButton.setOnClickListener(buttonClickListener);

        // Initialize handler on the main thread with direct message handling
        mainHandler = new Handler(Looper.getMainLooper()) {
            @Override
            public void handleMessage(Message msg) {
                String result = (String) msg.obj;
                daytimeProtocolTextView.setText(result);
            }
        };
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mainHandler.removeCallbacksAndMessages(null);
    }
}

Utilitare networking

Prin intermediul utilitarului nc (apelat cu adresa Internet și portul serverului) se poate interoga mesajul care a fost transmis. De asemenea, comanda time măsoară timpul în care a fost executată operația respectivă.

# On the developer machine
student@eim-lab:~$ nc 172.16.7.89 2000
Hello, EIM Student!

Există și în Android o variantă simplificată de nc, care poate fi apelată folosind busybox:

# On the android device
busybox nc ftp.ngc.com 21 
busybox nc -l -p 5000

Fiecare comunicație dintre client și server este tratată secvențial, ceea ce poate determina latențe semnificative în cazul în care sunt înregistrate mai multe solicitări de acest tip concomitent.

De asemenea, în momentul în care coada în care sunt stocate este completată, o cerere poate fi refuzată.

Firebase Cloud Messaging

Fiind la capitolul networking, vom încerca o funcționalitate de networking întâlnită foarte des în Android: push notifications.

Firebase Cloud Messaging (FCM) este un serviciu de mesagerie bazat pe cloud, oferit de Google, care permite trimiterea de mesaje și notificări către dispozitive Android, iOS și aplicații web. FCM oferă o infrastructură scalabilă pentru livrarea de notificări push și mesaje personalizate în timp real, fie că aplicația este activă, în fundal sau închisă.

Arhitectura sa se bazează pe un canal bidirecțional între serverele Firebase și dispozitivele utilizatorilor. FCM suportă două tipuri principale de mesaje:

  • Mesaje de notificare – Utilizate pentru afișarea de mesaje vizibile utilizatorilor, gestionate de FCM
  • Mesaje de date – Mesaje personalizate care declanșează logica aplicației fără afișarea implicită a notificărilor

Fluxul mesajelor

  1. Înregistrarea dispozitivelor: Fiecare dispozitiv obține un registration token unic generat de FCM, care este asociat cu aplicația instalată.
  2. Trimiterea mesajelor: Mesajele sunt trimise de serverul aplicației către FCM sau direct din Firebase Console, specificând token-ul dispozitivului sau un grup de utilizatori (topic-uri sau grupuri).
  3. Livrarea mesajelor: FCM prioritizează livrarea mesajelor și oferă mecanisme de retry pentru dispozitive offline.

Tipuri de mesaje

  1. Mesaje de notificare: Conțin un payload specific de notificare (titlu, body, imagine).
  2. Mesaje de date: Conțin un payload personalizat definit de dezvoltator. Necesită procesare manuală în aplicație, indiferent de starea acesteia si nu au o interfata vizuala pentru utilizator.
  3. Mesaje mixte: Combinație între notificări și date.

Pentru a ne asigura ca mesajele trimise prin intermediul FCM ajung la destinatar, putem seta un nivel de prioritate:

  • High Priority: Livrare imediată, utilizată pentru notificări critice
  • Normal Priority: Optimizată pentru economisirea bateriei (e.g., mesaje non-critice)

Note: Indiferent de prioritatea sau tipul de mesaje trimis prin intermediul FCM, livrarea acestora la destinatar poate fi influentata de limitele impuse de sistemul de operare(e.g., mecanisme de optimizare a duratei de viata a bateriei).

Mesajele pot avea ca destinatar/target:

  • un topic - un "canal" de notificari utilizat pentru a emite mesaje care fac parte dintr-o anumita categorie (e.g., "noutati", "promotii")
  • un token: vizeaza dispozitivul care are asociat token-ul specificat
  • un grup de tokeni sau un grup asociat unei aplicatii

Note: Pentru a putea primi mesaje trimise pe un anumit topic, este nevoie ca utilizatorul sa faca subscribe la topicul respectiv. De asemenea, exista si operatia complementara de unsubscribe care opreste primirea mesajelor trimise pe un anumit topic.

Folosirea serviciului FCM in cadrul unei aplicatii este conditionata de:

  1. Crearea unui proiect în Firebase Console
    • crearea unui proiect cu acelasi package name/ID ca si aplicatia pentru care ne dorim implementarea serviciului
    • adaugarea, in proiect, a servicului de Cloud Messaging
  2. Configurarea aplicației mobile Android
    • Descărcarea fișierului google-services.json, care trebuie plasat în directorul app/ din proiectul Android
    • adaugarea SDK-ul Firebase pentru a putea utiliza FCM. Acest lucru implică adăugarea dependențelor necesare în fișierul build.gradle
  3. Permisiunea de a primi notificari in aplicatie

Note: Incepand cu Android 13, permisiunea de a primi notificari in cadrul aplicatiei trebuie ceruta in mod explicit utilizatorului.

Activitate de laborator

1. Să se cloneze în directorul de pe discul local conținutul depozitului la distanță de la aceasta adresa.

2. Primul task este să creăm o aplicație cu un buton și un TextView. La apăsarea butonului, se va porni un nou thread care va comunica cu un server ce va rula pe calculatorul nostru. Acest server va returna un text pe care îl vom afișa în TextView.

Pentru a deschide un server TCP, cel mai rapid, putem folosi utilitarul nc astfel:

echo "hello world" | nc -l 4444

Serverul se va opri dupa prima conexiune

Alternativa este să ne creăm propriul server într-un limbaj de programare și să îl rulăm. Cel mai la îndemână este Python, dar putem lucra și în alte limbaje precum C.

    import socket
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    server_address = ('0.0.0.0', 5000)
    server_socket.bind(server_address)

    server_socket.listen(1)

    while True:
        connection, client_address = server_socket.accept()
        print(f'Connection from {client_address}')
        connection.sendall(b'hello')

Activitate de Laborator

1. Vom incepe prin a crea un proiect nou in Android Studio, cu un package ID ales de voi.

Note: Puteti alege atat Kotlin, cat si Java pentru rezolvarea laboratorului.

2. Utilizarea serviciului FCM(Firebase Cloud Messaging) implica existenta unui proiect configurat in Firebase, astfel:

  • Autentificati-va in Firebase(https://firebase.google.com/)
  • Dupa autentificare, accesati Firebase Console(https://console.firebase.google.com/)
  • Creati un proiect nou

  • Alegeti un nume pentru proiect
  • Accesati proiectul si alegeti din meniul Run optiunea Messaging
  • Selectati optiunea de proiect de Android

  • Completati datele pentru proiectul vostru

Note: Android package name trebuie sa fie cel pe care l-ati setat la crearea proiectului in Android Studio. Optional, puteti alege si un nume. Nu este nevoie sa completati Debug signing certificate SHA-1.

  • Inregistrati aplicatia si continuati la seciunea Download and add config file. Descarcati fisierul google-services.json si adaugati-l in proiect in directorul indicat

  • Continuati la sectiunea Add Firebase SDK si urmati indicatiile pentru a adauga dependentele necesare in proiectul vostru. Asigurati-va ca sync-ul pentru Gradle se realizeaza cu succes si finalizati crearea proiectului in Firebase

Note: Nu va fi nevoie de id("com.android.application"). Adaugarea acestui plugin va genera o eroare de Gradle. Dupa caz, poate fi nevoie sa adaugati in fisierul build.gradle.kts(Module :app), in sectiunea dependencies: implementation(libs.firebase.common.ktx) si implementation(libs.firebase.messaging.ktx) - pentru Kotlin sau implementation(libs.firebase.common) si implementation(libs.firebase.messaging) - pentru Java.

3. In proiectul creat in Android Studio, creati clasa MainActivity(daca aceasta nu este deja creata). In metoda onCreate(), vom initializa serviciul Firebase:

FirebaseApp.initializeApp(this);
Firebase.initialize(this) 

3. Pentru a putea primi notificari in aplicatia noastra, este nevoie sa cerem permisiunea utilizatorului in prealabil:

Note: Aveti o eroare la utilizarea constantei NOTIFICATION_PERMISSION_REQUEST_CODE. Aceasta trebuie definita. Gasiti valoarea sa pentru ca functia sa fie completa.


  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
      ActivityCompat.requestPermissions(
        this,
        new String[]{android.Manifest.permission.POST_NOTIFICATIONS},
        NOTIFICATION_PERMISSION_REQUEST_CODE
      );
    }
  }

  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
      ActivityCompat.requestPermissions(
        this,
        arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
        NOTIFICATION_PERMISSION_REQUEST_CODE
      )
    }
  }

De asemenea, va fi nevoie sa declarati permisiunile folosite de aplicatia voastra si in AndroidManifest.xml.

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

4. Pentru a putea loga primirea notificarilor, vom crea un service custom:


  public class CustomFirebaseMessagingService extends FirebaseMessagingService {}

  class CustomFirebaseMessagingService : FirebaseMessagingService() {}

Suprascrieti metoda onMessageReceived() si logati primirea de mesage printr-un mesaj sugestiv.

De asemenea, nu uitati sa definiti service-ul in manifest:

<service
    android:name=".CustomFirebaseMessagingService"
    android:exported="false"
    android:directBootAware="true">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>

Pentru a testa trimiterea de notificari, vom folosi Firebase Console si vom crea o campanie de notificari:

Alegeti un titlu si un continut pentru notificare.

Selectati User segment si selectati aplicatia creata de voi in sectiunea App. Continuati si programati trimiterea notificarii Now. Lansati campania si asteptati primirea notificarii.

5. In acest exercitiu veti lucra cu notiunea de 'topic' de mesaje. Incepeti prin a crea interfata necesara folosind un layout si activity-ul creat anterior. Mai jos, aveti o referinta pentru interfata:

  • la click pe butonul SUBSCRIBE, va fi apelata o metoda prin care veti face subscribe la topicul cu numele introdus in EditText
  • la click pe butonul UNSUBSCRIBE, va fi apelata o metoda prin care veti face unsubscribe la topicul cu numele introdus in EditText

Pentru a testa notificarile trimise pe topice, veti face subscribe la un topic si veti crea o campanie de notificari, dar spre deosebire de exemplul anterior, veti seta ca target Topic si veti selecta topicul dorit.

Testati trimiterea de notificari si dupa unsubscribe.

Protocolul Bluetooth

Bluetooth este un protocol standardizat pentru trimiterea și primirea datelor printr-o legătură wireless de 2,4 GHz. Este un protocol securizat, perfect pentru transmisii wireless pe distanțe scurte, cu consum redus de energie și costuri scăzute, între dispozitive electronice.

Rețelele Bluetooth (denumite frecvent piconeturi) utilizează un model de tip master/slave pentru a controla când și unde dispozitivele pot trimite date. În acest model, un singur dispozitiv master poate fi conectat la până la șapte dispozitive slave diferite. Orice dispozitiv slave din piconet poate fi conectat doar la un singur master.

Bluetooth Addresses

Fiecare dispozitiv Bluetooth are o adresă unică de 48 de biți, adesea abreviată BD_ADDR. Aceasta va fi de obicei prezentată sub forma unei valori hexadecimale de 12 cifre. Jumătatea cea mai semnificativă (24 de biți) a adresei este un identificator unic al organizației (OUI), care identifică producătorul. Cei 24 de biți inferiori sunt partea mai unică a adresei.

Această adresă ar trebui să fie vizibilă pe majoritatea dispozitivelor Bluetooth. De exemplu, pe acest Modul Bluetooth RN-42, adresa tipărită lângă "MAC NO." este 000666422152:

Procesul de conectare

Crearea unei conexiuni Bluetooth între două dispozitive este un proces cu mai multe etape, care implică trei stări progresive:

  • Inquiry -- Dacă două dispozitive Bluetooth nu știu absolut nimic unul despre celălalt, unul trebuie să efectueze un inquiry pentru a încerca să descopere celălalt. Un dispozitiv trimite o solicitare de inquiry, iar orice dispozitiv care ascultă o astfel de solicitare va răspunde cu adresa sa, și posibil cu numele său și alte informații.

  • Paging (Connecting) -- Paging este procesul de formare a unei conexiuni între două dispozitive Bluetooth. Înainte ca această conexiune să poată fi inițiată, fiecare dispozitiv trebuie să cunoască adresa celuilalt (găsită în procesul de inquiry).

  • Connection -- După ce un dispozitiv a completat procesul de paging, intră în starea de conexiune. În timp ce este conectat, un dispozitiv poate fie să participe activ, fie poate fi pus într-un mod de somn de joasă putere.

    • Active Mode -- Acesta este modul conectat obișnuit, unde dispozitivul transmite sau primește date activ.

    • Sniff Mode -- Acesta este un mod de economisire a energiei, unde dispozitivul este mai puțin activ. Va dormi și va asculta doar transmisiunile la un interval stabilit (de exemplu, la fiecare 100ms).

    • Hold Mode -- Modul Hold este un mod temporar de economisire a energiei unde un dispozitiv doarme pentru o perioadă definită și apoi se întoarce înapoi la modul activ când acel interval a trecut. Maestrul poate comanda un dispozitiv sclav să aștepte.

    • Park Mode -- Park este cel mai profund dintre modurile de somn. Un maestru poate comanda un sclav să "parceze", și acel sclav va deveni inactiv până când maestrul îi spune să se trezească.

Bonding si Pairing

Când două dispozitive Bluetooth împărtășesc o afinitate specială unul pentru celălalt, acestea pot fi legate împreună. Dispozitivele legate stabilesc automat o conexiune ori de câte ori sunt suficient de aproape. De exemplu, când pornesc mașina, telefonul din buzunarul meu se conectează imediat la sistemul Bluetooth al mașinii pentru că împărtășesc o legătură. Nu sunt necesare interacțiuni cu interfața utilizatorului!

Legăturile sunt create printr-un proces unic numit cuplare. Când dispozitivele se cuplă, își împărtășesc adresele, numele și profilele, și de obicei le stochează în memorie. De asemenea, împărtășesc o cheie secretă comună, care le permite să se lege ori de câte ori sunt împreună în viitor.

Cuplarea necesită de obicei un proces de autentificare unde un utilizator trebuie să valideze conexiunea dintre dispozitive. Fluxul procesului de autentificare variază și depinde de obicei de capabilitățile de interfață ale unuia dintre dispozitive sau al celuilalt. Uneori, cuplarea este o operațiune simplă de tipul "Just Works" (Funcționează Pur și Simplu), unde apăsarea unui buton este tot ce este necesar pentru a cupla (acesta este comun pentru dispozitive fără UI, ca headseturile). Alteori, cuplarea implică potrivirea codurilor numerice de 6 cifre. Procesele de cuplare mai vechi, legacy (v2.0 și anterioare), implică introducerea unui cod PIN comun pe fiecare dispozitiv. Codul PIN poate varia ca lungime și complexitate de la patru numere (de exemplu, "0000" sau "1234") la un șir alfanumeric de 16 caractere.

Bluetooth

Bluetooth, din perspectiva dezvoltării pe Android, reprezintă o tehnologie wireless esențială care permite dispozitivelor să comunice între ele pe distanțe scurte. Android oferă un API extins pentru Bluetooth, care permite dezvoltatorilor să realizeze diverse funcționalități, de la transferul simplu de date până la gestionarea conexiunilor complexe între dispozitive.

API-ul Bluetooth în Android este împărțit în două categorii principale:

  • Bluetooth clasic - Este folosit pentru comunicarea punct-la-punct între dispozitive, ideal pentru transferul de date la volum mare, cum ar fi fișierele audio sau datele de pe un dispozitiv periferic. Este utilizat frecvent în cazul căștilor, difuzoarelor și al altor dispozitive de periferie.

  • Bluetooth Low Energy (BLE) - Cunoscut și sub numele de Bluetooth 4.0+, este optimizat pentru consum redus de energie, fiind ideal pentru dispozitivele și senzorii care necesită transferuri de date mici și ocazionale. BLE este utilizat adesea în dispozitive wearable, dispozitive de urmărire a fitnessului și alte gadgeturi inteligente.

API-ul Android pentru Bluetooth permite dezvoltatorilor să execute o serie de acțiuni, inclusiv:

  • Scanarea dispozitivelor Bluetooth din apropiere și afișarea informațiilor despre acestea.
  • Inițializarea conexiunilor între dispozitive și gestionarea datelor transmise.
  • Crearea unui server Bluetooth pe dispozitiv, care poate accepta conexiuni de la alte dispozitive.
  • Gestionarea dispozitivelor asociate și păstrarea unei liste a dispozitivelor cunoscute.
  • Comunicarea cu profiluri Bluetooth specifice, cum ar fi A2DP (Advanced Audio Distribution Profile) pentru transmiterea audio sau HFP (Hands-Free Profile) pentru apeluri telefonice.

Pentru a utiliza Bluetooth în aplicațiile Android, dezvoltatorii trebuie să includă permisiunile necesare în fișierul manifest al aplicației și, în cazul accesului la BLE, să verifice dacă dispozitivul suportă BLE.

Pentru a utiliza funcționalitatea Bluetooth, va trebui să adăugați următoarele permisiuni în fișierul AndroidManifest.xml:

    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

    <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />

    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />

Exemplu de cum se cer permisiunile in Android:


    List<String> permissions = new ArrayList<>();
    permissions.add(Manifest.permission.BLUETOOTH_CONNECT);
    permissions.add(Manifest.permission.BLUETOOTH_SCAN);
    // Cerem permisiunile utilizatorului
    ActivityCompat.requestPermissions(this, permissions.toArray(new String[0]), REQUEST_PERMISSIONS);

BlueetoothAdapter

BluetoothAdapter este punctul central pentru toate operațiunile Bluetooth. Acesta:

  • Permite activarea și dezactivarea Bluetooth.
  • Inițiază procesul de descoperire a dispozitivelor din apropiere.
  • Listează dispozitivele asociate anterior.

Exemplu de activare a Bluetooth:

BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter == null) {
    // Dispozitivul nu suportă Bluetooth
} else if (!bluetoothAdapter.isEnabled()) {
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}

Descoperirea dispozitivelor

Pentru a găsi alte dispozitive Bluetooth din apropiere, folosim metoda startDiscovery() din BluetoothAdapter. Este necesar să înregistrați un BroadcastReceiver pentru a primi informații despre dispozitivele descoperite.

Exemplu:

IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(receiver, filter);

bluetoothAdapter.startDiscovery();

Etapele conectării dispozitivelor

După ce am descoperit alte dispozitive, vom crea o conexiune între dispozitivele noastre, folosind clasele BluetoothServerSocket BluetoothSocket.

1. Configurarea unui server Bluetooth

Serverul este responsabil de acceptarea conexiunilor inițiate de alte dispozitive. Acest lucru se face utilizând un BluetoothServerSocket.

Pentru a crea un server socket, folosim metoda listenUsingRfcommWithServiceRecord a clasei BluetoothAdapter. Aceasta creează un BluetoothServerSocket, care poate asculta conexiunile.

Exemplu de configurare:

BluetoothServerSocket serverSocket = null;
try {
    serverSocket = bluetoothAdapter.listenUsingRfcommWithServiceRecord(
        "BluetoothChatApp", // Numele serviciului
        MY_UUID // UUID-ul unic care identifică serviciul
    );
} catch (IOException e) {
    Log.e("ServerSocket", "Eroare la crearea server socket: " + e.getMessage());
}

UUID-ul (Universally Unique Identifier) este folosit pentru a identifica un serviciu specific. Acesta trebuie să fie același pe server și pe client.

2. Acceptarea unei conexiuni

După ce serverul a fost configurat, utilizăm metoda accept() pentru a aștepta și accepta conexiunile de la alte dispozitive. Exemplu:

BluetoothSocket socket = null;
try {
    // Metoda accept() este blocantă până când un dispozitiv se conectează
    socket = serverSocket.accept();
} catch (IOException e) {
    Log.e("ServerSocket", "Eroare la acceptarea conexiunii: " + e.getMessage());
}

accept() va returna un obiect BluetoothSocket pe care îl putem utiliza pentru a comunica cu dispozitivul conectat.

3. Inițierea conexiunii ca dispozitiv client

Dispozitivul client utilizează un BluetoothSocket pentru a iniția o conexiune către server.

Pentru a crea un socket pentru client, utilizăm metoda createRfcommSocketToServiceRecord a clasei BluetoothDevice pentru a crea canalul de comunicatie. Exemplu:

BluetoothSocket clientSocket = null;
try {
    clientSocket = bluetoothDevice.createRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException e) {
    Log.e("ClientSocket", "Eroare la crearea socket-ului: " + e.getMessage());
}

4. Conectarea la server

Odată creat socket-ul, folosim metoda connect() pentru a iniția conexiunea către server.

Exemplu:

try {
    clientSocket.connect();
} catch (IOException e) {
    Log.e("ClientSocket", "Eroare la conectare: " + e.getMessage());
    try {
        clientSocket.close(); // Închidem socket-ul în caz de eroare
    } catch (IOException closeException) {
        Log.e("ClientSocket", "Eroare la închiderea socket-ului: " + closeException.getMessage());
    }
}

Important: Înainte de a apela metoda connect(), este recomandat să oprim descoperirea dispozitivelor (folosind bluetoothAdapter.cancelDiscovery()) pentru a evita interferențele.

5. Comunicarea între dispozitive

După ce conexiunea a fost stabilită cu succes (atât pe server, cât și pe client), putem utiliza obiectul BluetoothSocket pentru a trimite și primi date.

Obtinerea fluxului de intrare InputStream pentru citirea datelor:

InputStream inputStream = socket.getInputStream();

Obtinerea fluxului de iesire OutputStream pentru trimiterea datelor:

OutputStream outputStream = socket.getOutputStream();

Exemplu trimitere date:

OutputStream outputStream = socket.getOutputStream();
outputStream.write("Mesaj de test".getBytes());

Exemplu primire date:

InputStream inputStream = socket.getInputStream();
byte[] buffer = new byte[1024];
int bytes = inputStream.read(buffer);
String message = new String(buffer, 0, bytes);

Handler

Pentru comunicarea rapida intre UI si thread-uri de executie putem folosi un Handler. Clasa Handler gestionează transmiterea de mesaje sau execuția unor rutine asociate unor fire de execuție prin intermediul unei cozi de așteptare (de tipul MessageQueue). Un obiect de acest tip este asociat unui singur fir de execuție. Acesta este creat în momentul în care este realizată instanța, cătrea cesta fiind transmise toate mesajele, secvențial.

Prin intermediul acestui obiect, se permite:

  • planificarea mesajelor spre a fi procesate la un moment fix din viitor (metodele de tip send…(): sendEmptyMessage(), sendMessage(), sendMessageAtTime(), sendMessageDelayed());
  • încapsularea unei acțiuni care va fi executată pe un alt fir de execuție decât firul de execuție al interfeței grafice (metodele de tip post…(): post(), postAtTime(), postDelayed()).

De exemplu, daca vrem sa trimitem un mesaj dintr-un Thread si sa il afisam ca si un Toast in interfata grafica vom proceda astfel:

Creeam un handler in care suprascriem metoda handleMessage ca sa afiseze un Toast cu mesajul.

handler = new Handler(Looper.getMainLooper()) {
    @Override
    public void handleMessage(Message inputMessage) {
        String messageText = (String) inputMessage.obj;
        Toast.makeText(MainActivity.this, messageText, Toast.LENGTH_SHORT).show();
    }
};

In thread, folosind o referinta la acest handler o sa chemam functia obtainMessage() urmata de sendToTarget.

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
 
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
            Log.d(TAG, "In a different thread " + Thread.currentThread());
 
            Message message = handler.obtainMessage();
            message.obj = "hello";
            message.sendToTarget();
            }
        };
        Thread thread = new Thread(runnable);
                thread.start();
    }
});

Activitate laborator

Veți avea o aplicație funcțională de chat care utilizează Bluetooth pentru a comunica între dispozitive Android. Laboratorul este de tip tutorial. La final, va trebui sa puteti trimite mesaje catre un alt telefon din sala.

Obiectivele laboratorului:

  • Cum să configurezi Bluetooth pe un dispozitiv Android.
  • Cum să gestionezi permisiunile necesare pentru Bluetooth.
  • Cum să listezi dispozitivele deja împerecheate.
  • Cum să creezi conexiuni între două dispozitive.
  • Cum să trimiți și să primești mesaje prin Bluetooth.

Resurse utile

  1. Find Bluetooth Devices
  2. BluetoothServerSocket
  3. BluetoothSocket
  4. Bluetooth Low Energy (BLE)
  5. Bluetooth

Configurarea unui server Bluetooth

Astazi vom implementa patru clase in aplicatia noastra de chat peste Bluetooth:

  • MainActivity - Activitatea principala unde se va gasi interfata grafica a aplicatiei de chat
  • ConnectThread - Responsabil pentru conectarea la un socket Bluetooth
  • AcceptThread - Serverul care asteapta conexiuni
  • ConnectedThread - Threadul pe care vor comunica cele 2 dispozitive Bluetooth
Click pentru a vedea interfata grafica a aplicatiei

Preview aplicație

0. Descarcarea scheletului

Vom porni de la urmatorul schelet de cod:

git clone https://github.com/eim-lab/Laborator07.git

Pentru varianta java: git checkout skel-java

Pentru solutie Java: git checkout solution-java

Pentru varianta kotlin: git checkout skel-kotlin

Pentru solutie Java: git checkout solution-kotlin

Pasul 1. Configurarea interfetei grafice:

În schelet avem deja definită o interfață grafică in XML care include:

  • O listă pentru afișarea mesajelor din chat.
  • Un câmp text pentru introducerea mesajelor.
  • Buton pentru listarea dispozitivelor Bluetooth împerecheate.
  • Buton pentru trimiterea mesajelor.

In continuare, vom defini o fuctie numita initViews, care inițializează toate componentele grafice definite în XML.

De notat faptul a vom folosi un ArrayAdapter pentru a lega o sursa de date de un obiect din interfata grafica.

    private void initViews() {
        // TODO 1: Implement the method that initializes the views
        // Conectăm componentele din XML la codul Java
        ListView chatListView = findViewById(R.id.chatListView);
        messageEditText = findViewById(R.id.messageEditText);
        sendButton = findViewById(R.id.sendButton);
        listDevicesButton = findViewById(R.id.listDevicesButton);

        // Setăm un eveniment pentru butonul de listare a dispozitivelor
        listDevicesButton.setOnClickListener(v -> listPairedDevices());

         // Pregătim o listă pentru mesaje
        chatMessages = new ArrayList<>();

        // Creăm un adaptor care transformă lista noastră de mesaje în elemente vizuale
        chatArrayAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, chatMessages);
        
        // Schimbarile din adaptor se vor vedea imediat in interfata grafica. In acest exemplue, chatListView de tip ListView va fi actualizata cand facem schimbari la chatArrayAdapter
        chatListView.setAdapter(chatArrayAdapter);
    }

Pasul 2. Initializarea conexiunii Bluetooth

Vom implementa o metoda initBluetooth, care inițializează conexiunea Bluetooth și verifică dacă telefonul suportă Bluetooth. In aceasta functie vom lua o referinta la dispozitivul Bluetooth sub forma unui BluetoothAdapter.

private void initBluetooth() {
    // Vom lua o referinta la adaptorul de bluetooth
    // de pe telefon. Putem vedea acest adaptor ca o interfata
    // cu driver-ul de bluetooth.
    bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

    // Verificăm dacă Bluetooth este disponibil
    if (bluetoothAdapter == null) {
        Toast.makeText(this, "Bluetooth is not available.", Toast.LENGTH_LONG).show();
        finish(); // Închidem aplicația dacă Bluetooth nu este disponibil
    }
}

Pasul 3. Gestionarea permisiunilor Bluetooth:

De ce sunt necesare permisiuni? Pe Android, orice funcție care accesează hardware-ul telefonului (precum Bluetooth) necesită permisiuni explicite din partea utilizatorului.

Adaugă metoda checkPermissions:

private void checkPermissions() {
    List<String> permissions = new ArrayList<>();

    // Pentru Android 12 sau mai nou, folosim permisiuni specifice Bluetooth
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        permissions.add(Manifest.permission.BLUETOOTH_CONNECT);
        permissions.add(Manifest.permission.BLUETOOTH_SCAN);
    } else {
        // Pentru versiuni mai vechi, accesul la locație este necesar
        permissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
    }

    // Cerem permisiunile utilizatorului
    ActivityCompat.requestPermissions(this, permissions.toArray(new String[0]), REQUEST_PERMISSIONS);
}

Pasul 4. Activarea Bluetooth programatic

Uneori, utilizatorii pot avea Bluetooth dezactivat. Aplicația trebuie să-l activeze automat.

Adaugă următorul cod:

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        // TODO 4: handle permission results
        if (requestCode == REQUEST_PERMISSIONS) {
            boolean permissionGranted = true;

            // Verificăm dacă toate permisiunile au fost acordate
            for (int result : grantResults) {
                if (result != PackageManager.PERMISSION_GRANTED) {
                    permissionGranted = false;
                    break;
                }
            }

            if (!permissionGranted) {
                Toast.makeText(this, "Permissions required for Bluetooth operation.", Toast.LENGTH_LONG).show();
                finish();
            }

            // Activăm Bluetooth dacă nu este deja activat
            if (!bluetoothAdapter.isEnabled()) {
                Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
                if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
                    return;
                }
                startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
            }
        }
    }

        // Handle Bluetooth enable result
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_ENABLE_BT && resultCode != RESULT_OK) {
            Toast.makeText(this, "Bluetooth must be enabled to continue.", Toast.LENGTH_LONG).show();
            finish();
        }
        super.onActivityResult(requestCode, resultCode, data);
    }

Pasul 5. Listarea dispozitivelor împerecheate

Pentru a permite comunicarea, utilizatorul trebuie să selecteze un dispozitiv împerecheat. Pentru a simplifica acest exercitiu, vom face pairing manual si doar vom afisa o lista cu dispozitivele imperecheate deja.

    private void listPairedDevices() {
        // TODO 5: Implement the method that displays a dialog for selecting a paired device
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
            return;
        }

        // Obținem dispozitivele împerecheate
        Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
        List<String> deviceList = new ArrayList<>();
        final List<BluetoothDevice> devices = new ArrayList<>();

        // Le adaugam in lista
        if (!pairedDevices.isEmpty()) {
            for (BluetoothDevice device : pairedDevices) {
                deviceList.add(device.getName() + "\n" + device.getAddress());
                devices.add(device);
            }
        }

        // Afișăm dispozitivele într-un dialog
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("Select Device");

        ArrayAdapter<String> deviceArrayAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, deviceList);

        // Tratam situatia in care un dispozitiv este apasat in cadrul dialogului
        builder.setAdapter(deviceArrayAdapter, (dialog, which) -> {
            selectedDevice = devices.get(which);

            // deschidem un thread de comunicare
            connectThread = new ConnectThread(this, bluetoothAdapter, selectedDevice, MY_UUID);
            connectThread.start();
        });

        builder.show();
    }

Pasul 6. Pornirea serverului pentru conexiuni Bluetooth

Serverul va asculta conexiuni de la alte dispozitive Bluetooth. Aceasta funcționalitate este implementată prin clasa AcceptThread, care gestionează un socket Bluetooth pentru a asculta conexiuni.

Ce face metoda startServer?

  • Creează un thread care inițiază un server socket Bluetooth.
  • Serverul rămâne activ până când o conexiune este acceptată sau este oprit manual.
    private void startServer() {
        // TODO 6: Implement server socket to listen for incoming connections
        // Inițializăm AcceptThread, care va gestiona conexiunile primite
        acceptThread = new AcceptThread(this, bluetoothAdapter, MY_UUID);
        acceptThread.start();
    }

Ce este AcceptThread?

  • AcceptThread este o clasă separată care face toată munca grea pentru ascultarea conexiunilor. Codul acesteia va fi prezentat in urmatoarea sectiune (Comunicarea prin Bluetooth)

Pasul 7. Trimiterea de mesaje prin Bluetooth:

Aceasta metodă permite trimiterea mesajelor către dispozitivul conectat. Folosim ConnectedThread pentru a scrie datele prin conexiunea Bluetooth. Metoda are responsabilitatea sa:

  • Preia textul din câmpul de introducere al utilizatorului.
  • Trimita textul folosind connectedThread.
  • Adauge mesajul în interfață.
    private void sendMessage() {
        // TODO 7: Implement the method that sends a message to the connected device
        sendButton.setOnClickListener(v -> {
            // Preluăm mesajul introdus de utilizator
            String message = messageEditText.getText().toString();

            // Verificăm dacă mesajul nu este gol și conexiunea este activă
            if (!message.isEmpty() && connectedThread != null) {

                // Trimitem mesajul ca un array de bytes
                connectedThread.write(message.getBytes());

                // Golește câmpul de text
                messageEditText.setText("");

                // Adaugă mesajul trimis în interfață
                addChatMessage("Me: " + message);
            }
        });
    }

Clasa ConnectedThread gestionează comunicarea efectivă între două dispozitive conectate. Se ocupă de: citirea mesajelor primite si trimiterea mesajelor. Detaliile de implementare vor fi prezentate in urmatoarea sectiune (Comunicarea prin Bluetooth).

Metoda addChatMessage este o metodă auxiliară pentru actualizarea listei de mesaje din interfață:

private void addChatMessage(String message) {
    chatMessages.add(message); // Adaugă mesajul în listă
    chatArrayAdapter.notifyDataSetChanged(); // Actualizează interfața
}

Pasul 8: Gestionarea opririi aplicației

Este important să eliberăm resursele utilizate de Bluetooth (socket-uri și thread-uri) când aplicația se închide. Vom face acest lucru în metoda onDestroy

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // TODO 8: cleanup threads
        // Închidem AcceptThread dacă rulează
        if (acceptThread != null) acceptThread.cancel();

        // Închidem ConnectThread dacă rulează
        if (connectThread != null) connectThread.cancel();

        // Închidem ConnectedThread dacă rulează
        if (connectedThread != null) connectedThread.cancel();
    }

Clase auxiliare

Vom construi urmatoarele clase: ConnectThread, AcceptThread si ConnectedThread. Aceste clase gestionează conexiunile Bluetooth și comunicarea dintre dispozitive. Vom detalia fiecare clasă, explicând rolul, implementarea și cum funcționează în contextul aplicației.

AcceptThread

Scop:

Gestionează acceptarea conexiunilor Bluetooth. Funcționează ca un server care așteaptă conexiuni de la alte dispozitive.

AcceptThread așteaptă și acceptă conexiuni de la alte dispozitive Bluetooth. AcceptThread este un simplu Thread: private class AcceptThread extends Thread.

Constructorul creează un BluetoothServerSocket, utilizat pentru a asculta conexiunile. Verifică permisiunile pentru Bluetooth înainte de a iniția socket-ul.

    public AcceptThread(MainActivity activity, BluetoothAdapter adapter, UUID uuid) {
        this.mainActivity = activity;

        BluetoothServerSocket tempSocket = null;
        try {
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
                if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
                    Log.d("AcceptThread", "BLUETOOTH_CONNECT permission not granted.");
                    return;
                }
            }
            tempSocket = adapter.listenUsingRfcommWithServiceRecord("BluetoothChatApp", uuid);
        } catch (IOException e) {
            Log.e("AcceptThread", "Error creating server socket: " + e.getMessage());
        }
        this.serverSocket = tempSocket;
    }

Metoda Run()

Rulează un loop care așteaptă conexiuni. Când o conexiune este acceptată, o gestionează prin metoda manageConnectedSocket.

    @Override
    public void run() {
        if (serverSocket == null) {
            Log.d(TAG, "ServerSocket is null, cannot accept connections.");
            return;
        }

        BluetoothSocket socket;
        while (!Thread.currentThread().isInterrupted()) {
            try {
                // Așteaptă o conexiune
                socket = serverSocket.accept();
                if (socket != null) {
                    Log.d(TAG, "Connection accepted. Managing socket...");

                     // Predă conexiunea aplicației
                    mainActivity.manageConnectedSocket(socket);
                    closeServerSocket();
                    break;
                }
            } catch (IOException e) {
                Log.e(TAG, "Error while accepting connection: " + e.getMessage());
                break;
            }
        }
        Log.d(TAG, "AcceptThread exiting.");
    }

Metoda Cancel()

Închide serverSocket, oprind acceptarea de conexiuni noi.

    public void cancel() {
        Log.d(TAG, "Canceling AcceptThread...");
        closeServerSocket();
    }

    private void closeServerSocket() {
        try {
            if (serverSocket != null) {
                serverSocket.close();
                Log.d(TAG, "ServerSocket closed successfully.");
            }
        } catch (IOException e) {
            Log.e(TAG, "Error closing ServerSocket: " + e.getMessage());
        }
    }

Clasa ConnectThread

ConnectThread încearcă să stabilească o conexiune cu un dispozitiv Bluetooth specificat. Creează un canal de comunicație (BluetoothSocket) către un alt dispozitiv Bluetooth.

    public ConnectThread(MainActivity activity, BluetoothAdapter adapter, BluetoothDevice device, UUID uuid) {
        this.mainActivity = activity;
        this.bluetoothAdapter = adapter;

        BluetoothSocket tmp = null;
        try {
            if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
                return;
            }
            tmp = device.createRfcommSocketToServiceRecord(uuid);
        } catch (IOException e) {
            Log.d("Connect->Constructor", e.toString());
        }
        socket = tmp;
    }

Metoda run()

Încearcă să stabilească o conexiune. Dacă reușește, predă conexiunea aplicației.

    public void run() {
        if (ActivityCompat.checkSelfPermission(mainActivity, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
            return;
        }
        if (socket == null) {
            Log.d("ConnectThread", "Socket is null, cannot connect.");
            return;
        }
        bluetoothAdapter.cancelDiscovery(); // Oprește căutarea altor dispozitive
        try {
            socket.connect(); // Se conectează la dispozitiv
            mainActivity.manageConnectedSocket(socket);
        } catch (IOException connectException) {
            mainActivity.runOnUiThread(() -> Toast.makeText(mainActivity, "Connection failed.", Toast.LENGTH_SHORT).show());
            try {
                socket.close();
            } catch (IOException closeException) {
                Log.d("Connect->Run", closeException.toString());
            }
        }
    }

Metoda cancel()

Închide conexiunea în caz de eroare sau la cererea utilizatorului.

    public void cancel() {
        try {
            if (socket == null) {
                return;
            }
            socket.close();
        } catch (IOException e) {
            Log.d("Connect->Cancel", e.toString());
        }
    }

ConnectedThread

ConnectedThread Gestionează trimiterea și primirea mesajelor după ce conexiunea este stabilită. O conexiunea este identificata printr-un socket bluetooth.

Constructorul primește un obiect BluetoothSocket și inițializează fluxurile de intrare și ieșire pentru comunicare. Configurează fluxurile de date (inputStream și outputStream) pentru a trimite și primi mesaje.


// BluetoothSocket este canalul de comuniatie stabilit cu un alt
// device bluetooth.
    public ConnectedThread(MainActivity activity, BluetoothSocket socket) {
        this.mainActivity = activity;
        this.socket = socket;
        InputStream tmpIn = null;
        OutputStream tmpOut = null;

        try {
            tmpIn = socket.getInputStream();
        } catch (IOException e) {
            Log.d("Connected->Constructor", e.toString());
        }

        try {
            tmpOut = socket.getOutputStream();
        } catch (IOException e) {
            Log.d("Connected->Constructor", e.toString());
        }

        inputStream = tmpIn;
        outputStream = tmpOut;
    }

Run()

Ascultă în mod continuu pentru mesaje primite și le afișează în interfață.

    public void run() {
        byte[] buffer = new byte[1024];
        int bytes;

        if (ActivityCompat.checkSelfPermission(mainActivity, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
            return;
        }

        // obtine numele device-ului cu care comunicam
        String remoteDeviceName = socket.getRemoteDevice().getName();

        mainActivity.runOnUiThread(() -> Toast.makeText(mainActivity, "Connected to " + remoteDeviceName, Toast.LENGTH_SHORT).show());

        while (true) {
            try {
                bytes = inputStream.read(buffer);  // Citește mesajul
                String incomingMessage = new String(buffer, 0, bytes);
                
                // Adaugam mesajul in threadul de UI al aplicatiei 
                mainActivity.runOnUiThread(() -> mainActivity.addChatMessage(remoteDeviceName + ": " + incomingMessage));
            } catch (IOException e) {
                mainActivity.runOnUiThread(() -> Toast.makeText(mainActivity, "Connection lost.", Toast.LENGTH_SHORT).show());
                break;
            }
        }
    }

Write()

Trimite un mesaj prin fluxul de ieșire.

    public void write(byte[] bytes) {
        try {
            outputStream.write(bytes);
        } catch (IOException e) {
            mainActivity.runOnUiThread(() -> Toast.makeText(mainActivity, "Failed to send message.", Toast.LENGTH_SHORT).show());
            Log.d("Connected->Write", e.toString());
        }
    }

Cancel()

Metoda cancel() închide conexiunea Bluetooth prin închiderea obiectului BluetoothSocket.

    public void cancel() {
        try {
            if (socket == null) {
                Log.d("ConnectedThread", "Socket is null, cannot close.");
                return;
            }
            socket.close();
        } catch (IOException e) {
            Log.d("Connected->Cancel", e.toString());
        }
    }

Rezumat

  • AcceptThread: Ascultă conexiunile Bluetooth primite.
  • ConnectThread: Inițiază conexiuni Bluetooth către alte dispozitive.
  • ConnectedThread: Gestionează comunicarea între dispozitive.

Aceste clase funcționează împreună pentru a crea o aplicație de chat complet funcțională. Asigură-te că gestionezi corect permisiunile și închizi conexiunile neutilizate!

Laborator 06. Invocarea de Servicii Web prin Protocolul HTTP

În dezvoltarea modernă a aplicațiilor mobile, observăm o tendință din ce în ce mai pregnantă de a transfera sarcina procesării datelor către servere în cloud. Această abordare, cunoscută sub numele de cloud computing, permite dispozitivelor de la edge (precum smartphone-urile sau tabletele cu Android) să funcționeze doar ca interfețe pentru utilizator, responsabile în principal cu apelarea serviciilor și afișarea rezultatelor. Avantajele sunt multiple: reducem consumul de resurse pe dispozitivele mobile, beneficiem de puterea de procesare superioară a serverelor din cloud, și putem scala aplicația mai ușor în funcție de necesități.

În plus, această arhitectură permite actualizarea și îmbunătățirea logicii de business fără a necesita modificări în aplicația mobilă, oferind astfel o flexibilitate sporită în dezvoltare și mentenanță. Astăzi vom explora implementarea practică a acestui model, concentrându-ne pe crearea unei aplicații Android care va comunica eficient cu serviciile cloud.

Protocolul HTTP

De multe ori, funcționalitatea pe care o pun la dispoziție aplicațiile Android este preluată din alte surse, datorită limitărilor impuse de capacitatea de procesare și memorie disponibilă ale unui dispozitiv mobil. O strategie posibilă în acest sens este utilizarea HTTP, pentru interogarea unor servicii web, al căror rezultat este de cele mai multe ori oferit în format JSON sau XML. De asemenea, descărcarea unor resurse se poate face prin inspectarea codului sursă al unor pagini Internet (documente HTML), în urma acestei operații detectându-se locația la care acestea sunt disponibile.

HTTP (Hypertext Transfer Protocol) este un protocol de comunicație responsabil cu transferul de hipertext (text structurat ce conține legături) dintre un client (de regulă, un navigator) și un server web, interacțiunea dintre acestea (prin intermediul unei conexiuni TCP persistente pe portul 80) fiind reglementată de RFC 2616. HTTP este un protocol fără stare, pentru persistența informațiilor între accesări fiind necesar să se utilizeze soluții adiacente (cookie, sesiuni, rescrierea URL-urilor, câmpuri ascunse).

Principalele concepte cu care lucrează acest protocol sunt cererea și răspunsul.

  • cererea este transmisă de client către serverul web și reprezintă o solicitare pentru obținerea unor resurse (identificate printr-un URL); aceasta conține denumirea metodei care va fi utilizată pentru transferul de informații, locația de unde se găsește resursa respectivă și versiunea de protocol;
  • răspunsul este transmis de serverul web către client, ca rezultat al solicitării primite, incluzând și o linie de stare (ce conține un cod care indică dacă starea comenzii) precum și alte informații suplimentare

Structura unei Cereri HTTP

O cerere HTTP conține una sau mai multe linii de text ASCII, precedate în mod necesar de denumirea metodei specificând operația ce se va realiza asupra conținutului respectiv:

DENUMIRE METODĂ DESCRIERE
GET descărcarea resursei specificate de pe serverul web pe client; majoritatea cererilor către un server web sunt de acest tip
GET /page.html HTTP/1.1 Host: www.server.com
POST transferul de informații de către client cu privire la resursa specificată, acestea urmând a fi prelucrate de serverul web
POST /page.html HTTP/1.1 Host: www.server.com attribute1=value1&...&attributen=valuen

GET vs. POST

Deși atât metoda GET cât și metoda POST pot fi utilizate pentru descărcarea conținutului unei pagini Internet, transmițând către serverul web valorile unor anumite atribute, între acestea există anumite diferențe:

  • o cerere GET poate fi reținută în cache, fapt ce nu este valabil și pentru o cerere POST;
  • o cerere GET rămâne în istoricul aplicației de navigare, fapt ce nu este valabil și pentru o cerere POST;
  • o cerere GET poate fi reținută printre paginile Internet favorite din cadrul programului de navigare, fapt ce nu este valabil și pentru o cerere POST;
  • o cerere GET impune unele restricții cu privire la lungimea (maxim 2048 caractere) și la tipul de date (doar caractere ASCII) transmise (prin URL), fapt ce nu este valabil și pentru o cerere POST;
  • o cerere GET nu trebuie folosită atunci când sunt implicate informații critice (acestea fiind vizibile în URL), fapt ce nu este valabil și pentru o cerere POST;
  • o cerere GET ar trebui să fie folosită doar pentru obținerea unei resurse, fapt ce nu este valabil și pentru o cerere POST.

O linie de cerere HTTP poate fi succedată de unele informații suplimentare, reprezentând antetele de cerere, acestea având forma atribut:valoare, fiind definite următoarele proprietăți:

  • User-Agent - informații cu privire la browser-ul utilizat și la platforma pe care rulează acesta
  • informații cu privire la conținutul pe care clientul îl dorește de la serverul web, având capacitatea de a-l procesa; dacă serverul poate alege dintre mai multe resurse pe care le găzduiește, va alege pe cele care respectă constrângerile specificate, altfel întoarce un cod de eroare
    • Accept - tipul MIME
    • Accept-Charset - setul de caractere
    • Accept-Encoding - mecanismul de codificare
    • Accept-Language - limba
  • Host (obligatoriu) - denumirea gazdei pe care se găsește resursa (specificată în URL); necesară întrucât o adresă IP poate fi asociată mai multor nume de DNS
  • Authorization - informații de autentificare în cazul unor operații care necesită drepturi privilegiate
  • Cookie - transmite un cookie primit anterior
  • Date - data și ora la care a fost transmisă cererea

Structura unui Răspuns HTTP

Un răspuns HTTP este format din linia de stare, antetele de răspuns și posibile informații suplimentare, conținând o parte sau toată resursa care a fost solicitată de client de pe serverul web.

În cadrul liniei de stare este inclus un cod din trei cifre care indică dacă solicitarea a putut fi îndeplinită sau nu (situație în care este indicată și cauza).

FAMILIE DE CODURISEMNIFICAȚIEDESCRIERE
1xxInformațierăspuns provizoriu, constând din linia de stare și alte antete (fără conținut, terminat de o linie vidă), indicând faptul că cererea a fost primită, procesarea sa fiind încă în desfășurare; nu este utilizată în HTTP/1.0
2xxSuccesrăspuns ce indică faptul că cererea a fost primită, înțeleasă, acceptată și procesată cu succes
3xxRedirectarerăspuns transmis de serverul web ce indică faptul că trebuie realizate acțiuni suplimentare din partea clientului (cu sau fără interacțiunea utilizatorului, în funcție de metoda folosită) pentru ca cererea să poată fi îndeplinită; în cazul în care redirectarea se repetă de mai multe ori, se poate suspecta o buclă infinită
4xxEroare la clientrăspuns transmis de serverul web ce indică faptul că cererea nu a putut fi îndeplinită, datorită unei erori la nivelul clientului; mesajul include și o entitate ce conține o descriere a situației, inclusiv tipul acesteia (permanentă sau temporară)
5xxEroare la servercod de răspuns ce indică clientului faptul că cererea nu a putut fi îndeplinită, datorită unei erori la nivelul serverului web; mesajul include și o entitate ce conține o descriere a situației, inclusiv tipul acesteia (permanentă sau temporară)

Mesajul conține și antetele de răspuns, având forma atribut:valoare, fiind definite următoarele proprietăți:

  • Server - informații cu privire la mașina care găzduiește resursa care este transmisă
  • informații cu privire la proprietățile conținutului care este transmis
    • Content-Encoding - mecanismul de codificare
    • Content-Language - limba
    • Content-Length - dimensiunea
    • Content-Type - tipul MIME
  • Last-Modified - ora și data la care pagina Internet a fost modificată
  • Location - informație prin care serverul web informează clientul de faptul că ar trebui folosit alt URL (resursa a fost mutată sau trebuie accesată o pagină Internet localizată în funcție de anumite preferințe)
  • Accept-Ranges - informație referitoare la transmiterea conținutului solicitat în mai multe părți, corespunzătoare unor intervale de octeți
  • Set-Cookie - transmiterea unui cookie de la serverul web la client, acesta trebuind să fie inclus în antetele ulterioare ale mesajelor schimbate între cele două entități

API interactiune HTTP

In general, nu vom scrie noi cererile HTTP peste sockets. Vom folosi astazi mai multe biblioteci ce implementeaza aceasta functionalitate.

Apache HTTP Components

Apache HTTP Components este un proiect open-source, dezvoltat sub licență Apache, punând la dispoziția utilizatorilor o bibliotecă Java pentru accesarea de resurse prin intermediul protocolului HTTP.

Vom folosi o versiune legacy in acest laborator. In cadrul laboratorului veti putea folosi orice alternative, precum OkHttp.

Pentru ca metodele din API-ul Apache HTTP Components să poată fi utilizate într-o aplicație Android este necesar să se specifice catre sistemul de build, în fișierul build.gradle din app:

...

android {

  // pentru fisiere build.gradle.kt
  useLibrary("org.apache.http.legacy")
  // pentru fisiere build.gradle
  useLibrary 'org.apache.http.legacy'
}

...

De asemenea, in android manifest trebuie sa punem permisiunea de network

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission
        android:name="android.permission.INTERNET"/>
    ...

Pentru a evita blocarea thread-ului main, oricare din apelurile catre API-ul de HTTP va trebui executat pe un thread secundar. Va reamintim o varianta cu un Thread si un handler pentru comunicare.

public class MainActivity extends AppCompatActivity {

    // Handler to communicate with UI thread
    private Handler mainHandler = new Handler(Looper.getMainLooper());
    private TextView resultTextView;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        resultTextView = findViewById(R.id.resultTextView);
        
        // Start HTTP request when a button is clicked
        Button fetchButton = findViewById(R.id.fetchButton);
        // When the button is pressed, make the HTTP request
        fetchButton.setOnClickListener(v -> makeHttpRequest());
    }
    
    private void makeHttpRequest() {
        // We create a new thread, and inside we will run the code for the HTTP API.
        new Thread(() -> {
            try {
                // mak
                mainHandler.post(() -> {
                    // Here we can run code on the main thread, and use variables
                    // defined in the activity such as restulTextView
                    resultTextView.setText(result);
                });
                
            } catch (Exception e) {
                // Handle error on main thread
                mainHandler.post(() -> {
                    resultTextView.setText("Error: " + e.getMessage());
                });
            }
        }).start();
    }
}

GET. Urmatorul exemplu prezinta o cere simpla de tip GET.

try {
  HttpClient httpClient = new DefaultHttpClient();
  HttpGet httpGet = new HttpGet("http://jepi.cs.pub.ro/expr/expr_get.php?operation=times&t1=9&t2=2");

  ResponseHandler<String> responseHandler = new BasicResponseHandler();
  String content = httpClient.execute(httpGet, responseHandler);
  if (content != null) {  
    /* do something with the response */
    Log.i(Constants.TAG, EntityUtils.toString(httpGetEntity));
  }            
} catch (Exception exception) {
  Log.e(Constants.TAG, exception.getMessage());
  if (Constants.DEBUG) {
    exception.printStackTrace();
  }
}

În situația în care se dorește transmiterea de parametri către serverul web, aceștia trebuie incluși în URL (în clar), fără a se depăși limita de 2048 de caractere și folosind numai caractere ASCII:

HttpGet httpGet = new HttpGet("http:*www.server.com?attribute1=value1&...&attributen=valuen");

POST. Urmatorul exemplu prezinta o cere simpla de tip POST.

try {
  HttpClient httpClient = new DefaultHttpClient();        
  HttpPost httpPost = new HttpPost("http://www.server.com");
  /* Lista de perechi tip (atribut, valoare) care vor contine
     informatiile transmise de client pe baza carora serverul
     va genera continutul, de exemplu (user, eim), (parola, 123)
  */  
  List<NameValuePair> params = new ArrayList<NameValuePair>();        
  params.add(new BasicNameValuePair("attribute1", "value1"));
  * ...
  params.add(new BasicNameValuePair("attributen", "valuen"));

  UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(params, HTTP.UTF_8);
  httpPost.setEntity(urlEncodedFormEntity);
             
  HttpResponse httpPostResponse = httpClient.execute(httpPost);  
  HttpEntity httpPostEntity = httpPostResponse.getEntity();  
  if (httpPostEntity != null) {
    * do something with the response
    Log.i(Constants.TAG, EntityUtils.toString(httpPostEntity));
   }
} catch (Exception exception) {
  Log.e(Constants.TAG, exception.getMessage());
  if (Constants.DEBUG) {
    exception.printStackTrace();
  }
}

Prelucrarea raspunsurilor. Prelucrarea unui răspuns HTTP se poate realiza:

  • prin prelucrarea obiectului de tip HttpEntity, utilizând fluxuri de intrare/ieșire:
    BufferedReader bufferedReader = null;
    StringBuilder result = new StringBuilder();
    try {
      * ...
      bufferedReader = new BufferedReader(new InputStreamReader(httpEntity.getContent()));
      int currentLineNumber = 0;
      String currentLineContent;
      while ((currentLineContent = bufferedReader.readLine()) != null) {
        currentLineNumber++;
        result.append(currentLineNumber).append(": ").append(currentLineContent).append("\n");
      }
      Log.i(Constants.TAG, result.toString());
    } catch (Exception exception) {
      Log.e(Constants.TAG, exception.getMessage());
      if (Constants.DEBUG) {
        exception.printStackTrace();
      }
    } finally {
      if(bufferedReader != null) {
        try {
          bufferedReader.close();
        } catch (IOException ioException) {
          Log.e(Constants.TAG, exception.getMessage());
          if (Constants.DEBUG) {
            ioException.printStackTrace();
          }
        }
      }
    }
    
  • utilizând un obiect de tip ResponseHandler, ce furnizează conținutul resursei solicitate, transmis ca parametru al metodei execute() a clasei HttpClient (pe lângă obiectul HttpGet|HttpPost)

Prelucrare JSON (JavaScript Object Notation)

Unele servicii web folosesc formatul JSON pentru transferul de date întrucât, spre diferență de XML care implică numeroase informații suplimentare, acesta optimizează cantitatea de date implicate. De asemenea, este foarte ușor pentru un utilizator uman să prelucreze informațiile reprezentate într-un astfel de format.

Acest mecanism de reprezentare a datelor a fost utilizat inițial în JavaScript, unde acesta descria literali sau obiecte anonime, folosite o singură dată (fără posibilitatea de a fi reutilizate), informațiile fiind transmise prin intermediul unor șiruri de caractere. Ulterior, a fost preliat pe scară largă.

În principiu, informațiile reprezentate în format JSON au structura unei colecții de perechi de tip (cheie, valoare), fiecare dintre acestea fiind grupate într-o listă de obiecte ordonate. Nu se folosesc denumiri de etichete, utilizându-se doar caracterele ", ,, {, }, [ și ].

[
  {
    "attribute1": "valuem1",
    "attribute2": "valuem2",
    ...,
    "attributen": "valuemn"
  }  
]

În Android, prelucrarea documentelor reprezentate în format JSON este realizată prin intermediul clasei JSONObject.

Pentru a intelege mai bine, vom studia utilizarea API-ului in practica prin parsarea raspunsului JSON intors de catre o aplicatie de cutremure, Geonames Earthquakes.

Detaliile care se doresc a fi vizualizate pentru fiecare cutremur în parte sunt:

  • așezarea geografică (latitudinea și longitudinea);
  • magnitudinea;
  • adâncimea la care a avut loc;
  • sursa informației;
  • data și ora la care s-a înregistrat.

Rezultatele sunt furnizate în următorul format:

{
  "earthquakes":[
                  {
                    "eqid":"c0001xgp",
                    "magnitude":8.8,
                    "lng":142.369,
                    "src":"us",
                    "datetime":"2011-03-11 04:46:23",
                    "depth":24.4,
                    "lat":38.322
                  },
                  {
                    "eqid":"c000905e",
                    "magnitude":8.6,
                    "lng":93.0632,
                    "src":"us",
                    "datetime":"2012-04-11 06:38:37",
                    "depth":22.9,
                    "lat":2.311
                  },
                  ...
                ]
}

Astfel, informațiile care trebuie preluate din rezultat sunt:

atribut JSONtip de datedetaliu
latdoublelatitudine
lngdoublelongitudine
magnitudedoublemagnitudinea
depthdoubleadâncimea
srcStringsursa informației
datetimeStringdata și ora la care s-au înregistrat datele

Dupa ce realizam un HTTP GET pentru a primi in format JSON datele, vom parsa astfel:

final ArrayList<EarthquakeInformation> earthquakeInformationList = new ArrayList<EarthquakeInformation>();
JSONObject result = new JSONObject(content);
JSONArray jsonArray = result.getJSONArray(Constants.EARTHQUAKES);
for (int k = 0; k < jsonArray.length(); k++) {
    JSONObject jsonObject = jsonArray.getJSONObject(k);
    earthquakeInformationList.add(new EarthquakeInformation(
    jsonObject.getDouble(Constants.LATITUDE),
    jsonObject.getDouble(Constants.LONGITUDE),
    jsonObject.getDouble(Constants.MAGNITUDE),
    jsonObject.getDouble(Constants.DEPTH),
    jsonObject.getString(Constants.SOURCE),
    jsonObject.getString(Constants.DATETIME)
    ));
}

Database local

Deseori avem nevoie de a stoca mai multe date pentru un utilizator. In acest sens, in Android avem la dispozitie o serie de functionalitati pentru a realiza acest lucru. Astazi vom folosi SQLite, care vine inclusa in sistemul de operare.

Unul dintre principalele principii ale bazelor de date SQL este schema: o declarație formală a modului în care este organizată baza de date. Schema este reflectată în instrucțiunile SQL pe care le folosești pentru a crea baza de date. Poate fi util să creezi o clasă însoțitoare, cunoscută sub numele de clasă contract, care specifică explicit structura schemei tale într-un mod sistematic și auto-documentat.

O clasă contract este un container pentru constante care definesc nume pentru URI-uri, tables și columns. Clasa contract îți permite să folosești aceleași constante în toate celelalte clase din același package. Acest lucru îți permite să modifici numele unei coloane într-un singur loc și să se propage în tot codul tău.

Un mod bun de a organiza o clasă contract este să pui definițiile care sunt globale pentru întreaga bază de date la nivelul root al clasei. Apoi să creezi o inner class pentru fiecare table. Fiecare inner class enumeră columns-urile table-ului corespunzător.

public final class FeedReaderContract {
    // To prevent someone from accidentally instantiating the contract class,
    // make the constructor private.
    private FeedReaderContract() {}

    /* Inner class that defines the table contents */
    public static class FeedEntry implements BaseColumns {
        public static final String TABLE_NAME = "entry";
        public static final String COLUMN_NAME_TITLE = "title";
        public static final String COLUMN_NAME_SUBTITLE = "subtitle";
    }
}

Crearea unui database

După ce ai definit cum arată baza ta de date, ar trebui să implementezi metode care creează și întrețin baza de date și tabelele. Iată câteva instrucțiuni tipice care creează și șterg un tabel:

private static final String SQL_CREATE_ENTRIES =
    "CREATE TABLE " + FeedEntry.TABLE_NAME + " (" +
    FeedEntry._ID + " INTEGER PRIMARY KEY," +
    FeedEntry.COLUMN_NAME_TITLE + " TEXT," +
    FeedEntry.COLUMN_NAME_SUBTITLE + " TEXT)";

private static final String SQL_DELETE_ENTRIES =
    "DROP TABLE IF EXISTS " + FeedEntry.TABLE_NAME;

La fel ca fișierele pe care le salvezi în memoria internă a dispozitivului, Android stochează baza ta de date în folderul privat al aplicației tale. Datele tale sunt securizate, deoarece în mod implicit această zonă nu este accesibilă altor aplicații sau utilizatorului.

Clasa SQLiteOpenHelper conține un set util de API-uri pentru gestionarea bazei tale de date. Când folosești această clasă pentru a obține referințe la baza ta de date, sistemul efectuează operațiunile potențial de lungă durată de creare și actualizare a bazei de date doar când este necesar și nu în timpul pornirii aplicației. Tot ce trebuie să faci este să apelezi getWritableDatabase() sau getReadableDatabase().

Pentru a utiliza SQLiteOpenHelper, creează o subclasă care suprascrie metodele callback onCreate() și onUpgrade(). De asemenea, poți implementa metodele onDowngrade() sau onOpen(), dar acestea nu sunt obligatorii.

public class FeedReaderDbHelper extends SQLiteOpenHelper {
    // If you change the database schema, you must increment the database version.
    public static final int DATABASE_VERSION = 1;
    public static final String DATABASE_NAME = "FeedReader.db";

    public FeedReaderDbHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(SQL_CREATE_ENTRIES);
    }
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // This database is only a cache for online data, so its upgrade policy is
        // to simply to discard the data and start over
        db.execSQL(SQL_DELETE_ENTRIES);
        onCreate(db);
    }
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        onUpgrade(db, oldVersion, newVersion);
    }
}

Pentru a accesa baza ta de date, instanțiază subclasa ta de SQLiteOpenHelper:

FeedReaderDbHelper dbHelper = new FeedReaderDbHelper(getContext());

Introducerea de date

Inserează date în baza de date prin transmiterea unui obiect ContentValues către metoda insert():

// Gets the data repository in write mode
SQLiteDatabase db = dbHelper.getWritableDatabase();

// Create a new map of values, where column names are the keys
ContentValues values = new ContentValues();
values.put(FeedEntry.COLUMN_NAME_TITLE, title);
values.put(FeedEntry.COLUMN_NAME_SUBTITLE, subtitle);

// Insert the new row, returning the primary key value of the new row
long newRowId = db.insert(FeedEntry.TABLE_NAME, null, values);

Citirea de date

Pentru a citi dintr-o bază de date, folosește metoda query(), transmițându-i criteriile tale de selecție și coloanele dorite. Metoda combină elemente din insert() și update(), cu excepția faptului că lista de coloane definește datele pe care vrei să le preiei (denumită "projection"), și nu datele de inserat. Rezultatele interogării îți sunt returnate într-un obiect Cursor.

SQLiteDatabase db = dbHelper.getReadableDatabase();

// Define a projection that specifies which columns from the database
// you will actually use after this query.
String[] projection = {
    BaseColumns._ID,
    FeedEntry.COLUMN_NAME_TITLE,
    FeedEntry.COLUMN_NAME_SUBTITLE
    };

// Filter results WHERE "title" = 'My Title'
String selection = FeedEntry.COLUMN_NAME_TITLE + " = ?";
String[] selectionArgs = { "My Title" };

// How you want the results sorted in the resulting Cursor
String sortOrder =
    FeedEntry.COLUMN_NAME_SUBTITLE + " DESC";

Cursor cursor = db.query(
    FeedEntry.TABLE_NAME,   // The table to query
    projection,             // The array of columns to return (pass null to get all)
    selection,              // The columns for the WHERE clause
    selectionArgs,          // The values for the WHERE clause
    null,                   // don't group the rows
    null,                   // don't filter by row groups
    sortOrder               // The sort order
    );

Stergerea datelor

Pentru a șterge rânduri dintr-un tabel, trebuie să furnizezi criterii de selecție care identifică rândurile pentru metoda delete(). Mecanismul funcționează la fel ca argumentele de selecție pentru metoda query(). Acesta împarte specificația de selecție în clauza de selecție și argumentele de selecție. Clauza definește coloanele care trebuie examinate și îți permite, de asemenea, să combini teste pe coloane. Argumentele sunt valori de testat care sunt legate în clauză. Deoarece rezultatul nu este tratat la fel ca o instrucțiune SQL obișnuită, acesta este imun la injecția SQL.

// Define 'where' part of query.
String selection = FeedEntry.COLUMN_NAME_TITLE + " LIKE ?";
// Specify arguments in placeholder order.
String[] selectionArgs = { "MyTitle" };
// Issue SQL statement.
int deletedRows = db.delete(FeedEntry.TABLE_NAME, selection, selectionArgs);

Actualizarea datelor

Când trebuie să modifici un subset din valorile bazei tale de date, folosește metoda update(). Actualizarea tabelului combină sintaxa ContentValues a metodei insert() cu sintaxa WHERE a metodei delete().

SQLiteDatabase db = dbHelper.getWritableDatabase();

// New value for one column
String title = "MyNewTitle";
ContentValues values = new ContentValues();
values.put(FeedEntry.COLUMN_NAME_TITLE, title);

// Which row to update, based on the title
String selection = FeedEntry.COLUMN_NAME_TITLE + " LIKE ?";
String[] selectionArgs = { "MyOldTitle" };

int count = db.update(
    FeedReaderDbHelper.FeedEntry.TABLE_NAME,
    values,
    selection,
    selectionArgs);

Activitate de Laborator

Un lucru din ce în ce mai întâlnit este procesarea datelor pe un server în cloud, în timp ce pe dispozitivul de la edge (în cazul nostru, dispozitivul Android) se realizează doar apelarea unui serviciu. Astăzi vom implementa această arhitectură. Scheletul laboratorului.

1a. Din browser, vom verifica în browser funcționarea serverului serverului pentru GET și POST

1b. Să se verifice la linia de comandă functionarea severului

curl  -X  POST  --data 'operation=times&t1=9&t2=2' http://jepi.cs.pub.ro/expr/expr_post.php

curl  "http://jepi.cs.pub.ro/expr/expr_get.php?operation=times&t1=9&t2=2"

echo -e 'GET /expr/expr_get.php?operation=plus&t1=9&t2=2 HTTP/1.1\r\nHost:jepi.cs.pub.ro\r\n\r\n' | nc jepi.cs.pub.ro 80

1c. Studiați implementarea serverului în php sau python http://jepi.cs.pub.ro/expr/. Puteți rula un server local cu GET în python expr_get.py pe portul 8080 pentru a experimenta.

2. Pe baza functionalitatii serverului, se cere să se implementeze in Android un calculator, care suportă operațiile de adunare/scădere/înmulțire/împărțire a două numere reale, pe baza rezultatului furnizat de un serviciu web, accesibil prin HTTP, la adresele Internet specificate în interfața Constants, pentru fiecare dintre metodele suportate pentru transmiterea informațiilor:

  • GET: Constants.GET_WEB_SERVICE_ADDDRESS
  • POST: Constants.POST_WEB_SERVICE_ADDRESS

Cele două numere reale sunt specificate în cadrul unor câmpuri text. În situația în care unul dintre acestea nu este precizat, se va afișa un mesaj de eroare corespunzător.

Operația care se dorește a fi efectuată precum și metoda prin care vor fi transmise informațiile către serviciul web vor putea fi selectate prin intermediul unor liste.

Parametrii ce trebuie incluși în cadrul cererii HTTP sunt:

operation = plus|minus|divide|times
t1 = număr real
t2 = număr real

De exemplu, http://jepi.cs.pub.ro/expr/expr_get.php?operation=times&t1=9&t2=2 pentru a inmulti 9 cu 8.

În cadrul unui câmp text va putea fi vizualizat răspunsul HTTP furnizat de serviciul web.

3. Vom extinde aplicatia cu un textview care afiseaza un citat motivational. Vom adauga un buton care face un request la adresa https://dummyjson.com/quotes, si afiseaza intr-un textview primul citat din JSON-ul returnat.

4. Să se salveze într-o bază de date SQLite toate operațiile executate de utilizator. Se va adăuga un nou buton în aplicație și la apăsarea acestuia, istoricul va fi afișat în Logcat.

Laborator 09. Descoperirea Serviciilor de Rețea

O problemă des întâlnită de un dispozitiv mobil este descoperirea de funcționalități care pot fi accesate într-o rețea locală vizitată. De obicei, se dorește stabilirea de conexiuni punct la punct peste care pot fi tehnologiile clasice de tipul client/server sau RPC. Totodată, pot fi expuse și servicii clasice ale unor alte dispozitive din cadrul rețelei locale cum ar fi: calculatoare, imprimante, televizoare, ceasuri inteligente, acestea putând fi astfel accesate fără configurare prealabilă.

Serviciile din rețea pot fi implementate folosind două variante:

  1. Android Network Service Discovery (NSD), un protocol integrat în Android începând cu nivelul de API 16 (Jelly Bean), pentru implementarea de servicii disponibile în rețeaua locală (neabordat în acest laborator);
  2. JmDNS, un proiect open-source care își propune implementarea în Java a unor funcționalități legate de proiectarea și dezvoltarea de servicii disponibile în rețeaua locală, fără a realiza nici un fel de configurări legate de infrastructura de comunicație.

Atât Android NSD cât și JmDNS folosesc multi-cast DNS (utilizarea de operații DNS în rețele de dimensiuni mici, în care nu există un server propriu-zis pentru un astfel de serviciu) pentru accesul la servicii în rețeaua locală.

Operațiile utilizate în implementarea serviciilor de rețea sunt:

  1. configurarea diferiților parametri (pregătirea mediului de lucru);
  2. înregistrarea unui serviciu, prin care celelalte dispozitive din rețeaua locală pot afla detalii cu privire la funcționalitatea oferită (tip de serviciu, adresă, port, descriere);
  3. descoperirea unui serviciu, prin care un dispozitiv este informat cu privire la serviciile care pot fi accesate în rețeaua locală, filtrându-le în funcție de denumire și tip;
  4. rezolvarea unui serviciu, prin care sunt identificate adresa și portul la care trebuie realizată o conexiune în vederea exploatării funcționalității pe care acesta o pune la dispoziție.

Note

Operațiile de înregistrare respectiv descoperire / rezolvare sunt independente, astfel încât o aplicație poate alege doar să înregistreze un serviciu, în timp ce altă aplicație poate doar să descopere / rezolve serviciile din rețeaua locală. Totodată, operațiile pot fi realizate și împreună, fără a exista o ordine prestabilită.


Tipuri de Servicii

Cele mai multe servicii accesibile prin rețeaua locală sunt descrise prin intermediul unui tip care are de obicei forma _<protocol_nivel_aplicatie>._<protocol_nivel_transport>., unde:

  • <protocol_nivel_aplicatie> poate fi standard sau definit de utilizator;
  • <protocol_nivel_transport> este de regulă tcp sau udp (sau variații ale acestora).

Se poate consulta lista cu tipurile de servicii rezervate, gestionată de IANA (Autoritatea Internațională pentru Numere Alocate). Unele tipuri de servicii au definite și porturile pe care pot fi accesate.Pentru rezervarea unui astfel de tip de serviciu, este necesară o solicitare prealabilă, care poate fi aprobată sau respinsă, după caz.

În cazul JmDNS, trebuie să se precizeze faptul că serviciul rulează local, definindu-se un tip corespunzător aplicației care implementează funcționalitatea respectivă (_<denumire_aplicatie>._tcp.local.).

Zeroconf sub Linux

Se poate crea un advertisement de serviciu sub Linux prin crearea unui fișier de configurare, folosiți un alt nume în loc de perfectdesktop:

$ cat   /etc/avahi/services/chat.service 
<?xml version="1.0" standalone='no'?>
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
  <name>Chat-perfectdesktop</name>
  <service>
    <type>_chatservice._tcp</type>
    <port>5003</port>
  </service>
</service-group>

Se restartează severul de Zeroconf cu comanda

# systemctl restart avahi-daemon.service

Apoi se poate verifica cu tcpdump traficul specific DNS-SD care este generat:

# tcpdump -ni eth0 -s0 -w 'file1.pcap' 'udp port 5353'

Traficul capturat în file1.pcap poate fi examinat cu wireshark file1.pcap. Dacă ați rulat tcpdump pe telefon, este necesară aducerea fișierului .pcap pe desktop cu ajutorul comenzii adb pull /sdcard/file1.pcap .

Din alt terminal, se poate lansa o căutare de servicii de tipul chatservice:

# avahi-browse -rk _chatservice._tcp

Iar în tcpdump se observă primirea queryurilor pentru serviciu, și răspunsul desktop-ului. Atenție, acesta nu este un serviciu care rulează pe portul 5003, ci doar anunțul(advertisement) pentru serviciu.

Configurare

Inițializarea mediului de lucru presupune instanțierea unor obiecte care gestionează operațiile ce pot fi realizate la nivelul serviciilor de rețea, respectiv a unor obiecte ascultător pentru evenimentele care pot surveni în cadrul acestora.

JmDNS

JmDNS folosește pachete de tip multicast pentru a gestiona serviciile disponibile în rețea. Politica Android este de a dezactiva implicit astfel de transferuri, pentru a optimiza bateria, motiv pentru care această funcționalitate trebuie activată temporar, doar pe parcursul aplicației Android. În acest sens, trebuie obținut mutexul corespunzător operațiilor de acest tip, care va fi eliberat ulterior:

public class ChatActivity extends Activity {

  * ...
  
  protected WifiManager wifiManager = null;

  @Override
  protected void onCreate(Bundle state) {
    super.onCreate(state);
  
    * ...

    WifiManager wifiManager = (WifiManager)getSystemService(Context.WIFI_SERVICE);
    multicastLock = wifiManager.createMulticastLock(Constants.TAG);
    multicastLock.setReferenceCounted(true);
    multicastLock.acquire();
  }

  @Override
  protected void onDestroy() {
    super.onDestroy();
  
    * ...
  
    if (multicastLock != null) {
      multicastLock.relase();
      multicastLock = null;
    }
  }
  
  * ...
  
}

Configurarea JmDNS presupune crearea unei instanțe a unui obiect de tipul JmDNS, care oferă funcționalități referitoare la gestiunea serviciilor existente doar în cadrul rețelei locale. Se utilizează metoda statică create(InetAddress, String), care primește ca parametri:

  • adresa mașinii, obținută prin intermediul metodei getIpAddress() apelată pe obiectul ConnectionInfo asociat obiectului care gestionează interfața pentru comunicația prin intermediul rețelei fără fir;
  • denumirea mașinii (determinată pe baza adresei, prin rezoluție inversă).
try {
  WifiManager wifiManager = ((ChatActivity)context).getWifiManager();
  InetAddress address = InetAddress.getByAddress(
    ByteBuffer.allocate(4).putInt(
      Integer.reverseBytes(wifiManager.getConnectionInfo().getIpAddress())
    ).array()
  );
  String name = address.getHostName();
  Log.i(Constants.TAG, "address = " + address + " name = " + name);
  jmDns = JmDNS.create(address, name);
} catch (IOException ioException) {
  Log.e(Constants.TAG, "An exception has occurred: " + ioException.getMessage());
  if (Constants.DEBUG) {
    ioException.printStackTrace();
  }
}   

Note

Instanțierea unui obiect de tip JmDNS trebuie să se realizeze pe firul de execuție al comunicației prin rețea, în caz contrar generându-se o excepție de tipul android.os.NetworkOnMainThreadException.


API-ul pe care îl pune la dispoziție un astfel de obiect este asincron, astfel încât metodele apelate în cadrul claselor ascultător pentru diferite evenimente (înregistrare, descoperire, rezolvare) sunt executate în contextul unor fire de execuție dedicate, așa cum trebuie procedat în condițiile unor operații ce implică comunicația prin rețea.

  • înregistrare
  • descoperire
    • addServiceListener(String, ServiceListener) - folosită pentru pornirea descoperirii de servicii accesibile în rețeaua locală, operație care va fi realizată permanent, până când se va specifica altfel (explicit), afectând resurse precum transferul de informații prin rețeaua locală (lățimea de bandă) și bateria; vor fi monitorizate doar serviciile de un anumit tip;
    • removeServiceListener(ServiceListener) - folosită pentru oprirea descoperirii de servicii de un anumit tip, atunci când acesta nu mai este necesară sau aplicația Android este întreruptă temporar.
  • rezolvare - requestServiceInfo(String, String), pentru a identifica parametrii de conexiune (adresă și port) ai unui serviciu pentru care se cunoaște tipul și denumirea.â

Se observă că de regulă aceste metode primesc parametrii de tip:

  • ServiceInfo - în care sunt stocate perechi de tipul (atribut, valoare) cu privire la serviciul disponibil în rețea:
    • denumire;
    • tip / subtip;
    • adresă / denumire dispozitiv care găzduiește serviciul;
    • port pe care poate fi accesat serviciul;
    • prioritate;
    • protocol
    • date
    • URL.
  • ascultător pentru diferite evenimente legate de operațiile cu serviciile accesibile în rețea (înregistrare, descoperire, rezolvare), fiecare definind metode care descriu comportamentul pentru fiecare rezultat posibil al acestora.

Resursele asociate obiectului de tip JmDNS trebuie eliberate în momentul în care acesta nu mai este necesar (aplicația Android este distrusă):

try {
  if (jmDns != null) {
    jmDns.close();
    jmDns = null;
  }
} catch (IOException ioException) {
  Log.e(Constants.TAG, "An exception has occurred: " + ioException.getMessage());
  if (Constants.DEBUG) {
    ioException.printStackTrace();
  }
}

În fișierul AndroidManifest.xml, permisiunile care trebuie oferite aplicației vizează:

  • accesul la Internet;
  • schimbarea politicii cu privire la procesarea pachetelor de tip multi-cast (implicit dezactivate, pentru a optimiza consumul de energie);
  • accesul la starea rețelei fără fir;
  • accesul la starea rețelei cu fir.
<manifest xmlns:android="http:*schemas.android.com/apk/res/android"
    package="ro.pub.cs.systems.eim.lab09.chatservice"
    android:versionCode="1"
    android:versionName="1.0">
    
    <uses-permission
        android:name="android.permission.INTERNET"/>
    <uses-permission
        android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
    <uses-permission
        android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission
        android:name="android.permission.ACCESS_NETWORK_STATE" />
        
    <!-- ... -->

</manifest>

Înregistrarea / Deînregistrarea unui Serviciu

Pentru înregistrarea unui serviciu, se apelează metoda registerService(ServiceInfo) din clasa JmDNS, ulterior serviciul putând fi accesat din cadrul altor mașini fizice / dispozitive mobile.

Pentru deînregistrarea unui serviciu, se poate apela una din metodele unregisterService(ServiceInfo), respectiv unregisterAllServices() din clasa JmDNS.


Note

Aceste metode primesc ca parametru un obiect de tip ServiceInfo, ce reține informații precum denumirea și tipul serviciului, adresa și portul mașinii / dispozitivului pe care va fi disponibil (ce pot fi preluate, din cadrul unui obiect de tip ServerSocket).


public void registerNetworkService() throws Exception {
  chatServer = new ChatServer(this);
  ServerSocket serverSocket = chatServer.getServerSocket();
  if (serverSocket == null) {
    throw new Exception("Could not get server socket");
  }
  chatServer.start();
  
  ServiceInfo serviceInfo = ServiceInfo.create(
    Constants.SERVICE_TYPE,
    Constants.SERVICE_NAME,
    port,
    Constants.SERVICE_DESCRIPTION
  );
  
  if (jmDns != null && serviceInfo != null) {
    serviceName = serviceInfo.getName();
    jmDns.registerService(serviceInfo);
  }
}

public void unregisterNetworkService() {
  if (jmDns != null) {
    jmDns.unregisterAllServices();
  } 
  
  chatServer.stopThread();
  
  * ...
}

Note

JmDNS nu oferă informații cu privire la rezultatul operațiilor de înregistrare / deînregistrare.


Descoperirea Serviciilor Accesibile

Operațiile de pornire / oprire a descoperirii serviciilor disponibile trebuie să aibă în vedere resursele afectate precum și impactul asupra altor funcționalități cum ar fi viteza de transfer prin rețea, respectiv autonomia.

De regulă, pornirea operației de descoperire a serviciilor disponibile este realizată pe metoda onResume(), adică din momentul în care activitatea este vizibilă.

Similar, oprirea operației de descoperire a serviciilor disponibile este realizată pe metoda onPause(), adică din momentul în care activitatea nu este vizibilă.

Totodată, este recomandat să se pună la dispoziția utilizatorilor elemente în cadrul interfeței grafice prin intermediul cărora aceste operații să poată fi controlate prin interacțiunea cu cei care folosesc aplicația Android.

JmDNS

**Pornirea ** operației de descoperire a serviciilor disponibile se face prin intermediul metodei addServiceListener(String, ServiceListener) din clasa JmDNS care primește ca parametri:

  • tipul de serviciu;
  • un obiect ascultător de tipul ServiceListener, care reacționează la evenimentele legate de serviciile găsite / pierdute.

Oprirea operației de descoperire a serviciilor disponibile se face prin intermediul metodei removeServiceListener(String, ServiceListener) din clasa JmDNS care primește ca parametru un obiect ascultător de tipul ServiceListener, care reacționează la evenimentele legate de serviciile găsite / pierdute.

Ca atare, informațiile cu privire la găsirea / pierderea serviciilor în rețeaua locală vor fi furnizate numai între apelurile metodelor addServiceListener(), respectiv removeServiceListener().

public void startNetworkServiceDiscovery() {
  if (jmDns != null && serviceListener != null) {
    jmDns.addServiceListener(Constants.SERVICE_TYPE, serviceListener);
  }
}
    
public void stopNetworkServiceDiscovery() {
  if (jmDns != null && serviceListener != null) {
    jmDns.removeServiceListener(Constants.SERVICE_TYPE, serviceListener);
  }
        
  * ...
}

Pentru obiectul de tip ServiceListener trebuie implementate metodele apelate în mod automat în momentul în care un serviciu este găsit, respectiv este pierdut:

  • serviceAdded(ServiceEvent) - se verifică parametrii serviciului descoperit (tip și denumire), putându-se întâlni următoarele situații:

    1. tip de serviciu necunoscut (cu toate că descoperirea implică filtrarea după un tip de servicii specific);
    2. descoperirea serviciului oferit de mașina curentă / dispozitivul curent - se realizează comparația dintre denumirea serviciului găsit și denumirea serviciului curent;
    3. descoperirea unui serviciu oferit de o altă mașină / un alt dispozitiv (se poate folosi un șablon pe denumirea serviciului) - se trece la rezolvarea serviciului respectiv (prin invocarea metodei requestServiceInfo(String, String) din cadrul obiectului de tip JmDNS.
  • serviceRemoved(ServiceEvent) - se gestionează corespunzător lista de servicii descoperite.

Un obiect de tipul ServiceInfo conține, printre altele, și informații cu privire la:

ServiceListener serviceListener = new ServiceListener() {

  * ...
  
  @Override
  public void serviceAdded(ServiceEvent serviceEvent) {
    if (!serviceEvent.getType().equals(Constants.SERVICE_TYPE)) {
      Log.i(Constants.TAG, "Unknown Service Type: " + serviceEvent.getType());
    } else if (serviceEvent.getName().equals(serviceName)) {
      Log.i(Constants.TAG, "The service running on the same machine has been discovered: " + serviceName);
    } else if (serviceEvent.getName().contains(Constants.SERVICE_NAME_SEARCH_KEY)) {
      Log.i(Constants.TAG, "The service should be resolved now: " + serviceEvent);
      jmDns.requestServiceInfo(serviceEvent.getType(), serviceEvent.getName());
    }
  }
  
  @Override
  public void serviceRemoved(final ServiceEvent serviceEvent) {
    ServiceInfo serviceInfo = serviceEvent.getInfo();
    if (serviceInfo == null) {
      Log.e(Constants.TAG, "Service Info for Service is null!");
      return;
    }
    
    String[] hosts = serviceInfo.getHostAddresses();
    String host = null;
    if (hosts.length != 0) {
      host = hosts[0];
      if(host.startsWith("/")) {
        host = host.substring(1);
      }
    }
    
    int port = serviceInfo.getPort();
    
    * ...
    
    ArrayList<NetworkService> discoveredServices = chatActivity.getDiscoveredServices();
    NetworkService networkService = new NetworkService(serviceEvent.getName(), host, port, -1);
    if (discoveredServices.contains(networkService)) {
      int index = discoveredServices.indexOf(networkService);
      discoveredServices.remove(index);
      chatActivity.setDiscoveredServices(discoveredServices);
    }
  }
};

Note

Alternativ, rezolvarea serviciului descoperit poate fi realizată ad-hoc (fără a se apela metoda serviceResolved()), prin apelul metodei getServiceInfo(String, String), cu precizarea că timpul său de execuție poate fi considerabil:

ServiceInfo serviceInfo = serviceEvent.getDNS().getServiceInfo(
  serviceEvent.getType(), 
  serviceEvent.getName()
);


Rezolvarea Serviciilor Descoperite Anterior

Rezolvarea unui serviciu descoperit anterior implică obținerea de informații suplimentare cu privire la acesta.

Astfel, pentru un serviciu pentru care se cunoaște doar tipul și denumirea, se poate determina, prin intermediul multi-cast DNS, adresa și portul la care acesta este disponibil, putând fi accesat.

JmDNS

În cadrul metodei serviceAdded() din ascultătorul de tip ServiceListener, în situația în care denumirea serviciului corespunde unui anumit șablon (deși o astfel de condiție nu este întotdeauna necesară), se invocă metoda requestServiceInfo(String, String) care primește ca parametri:

  • tipul serviciului;
  • denumirea serviciului.
Service serviceListener = new ServiceListener() {
  * ...
  
  @Override
  public void serviceAdded(ServiceEvent serviceEvent) {
  
    * ...
    if (serviceEvent.getName().contains(Constants.SERVICE_NAME_SEARCH_KEY)) {
      Log.i(Constants.TAG, "The service should be resolved now: " + serviceEvent);
      jmDns.requestServiceInfo(serviceEvent.getType(), serviceEvent.getName());
    }               
  }
};

În situația în care operația de rezolvare a serviciului a fost realizată cu succes, se apelează metoda serviceResolved(ServiceEvent) în cadrul aceluiași obiect ascultător (de tip ServiceListener). Aceasta primește ca parametru un obiect ServiceEvent care încapsulează informații cu privire la serviciul rezolvat, sub forma unui obiect de tip ServiceInfo. Și în această situație se va gestiona corespunzător lista de servicii descoperite în rețeaua locală, obținându-se și un canal de comunicație către parametrii determinați.

ServiceListener serviceListener = new ServiceListener() {
  @Override
  public void serviceResolved(ServiceEvent serviceEvent) {
    if (serviceEvent.getName().equals(serviceName)) {
      Log.i(Constants.TAG, "The service running on the same machine has been discovered.");
      return;
    }
    
    ServiceInfo serviceInfo = serviceEvent.getInfo();
    if (serviceInfo == null) {
      Log.e(Constants.TAG, "Service Info for Service is null!");
      return;
    }
    
    String[] hosts = serviceInfo.getHostAddresses();
    String host = null;
    if (hosts.length == 0) {
      Log.e(Constants.TAG, "No host addresses returned for the service!");
      return;
    }
    host = hosts[0];
    if(host.startsWith("/")) {
      host = host.substring(1);
    }
    
    int port = serviceInfo.getPort();
    
    ArrayList<NetworkService> discoveredServices = chatActivity.getDiscoveredServices();
    NetworkService networkService = new NetworkService(serviceEvent.getName(), host, port, Constants.CONVERSATION_TO_SERVER);
    if (!discoveredServices.contains(networkService)) {
      discoveredServices.add(networkService);
      communicationToServers.add(new ChatClient(null, host, port));
      chatActivity.setDiscoveredServices(discoveredServices);
    }
    
    Log.i(Constants.TAG, "A service has been discovered on " + host + ":" + port);              
  }
  
  * ...
};

Gestiunea unei Conexiuni către Servicii Disponibile

Fiecare dispozitiv poate fi avea în același timp rolul de:

  • server pentru serviciile pe care le-a înregistrat, putând primi solicitări din partea clienților;
  • client pentru serviciile pe care le-a descoperit, putând trimite solicitări către servere.

În acest sens, va trebui menținut un obiect server, care va gestiona comunicația cu clienții.

Totodată, vor trebui menținuți mai mulți clienți, reprezentând canalele de comunicație de tip punct-la-punct care pot fi de două tipuri:

  • canale de comunicație pentru solicitări primite (clienți);
  • canale de comunicație pentru servicii descoperite (servere).
public class NetworkServiceDiscoveryOperations {
    
  private ChatServer chatServer = null;
  
  private List<ChatClient> communicationToServers   = null;
  private List<ChatClient> communicationFromClients = null;
  
  * ...
  
}

Actualizarea acestor obiecte va fi realizată mai ales în situația în care un serviciu este rezolvat, respectiv un serviciu este pierdut.

Obiectul de tip ChatServer este de fapt un fir de execuție pe care se instanțiază un obiect de tip ServerSocket, așteptându-se, în bucla principală, solicitări de la clienți, actualizându-se corespunzător lista conținând canalele de comunicație pentru solicitările primite.

De remarcat faptul că a fost definită și o metodă pentru oprirea firului de execuție și închiderea obiectului ServerSocket, aceasta urmând a fi apelată la deînregistrarea serviciului respectiv.

public class ChatServer extends Thread {

  private NetworkServiceDiscoveryOperations networkServiceDiscoveryOperations = null;
  
  private ServerSocket serverSocket = null;
  
  public ChatServer(NetworkServiceDiscoveryOperations networkServiceDiscoveryOperations, int port) {
    this.networkServiceDiscoveryOperations = networkServiceDiscoveryOperations;
    try {
      serverSocket = new ServerSocket(port);
    } catch (IOException ioException) {
      Log.e(Constants.TAG, "An error has occurred during server run: " + ioException.getMessage());
      if (Constants.DEBUG) {
        ioException.printStackTrace();
      }
    }
  }
  
  public void run() {
    try {
      while (!Thread.currentThread().isInterrupted()) {
        Socket socket = serverSocket.accept();
        List<ChatClient> communicationFromClients = networkServiceDiscoveryOperations.getCommunicationFromClients();
        communicationFromClients.add(new ChatClient(null, socket));
        networkServiceDiscoveryOperations.setCommunicationFromClients(communicationFromClients);
      }
    } catch (IOException ioException) {
      Log.e(Constants.TAG, "An error has occurred during server run: " + ioException.getMessage());
      if (Constants.DEBUG) {
        ioException.printStackTrace();
      }
    }
  }
  
  public void stopThread() {
    interrupt();
    try {
      if (serverSocket != null) {
        serverSocket.close();
      }
    } catch (IOException ioException) {
      Log.e(Constants.TAG, "An error has occurred while closing server socket: " + ioException.getMessage());
      if (Constants.DEBUG) {
        ioException.printStackTrace();
      }
    }
  }
}

Obiectul de tip ChatClient definește un canal de comunicație bidirecțional între două mașini / dispozitive, care poate fi creat pe baza unei adrese și a unui port (conexiune către server) sau pe baza altui canal de comunicație (conexiune de la client).

Acesta încapsulează două fire de execuție:

  1. SendThread - pentru trimiterea de mesaje;
  2. ReceiveThread - pentru primirea de mesaje.

Atât mesajele trimise cât și mesajele trimise sunt plasate și în interfața grafică (dacă aceasta este vizibilă) dar și într-un obiect membru al clasei.

Au fost definite metode atât pentru pornirea cât și pentru oprirea firelor de execuție. Astfel, firele de execuție pentru trimiterea / primirea de mesaje rulează cât timp nu sunt întrerupte.

public class ChatClient {
  private Socket  socket  = null;
  
  private SendThread    sendThread    = null;
  private ReceiveThread receiveThread = null;
  
  private BlockingQueue<String> messageQueue        = new ArrayBlockingQueue<String>(Constants.MESSAGE_QUEUE_CAPACITY);
  private List<Message>         conversationHistory = new ArrayList<Message>();
  
  public ChatClient(final String host, final int port) {
    try {
      socket = new Socket(host, port);
    } catch (IOException ioException) {
      Log.e(Constants.TAG, "An exception has occurred while creating the socket: " + ioException.getMessage());
      if (Constants.DEBUG) {
        ioException.printStackTrace();
      }
    }
    if (socket != null) {
      startThreads();
    }
  }
  
  public ChatClient(Context context, Socket socket) {
    this.socket  = socket;
    if (socket != null) {
      startThreads();
    }
  }
  
  public void sendMessage(String message) {
    try {
      messageQueue.put(message);
    } catch (InterruptedException interruptedException) {
      Log.e(Constants.TAG, "An exception has occurred: " + interruptedException.getMessage());
      if (Constants.DEBUG) {
        interruptedException.printStackTrace();
      }
    }
  }
  
  private class SendThread extends Thread {
    @Override
    public void run() {
      PrintWriter printWriter = Utilities.getWriter(socket);
      if (printWriter != null) {
        try {
          while (!Thread.currentThread().isInterrupted()) {
            String content = messageQueue.take();
            if (content != null) {
              printWriter.println(content);
              printWriter.flush();
              Message message = new Message(content, Constants.MESSAGE_TYPE_SENT);
              conversationHistory.add(message);
              * display the message in the graphic user interface
            }
          }
        } catch (InterruptedException interruptedException) {
          Log.e(Constants.TAG, "An exception has occurred: " + interruptedException.getMessage());
          if (Constants.DEBUG) {
            interruptedException.printStackTrace();
          }
        }
      }
    }
    
    public void stopThread() {
      interrupt();
    }
  }
  
  private class ReceiveThread extends Thread {
    @Override
    public void run() {
      BufferedReader bufferedReader = Utilities.getReader(socket);
      if (bufferedReader != null) {
        try {
          while (!Thread.currentThread().isInterrupted()) {
            String content = bufferedReader.readLine();
            if (content != null) {
              Message message = new Message(content, Constants.MESSAGE_TYPE_RECEIVED);
              conversationHistory.add(message);
              * display the message in the graphic user interface
            }
          }
        } catch (IOException ioException) {
          Log.e(Constants.TAG, "An exception has occurred: " + ioException.getMessage());
          if (Constants.DEBUG) {
            ioException.printStackTrace();
          }
        }
      }
    }
    
    public void stopThread() {
      interrupt();
    }
  }
  
  public void setConversationHistory(List<Message> conversationHistory) {
    this.conversationHistory = conversationHistory;
  }
  
  public List<Message> getConversationHistory() {
    return conversationHistory;
  }
  
  public void startThreads() {
    sendThread = new SendThread();
    sendThread.start();
    
    receiveThread = new ReceiveThread();
    receiveThread.start();
  }
  
  public void stopThreads() {
    sendThread.stopThread();
    receiveThread.stopThread();
    try {
      if (socket != null) {
        socket.close();
      }
    } catch (IOException ioException) {
      Log.e(Constants.TAG, "An exception has occurred while closing the socket: " + ioException.getMessage());
      if (Constants.DEBUG) {
        ioException.printStackTrace();
      }
    }
  }
}

Activitate de Laborator

Se dorește implementarea unei aplicații Android de tip mesagerie instantanee bazată pe legături de tip punct-la-punct stabilite între dispozitive mobile care oferă un serviciu de acest tip și dispozitive mobile care l-au descoperit, putându-l accesa în cadrul rețelei locale.

Funcționalitățile pe care le implementează această aplicație Android sunt:

  1. înregistrarea / deînregistrarea unui serviciu de mesagerie instantanee, pe un anumit port pe care îl specifică;

    Important note: trebuie folosit un port mai mare de 1024 ca sa nu fie privilegiat.

  2. pornirea / oprirea operației de descoperire a serviciilor în rețeaua locală;

  3. conectarea / deconectarea la un serviciu descoperit sau la un dispozitiv mobil care a accesat serviciul înregistrat;

  4. transmiterea de mesaje în cadrul legăturii de tip punct-la-punct.

Va fi utilizat următorul cod de culori pentru butoanele care au atașată funcționalitatea de înregistrare / deînregistrare, respectiv pornire / oprire:

  • roșu - serviciul este oprit;
  • verde - serviciul este pornit.

În cazul în care descoperirea serviciilor este pornită, vor fi întreținute două liste:

  • lista cu serviciile descoperite, disponibile în cadrul altor dispozitive mobile din rețeaua locală;
  • lista conținând conversațiile cu alte dispozitive mobile care au accesat serviciul înregistrat.

În situația în care descoperirea serviciilor este oprită, lista cu serviciile descoperite va fi vidă.

Comunicația se va realiza în cadrul unei ferestre dedicate, în care pot fi vizualizate diferit mesajele trimise și mesajele primite. Din cadrul acestei interfețe grafice se poate reveni în orice moment la panoul de control.

1. În contul GitHub personal, să se creeze un depozit denumit 'Laborator09'. Inițial, acesta trebuie să fie gol (nu trebuie să bifați nici adăugarea unui fișier README.md, nici a fișierului .gitignore sau a a fișierului LICENSE).

2. Să se cloneze în directorul de pe discul local conținutul depozitului la distanță de la .

În urma acestei operații, directorul Laborator09 va trebui să se conțină directoarele labtasks și solutions.

student@eg106:~$ git clone https://www.github.com/eim-lab/Laborator09.git

3. Să se încarce conținutul descărcat în cadrul depozitului 'Laborator09' de pe contul GitHub personal.

student@eg106:~$ cd Laborator09
student@eg106:~/Laborator09$ git remote add Laborator09_perfectstudent https:*github.com/perfectstudent/Laborator09
student@eg106:~/Laborator09$ git push Laborator09_perfectstudent master

4. Să se importe în mediul integrat de dezvoltare Android Studio proiectul ChatServiceJmDNS din directorul labtasks.

5. Să se ruleze aplicația Android pe două dispozitive mobile care să se găsească în aceeași rețea.

5a.


Note

În cazul Genymotion, se pot crea două imagini pentru două dispozitive mobile (virtuale) de același tip, care vor putea fi rulate separat, fiecare dintre acestea primind adrese Internet diferite din intervalul 192.168.56.101192.168.56.254. Pentru interfața WiFi, Genymotion nu poate folosi decât NAT sau Bridge (manual Genymotion). Pentru acest laborator, este necesară configurația bridge.

În VirtualBox (> versiunea 4.3.26), se verifică faptul că dispozitivele virtuale comunică între ele prin intermediul unei interfețe de rețea, configurată să folosească Bridge.

În acest sens, trebuie realizate următoarele operații:

  • în configurația aferentă fiecărui dispozitiv virtual (MachineSettings sau Ctrl + S), se va selecta Bridge folosind rețeaua astfel definită pentru interfața Adapter 2
  • apoi se setează o adresă MAC random pentru adapter 2 (manual, sau cu butonul asociat)

Acestea vor putea rula instanțe diferite ale aplicației Android, fiecare folosind o denumire proprie pentru serviciu (la valoarea generică Constants.SERVICE_NAME definită în pachetul ro.pub.cs.systems.eim.lab09.chatservice.general se sufixează în mod automat un șir de caractere generat aleator, astfel încât aceasta să fie unică în rețeaua locală).

Se verifică faptul că fiecare aplicație Android rulează pe un dispozitiv diferit:

vbox_bridge.png
  • în configurația aferentă fiecărui dispozitiv virtual (MachineSettings sau Ctrl + S), se va selecta Bridged folosind rețeaua astfel definită pentru interfața Adapter 2
  • În acest mod, fiecare emulator va fi cuplat în rețeaua laboratorului, iar cu comanda avahi-browse -rk _chatservice._tcp se pot vizualiza toate instanțele care rulează în acel moment

Telefon personal

  • Pentru a folosi telefonul personal, se recomanda rețeaua WiFi EG106, care este în bridge cu toate PCurile din sala. Se poate folosi un emulator în bridge ca partener pentru telefon.
  • În Logcat, se pot utiliza filtre diferite pentru fiecare dintre dintre instanțele aplicației Android, astfel încât să se faciliteze procesul de depanare.

5b. Să se utilizeze utilitarul ZeroConf Browser, deja instalat pe emulatoare pentru a identifica serviciile pornite în rețea. Dacă emulatoarele sunt în aceeași rețea cu o mașină Linux, se poate rula avahi-browse -rk _chatservice._tcp pentru a vizualiza serviciile pornite

5c. La momentul publicării cu succes a serviciului (când butonul devine verde), să se colecteze adresele IP locale pe care serviciul devine vizibil. Aceasta se face în funcția OnCreateView a listenerului care primește schimbarea stării serviciului (ServiceDiscoveryStatusButtonListener).

        ...
        if (view == null) {
            view = inflater.inflate(R.layout.fragment_chat_network_service, parent, false);
        }
        * List all IPs where the server is visible
        Enumeration interfaces = null;
        try {
            interfaces = NetworkInterface.getNetworkInterfaces();
        } catch (IOException e){
            Log.e(Constants.TAG, "Could not query interface list: " + e.getMessage());
            if (Constants.DEBUG) {
                e.printStackTrace();
            }
        }
        String IPs = "";
        while(interfaces.hasMoreElements())
        {
            NetworkInterface n = (NetworkInterface) interfaces.nextElement();
            Enumeration ee = n.getInetAddresses();
            while (ee.hasMoreElements())
            {
                InetAddress i = (InetAddress) ee.nextElement();
                if (i instanceof Inet4Address) {
                    if(IPs.length() > 0)
                        IPs += ", ";
                    IPs += i.getHostAddress().toString();
                }
            }
        }
        TextView LocalIPs = (TextView)view.findViewById(R.id.service_discovery_local_addr);
        LocalIPs.setText(IPs);

        return view;

Verificați afișarea adreselor pe care serviciul devine disponibil. Utilitarele de explorare servicii (ZeroconfBrowser, avahi-browser) vor vedea serviciul pe una dintre aceste adrese.

5d. La apăsarea butonului de publicare serviciu, se va modifica titlul aplicației pentru a reflecta starea curentă a publicării:

În register:

 chatActivity.setTitle(serviceInfo.getName());
        Log.i(Constants.TAG, "Register service " +
                serviceInfo.getName() + ":" +
                serviceInfo.getTypeWithSubtype() + ":" +
                serviceInfo.getPort()
        );

În unregister:

chatActivity.setTitle("Chat Service JmDNS");

6. Să se implementeze rutina pentru trimiterea mesajelor, pe metoda run() a firului de execuție SendThread din clasa ChatClient a pachetului ro.pub.cs.systems.eim.lab09.chatservice.networkservicediscoveryoperations.

Metoda va rula cât timp firul de execuție nu este întrerupt, informație oferită de metoda isInterrupted().

while (!Thread.currentThread().isInterrupted()) {
  * ...
}

Pe fiecare iterație, se vor realiza următoarele operații:

  1. se va prelua un mesaj din coada messageQueue (de tip BlockingQueue<String>); metoda take() este blocantă, în așteptarea unei valori care să poată fi furnizată - astfel, în momentul în care firul de execuție este întrerupt, se va genera o excepție de tipul InterruptedException care trebuie tratată;
  2. în cazul unui mesaj nenul, sunt realizate următoarele operații:
    1. se trimite mesajul pe canalul de comunicație corespunzător (se apelează metoda println() a obiectului de tip PrintWriter);
    2. se construiește un obiect de tip Message, format din:
      1. conținut: valoarea propriu-zisă a mesajului (șirul de caractere);
      2. tip: mesaj transmis (Constants.MESSAGE_TYPE_SENT);
    3. se atașează mesajul istoricului conversației (obiectul conversationHistory este de tipul List<Message>) - în acest fel, chiar dacă interfața grafică nu este vizibilă la momentul în care este trimis mesajul, conversația poate fi încărcată atunci când se întâmplă acest lucru;
    4. se afișează mesajul în fragmentul de tip ChatConversationFragment (prin intermediul metodei appendMessage()), dacă acesta este asociat activității la momentul respectiv if (context != null) { ChatActivity chatActivity = (ChatActivity)context; FragmentManager fragmentManager = chatActivity.getFragmentManager(); Fragment fragment = fragmentManager.findFragmentByTag(Constants.FRAGMENT_TAG); if (fragment instanceof ChatConversationFragment && fragment.isVisible()) { ChatConversationFragment chatConversationFragment = (ChatConversationFragment)fragment; chatConversationFragment.appendMessage(message); } }

7. Să se implementeze rutina pentru primirea mesajelor, pe metoda run() a firului de execuție ReceiveThread din clasa ChatClient a pachetului ro.pub.cs.systems.eim.lab09.chatservice.networkservicediscoveryoperations.

Metoda va rula cât timp firul de execuție nu este întrerupt, informație oferită de metoda isInterrupted().

while (!Thread.currentThread().isInterrupted()) {
  * ...
}

Pe fiecare iterație, se vor realiza următoarele operații:

  1. se primește mesajul pe canalul de comunicație corespunzător (se apelează metoda readLine() a obiectului de tip BufferedReader);
  2. în cazul unui mesaj nenul, sunt realizate următoarele operații:
    1. se construiește un obiect de tip Message, format din:
      1. conținut: valoarea propriu-zisă a mesajului (șirul de caractere);
      2. tip: mesaj transmis (Constants.MESSAGE_TYPE_RECEIVED);
    2. se atașează mesajul istoricului conversației (obiectul conversationHistory este de tipul List<Message>);
    3. se afișează mesajul în fragmentul de tip ChatConversationFragment (prin intermediul metodei appendMessage()), dacă acesta este asociat activității la momentul respectiv.

8. Să se examineze folosind tcpdump/wireshark schimbul de mesaje între dispozitive

  1. asigurați-vă folosind ping că telefoanele sunt în contact IP între ele, și cu mașina host (Linux)
  2. notați adresele IP folosite de fiecare dispozitiv: pentru telefoane, ele sunt afișate pe rândul 2 al activității, pentru Linux, folosind comanda ip ro.
  3. porniți din starea în care toate butoanele sunt pe roșu (serviciul nu este public, iar descoperirea este inactivă). În linux, directorul /etc/avahi/services/ nu conține nimic
  • Pe linux, cu comanda avahi-browse -ac, identificați serviciile și stațiile disponibile în rețeaua locală
  • Pe Android, folosind utilitarul ZeroconfBrowser, identificați serviciile și stațiile disponibile în rețeaua locală. Alternativă din Google Play: ServiceBrowser by Andriy Druk.
  • Folosind avahi-browse -rk _chatservice._tcp, asigurați-vă că nu există instanțe ale serviciului pe vreuna dintre mașini
    • pregătiți recoltarea pachetelor folosind comanda tcpdump -ni any 'udp port 5353' -w ./dnssd.pcap
  • activați serviciul pe unul dintre telefoane, comanda avahi-browse de mai sus identifică apariția numelor și perechii IP/port.
    • ce fel de mesaj este folosit pentru publicarea serviciului?
    • observați folosirea înregistrărilor de tip PTR, SRV
    • dacă ați pornit mai multe emulatoare/telefoane, observați cumularea răspunsurilor anterioare în fiecare răspuns
    • cum se face descoperirea serviciilor (prin ce tip de mesaje)?

9. Folosind instrucțiunile din secțiunea Zeroconf sub Linux de mai sus

  • pe PC-ul local publicați un serviciu pe portul 5003
    • rulați serviciul folosind while true; do nc -v -l -p 5003; done
  • verificați interoperabilitatea cu clienții implementați pe Android
    • puteți folosi clienți Linux cu comanda nc <adresă> <port> pentru a conversa cu un server descoperit la adresa:port (pe Android sau Linux).

10. Să se încarce modificările realizate în cadrul depozitului 'Laborator09' de pe contul GitHub personal, folosind un mesaj sugestiv.

student@eg106:~/Laborator09$ git add *
student@eg106:~/Laborator09$ git commit -m "implemented tasks for laboratory 09"
student@eg106:~/Laborator09$ git push Laborator09_perfectstudent master

Laborator 10. Gestiunea Apelurilor Multimedia folosind SIP & VoIP

SIP (Session Initiation Protocol)

SIP (Session Initiation Protocol) este un protocol de nivel aplicație, definit de RFC 3261, folosit împreună cu alte protocoale pentru a gestiona sesiunile de comunicație multimedia la nivelul Internetului. Este frecvent folosit în cadrul tehnologiei VoIP, una dintre cele mai ieftine, portabile, flexibile și facile soluții pentru transmiterea de conținut audio și video prin intermediul rețelei de calculatoare. Singura cerință impusă pentru folosirea tehnologiei VoIP este existența unei legături la Internet.

Prin intermediul SIP sunt gestionate sesiuni care reprezintă o legătură punct la punct, un canal de comunicație între două entități. Este inspirat de HTTP și SMTP, de la care a preluat arhitectura client-server, respectiv schemele de codificare ale mesajelor, împărțite în antet și corp. SIP folosește SDP (Session Decription Protocol) pentru a califica o sesiune (unicast sau multicast) și RTP (Real Time Transport Protocol) pentru a transmite conținutul multimedia prin intermediul Internetului.

În cadrul infrastructurii de comunicație, fiecare element este identificat prin intermediul unui URI (Uniform Resource Identifier), având un rol determinat:

  • agent utilizator (eng. user agent) reprezintă o entitate care comunică (telefon mobil, tabletă, calculator): aceasta poate porni sau opri o sesiune și de asemenea poate opera modificări asupra ei; poate fi de mai multe tipuri:
    • UAC (User Agent Client) - entitatea care trimite cererea și primește răspunsul;
    • UAS (User Agent Service) - entutatea care primește cererea și trimite răspunsul;
  • agent intermediar (eng. proxy server) reprezintă un element din cadrul rețelei de calculatoare care retransmite mesajul între agenți; acesta poate înțelege conținutul mesajului, pe baza căruia decide pe ce rută să îl ghideze; numărul de astfel de elemente între doi agenți utilizatori este de maximum 70; poate fi de mai multe tipuri:
    • cu stare (eng. stateful) - reține informații despre mesajele pe care le-a prelucrat și le poate folosi (în situația în care nu se primește nici un răspuns sau în situația în care mesajul ajunge încă o dată sub aceeași formă);
    • fără stare (eng. stateless) - nu reține informații despre mesajele pe care le-a prelucrat;
  • serverul de înregistrare (eng. registrar server) reține URI-uri despre entități pe care le stochează într-o bază de date și pe care le partajează cu alte servere de înregistrare din cadrul aceluiași domeniu; prin intermediul său, agenții utilizatori se autentifică în cadrul rețelei de calculatoare;
  • serverul de redirectare (eng. redirect server) verifică baza de date cu locații, transmițând un răspuns către agentul utilizator;
  • serverul de localizare (eng. location server) oferă informații cu privire la plasarea posibilă a agentului utilizator către serverele intermediare sau serverele de redirectare (doar acestea îl pot accesa)

Un flux operațional standard al unei sesiuni SIP implică următoarele operații:

  1. tranzacția 1:
    1. un agent utilizator (sursă) trimite o cerere de tip INVITE către un agent intermediar în scopul de a contacta un alt agent utilizator (destinație);
    2. agentul intermediar
      1. trimite înapoi (imediat) un răspuns de tip 100 Trying către agentul utilizator sursă (pentru ca acesta să nu mai transmită nimic);
      2. caută agentul utilizator destinație folosind un server de localizare și îi trimite (mai departe) cererea de tip INVITE;
    3. agentul utilizator destinație transmite, prin intermediul agentului intermediar, un răspuns de tipul 180 Ringing, către agentul utilizator sursă;
  2. tranzacția 2: în momentul în care agentul utilizator destinație este contactat, acesta transmite, tot prin intermediul agentului intermediar, un răspuns de tipul 200 OK, către agentul utilizator sursă și, din acest moment, conexiunea este realizată, transmițându-se pachete RTP în ambele sensuri;
  3. tranzacția 3: orice participant poate transmite un mesaj de tipul BYE pentru a termina legătura, fiind necesar ca acesta să fie confirmat prin 200 OK de către cealaltă parte.

Se observă faptul că o sesiune de comunicare este împărțită în mai multe tranzacții care împreună alcătuiesc un dialog.

Mesajele în protocolul SIP sunt de două tipuri:

  • cereri au forma <METODĂ> <URI> unde metodele pot fi:
    • de bază
      • INVITE reprezintă o cerere pentru deschiderea unei sesiuni cu un agent utilizator, putând conține informații de tip multimedia în corpul său; aceasta este considerată că a fost îndeplinită cu succes dacă s-a primit un cod de răspuns de tipul 2xx sau s-a transmis un ACK; un dialog stabilit între doi agenți utilizatori continuă până în momentul în care se transmite un mesaj de tipul BYE;
      • BYE este metoda folosită pentru a închide o sesiune cu un agent utilizator, putând fi trimisă de oricare dintre entitățile din canalul de comunicație, fără a trece prin serverul de înregistrare;
      • REGISTER indică o cerere de înregistrare a unui agent utilizator către un server de înregistrare; un astfel de mesaj este transmis mai departe până ajunge la o entitate care deține autoritatea de a realiza această operație; o înregistrare poate fi realizată de un agent utilizator în numele altui agent utilizator (eng. third party registration);
      • CANCEL este operația folosită pentru a închide o sesiune care nu a fost încă deschisă, putând fi transmisă fie de către un agent utilizator fie de către un agent intermediar;
      • ACK este folosit pentru a confirma o cerere de tip INVITE;
      • OPTIONS este utilizat pentru a interoga un agent utilizator sau un server intermediar despre capabilitățile sale și pentru a determina disponibilitatea sa, rezultatul fiind o listă a funcționalităților entității respective;
    • extensii: SUBSCRIBE, NOTIFY, REFER, INFO, UPDATE, PRACK, MESSAGE;
  • răspunsuri reprezintă un mesaj generat de un agent utilizator de tip server sau de un server SIP în replică la o cerere provenită de la un agent utilizator de tip client; acesta poate reprezenta inclusiv o confirmare formală pentru a preveni retransmisiile; există mai multe tipuri de coduri de răspuns:
CLASATIPDESCRIEREACȚIUNE
1xxProvizoriuInformațieSe precizează starea unui apel înainte ca un rezultat să fie disponibil.
2xxDefinitivSuccesCererea a fost procesată cu succes. Pentru cereri de tip INVITE se întoarce ACK. Pentru alte tipuri cereri, se oprește retransmiterea acestora.
3xx:::RedirectareSe indică faptul că au fost furnizate mai multe locații posibile astfel încât ar trebui interogat un alt server pentru a se putea obține informația necesară.
4xx:::Eroare la ClientCererea nu a fost procesată cu succes datorită unei erori la client, fiind necesar ca aceasta să fie reformulată.
5xx:::Eroare la ServerCererea nu a fost procesată cu succes datorită unei erori la server, putând fi retransmisă către o altă entitate.
6xx:::Eroare GlobalăCererea a eșuat și nu există nici o șansă de a fi procesată corect pe o altă entitate, nici măcar dacă este reformulată.

Exemple:

  • 100 Trying, 180 Ringing, 181 Call is Being Forwarded, 182 Call Queue, 183 Session Progress;
  • 200 OK, 202 Accepted;
  • 300 Multiple Choices, 301 Moved Permanently, 302 Moved Temporarily, 305 Use Proxy, 380 Alternative Service;
  • 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 405 Method Not Allowed, 406 Not Acceptable, 407 Proxy Authentication Required, 408 Request Timeout, 422 Session Timer Interval Too Small, 423 Interval Too Brief, 480 Temporarily Unavailable, 481 Dialog/Transaction Does Not Exist, 483 Too Many Hops, 486 Busy Here, 487 Request Terminated;
  • 500 Server Internal Error, 501 Not Implemented, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout, 505 Version Not Supported, 513 Message Too Large, 580 Preconditions Failure;
  • 600 Busy Everywhere, 603 Decline, 604 Does Not Exist Anywhere, 606 Not Acceptable.

Configurare


Note

În cadrul acestui laborator este necesar un dispozitiv mobil fizic sau un emulator cu acces la microfonul sistemului de operare. De asemenea, pe dispozitivul mobil trebuie să fie instalat Google Play Services întrucât este necesară descărcarea și instalarea unei aplicații Android (de exemplu Linphone, din Playstore) pentru realizarea de apeluri telefonice folosind SIP & VoIP.


Se va utiliza, la alegere, un furnizor (gratuit) de servicii SIP, accesibil ulterior înregistrării (creării unui cont):

  • recomandare 1: Linphone
  • recomandare 2: PBXES
  • recomandare 3: antisip
  • sunt mulți alți provideri de SIP care oferă diverse servicii contra cost: numere de telefon stabile în diverse țări, SMS, cutii poștale vocale, rutarea apelurilor de la și căre PSTN prin SIP, etc
  • În această secțiune vă veti crea un cont, si veti obtine credențiale de acest tip:
Phone ConfigurationSIP account
Address of Record:eim-lab@sip.linphone.org
SIP Password:YoUrPaSs
Username:eim-lab
Domain/Proxy:sip.linphone.org

Utilizare pe Dispozitivul Mobil

Adrese SIP pentru Testare

Pot fi folosite următoarele adrese de test:

  • 904@mouselike.org;
  • thetestcall@sip.linphone.org;
  • altele.

Linphone Stack

Stiva SIP face parte din SDK-ul Android începând cu versiunea 2.3 (Gingerbread, API level 9), însă nu dispune încă de toate funcționalitățile (mesagerie instantanee, apeluri video), iar începând cu versiunea 11 a fost dezactivată.

Se preferă însă utilizarea API-urilor Flexisip, care implementează o stivă SIP completă, dispunând și de documentarea metodelor care pot fi utilizate:

Pentru integrarea acestei funcționalități în cadrul unui proiect Android Studio, este necesat ca inițial să se cloneze depozitul labtasks corespunzător.

1. În fișierul build.gradle corespunzător aplicației siplinphone se poate include biblioteca linphone fie din maven-ul linphone:

repositories {
    maven {
        url "https://linphone.org/maven_repository"
    }
}

dependencies {
    ...
    implementation 'org.linphone:linphone-sdk-android:5.0+'
    ...
}

fie ca un fișier .aar inclus direct în proiect în directorul 'libs':

repositories {
    flatDir {
        dirs("libs")
    }
}

dependencies {
    ...
    implementation(name:'linphone-sdk-android-5.3.96', ext:'aar')
    ...
}

2. În fișierul AndroidManifest.xml se poate include acest serviciu pentru a preveni terminarea aplicației în timpul unui apel (nu este obligatoriu ).


 <service android:name="org.linphone.core.tools.service.CoreService"
            android:foregroundServiceType="phoneCall|camera|microphone"
            android:stopWithTask="false" />

Inițial, este necesar să se obțină o referință către obiectul de tip Core fără de care nu se poate folosi biblioteca linphone. Acest lucru poate fi realizat prin intermediul obiectului Factory, obținut după importul 'org.linphone.core.*'.
De regulă, o astfel de operație este realizată pe metoda onCreate() a activității principale a aplicației Android.

        val factory = Factory.instance()
        val core = factory.createCore(null, null, this)

Obiectele Core și Factory furnizezaă furnizează toate metodele care sunt necesare pentru operațiile specifice SIP: autentificare, înregistrare, înregistrarea callback-urilor pentru evenimentele specifice plarsării și primirii apelurilor.

Motorul Linphone trebuie configurat prin specificarea unor parametri, reținuți sub forma unor perechi de tipul (cheie, valoare). O parte dintre aceștia sunt specificați folsoind utilitare din obiectele factory sau core:

Pentru ca serviciul SIP să poată fi accesat, trebuie demarată procedura REGISTER:

        val authInfo = Factory.instance().createAuthInfo(username, null, password, null, null, domain, null)

        val params = core.createAccountParams()
        val identity = Factory.instance().createAddress("sip:$username@$domain")
        params.identityAddress = identity

        val address = Factory.instance().createAddress("sip:$domain")
        address?.transport = transportType
        params.serverAddress = address
        params.setRegisterEnabled(true)
        val account = core.createAccount(params)

        core.addAuthInfo(authInfo)
        core.addAccount(account)

        core.defaultAccount = account
        core.addListener(coreListener)
        core.start()

Deasemenea, trebuie cerută de la utilizator permisiunea 'RECORD_AUDIO'

if (packageManager.checkPermission(Manifest.permission.RECORD_AUDIO, packageName) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(arrayOf(Manifest.permission.RECORD_AUDIO), 0)
}

Obiectul coreListener permite suprascrierea unor callback-uri care ne ajută să implementăm funcționalități SIP Acesta trebuie inițializat înainte de procedura REGISTER, deoarece imediat ce utilizatorul este prezent pe servere poate primi apeluri saau mesaje. Acesta este derivat din clasa CoreListenerStub din care se vor suprascrie cel puțin funcțiile onAccountRegistrationStateChanged și onCallStateChanged.

onAccountRegistrationStateChanged primește un parametru state care poate avea următoarele valori pe care trebuie sa le tratăm:

  • RegistrationState.Failed: credențialele nu au fost acceptate, sau serverul SIP nu răspunde
  • RegistrationState.Ok: înregistrarea s-a efectuat cu succes

onCallStateChanged deasemenea primește un parametru state ce poate avea următoarele valori de tratat:

  • Call.State.IncomingReceived: se primește un apel audio
  • Call.State.Connected: interlocutorul a răspuns
  • Call.State.Released: interlocutorul a închis apelul
  • Call.State.Connected: interlocutorul a răspuns la apel

Pentru aceste situații de obicei se activează/dezactivează diverse elemente de interfață care sunt relevante pentru noua stare a apelului.

Activitate de Laborator

Se dorește implementarea unei aplicații Android care folosește stiva Android Linphone pentru a realiza apeluri de voce precum și comunicație prin mesagerie instantanee folosind SIP.

Proiectul conține o singură activitate cu două layouturi care primesc alternativ vizibilitatea View.GONE și View.VISIBLE pentru a apărea și a dispărea din spațiul activitătii curente. Varianta inițială a scheletului schimbă între cele două layouturi folosind butoanele Register/Unregister.

1. În contul Github personal, să se creeze un depozit denumit 'Laborator10'. Inițial, acesta trebuie să fie gol (nu trebuie să bifați nici adăugarea unui fișier README.md, nici a fișierului .gitignore sau a a fișierului LICENSE).

2. Să se cloneze în directorul de pe discul local conținutul depozitului la distanță de la .

În urma acestei operații, directorul Laborator10 va trebui să se conțină directorul labtasks.

student@eim-lab:~$ git clone https://github.com/eim-lab/Laborator10.git

Pentru java, comutați pe branchul master-java:

student@eim-lab:~$ git checkout -b master-java

3. Să se încarce conținutul descărcat în cadrul depozitului 'Laborator10' de pe contul Github personal.

student@eim-lab:~$ cd Laborator10
student@eim-lab:~/Laborator10$ git remote add Laborator10_perfectstudent https://github.com/perfectstudent/Laborator10
student@eim-lab:~/Laborator10$ git push Laborator10_perfectstudent master

4. Să se importe în mediul integrat de dezvoltare Android Studio proiectul labtasks/siplinphone.

Credențiale SIP în proiectul Android

Puteți pune credențialele de conectare în fișierul app/src/main/res/values/strings.xml

<resources>
    <string name="mysipname">eim-lab</string>
    <string name="mysipdomain">some_server.sip</string>
    <string name="mysippassword">PaSsWoRd</string>
</resources>

cu condiția să îl scoateți din git, pentru a nu trimite accidental credențialele în repo:

$ cp solutions/SIPLinphone/app/src/main/res/values/strings.xml /tmp
$ git rm solutions/SIPLinphone/app/src/main/res/values/strings.xml
$ mv /tmp/strings.xml solutions/SIPLinphone/app/src/main/res/values/

5. În activitatea MainActivity, layoutul principal, să se implementeze ascultători pentru butoanele Register și Unregister:

  • Register

    1. va configura motorul Linphone, ai cărui parametri sunt plasați sub forma unor valori asociate unor chei
    2. va porni motorul Linphone (metoda start())

    Aceste funcționalități for fi grupate în funcția login() care poate fi apelată din listener

    private fun login() {
        val username = findViewById<EditText>(R.id.username).text.toString()
        val password = findViewById<EditText>(R.id.password).text.toString()
        val domain = findViewById<EditText>(R.id.domain).text.toString()
        val transportType = when (findViewById<RadioGroup>(R.id.transport).checkedRadioButtonId) {
            R.id.udp -> TransportType.Udp
            R.id.tcp -> TransportType.Tcp
            else -> TransportType.Tls
        }
        val authInfo = Factory.instance().createAuthInfo(username, null, password, null, null, domain, null)
    
        val params = core.createAccountParams()
        val identity = Factory.instance().createAddress("sip:$username@$domain")
        params.identityAddress = identity
    
        val address = Factory.instance().createAddress("sip:$domain")
        address?.transport = transportType
        params.serverAddress = address
        params.setRegisterEnabled(true)
        val account = core.createAccount(params)
    
        core.addAuthInfo(authInfo)
        core.addAccount(account)
    
        core.defaultAccount = account
        core.addListener(coreListener)
        core.start()
    
        if (packageManager.checkPermission(Manifest.permission.RECORD_AUDIO, packageName) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(arrayOf(Manifest.permission.RECORD_AUDIO), 0)
            return
        }
    }
    
    
    • Unregister *
     findViewById<Button>(R.id.unregister).setOnClickListener {
            val account = core.defaultAccount
            if(account != null) {
                val params = account.params
                val clonedParams = params.clone()
                clonedParams.setRegisterEnabled(false)
                account.params = clonedParams
                it.isEnabled = false
            }
        }
    

    Succesul acestor operații nu poate fi vizualizat imediat, deoarece aceste rezultate vor fi furnizate prin callback-uri ale obiectului corelistener care va fi definit ca mebru privat al activității:

    class MainActivity:  AppCompatActivity() {
      private lateinit var core: Core
      private val coreListener = object: CoreListenerStub() {
        override fun onAccountRegistrationStateChanged(core: Core, account: Account, state: RegistrationState?, message: String) {
          ...
        }
        override fun onCallStateChanged(core: Core, call: Call, state: Call.State?, message: String) {
          ...
        }
    

Pentru proiectul de astăzi, coreListener va primi callback-uri legate de starea procedurilor de REGISTER, și a stării apelului (incoming sau outgoing).

6. Să se implementeze callback-urile din coreListener pentru starea înregistrării în care se activează și se dezactivează butoanele sau layouturile:

override fun onAccountRegistrationStateChanged(core: Core, account: Account, state: RegistrationState?, message: String) {
            when (state) {
                RegistrationState.Failed -> {
                    findViewById<Button>(R.id.register).isEnabled = true
                }

                RegistrationState.Cleared -> {
                    findViewById<LinearLayout>(R.id.register_layout).visibility = View.VISIBLE
                    findViewById<RelativeLayout>(R.id.call_layout).visibility = View.GONE
                    findViewById<Button>(R.id.register).isEnabled = true
                }

                RegistrationState.Ok -> {
                    findViewById<LinearLayout>(R.id.register_layout).visibility = View.GONE
                    findViewById<RelativeLayout>(R.id.call_layout).visibility = View.VISIBLE
                    findViewById<Button>(R.id.unregister).isEnabled = true
                    findViewById<EditText>(R.id.remote_address).isEnabled = true
                }
                else -> {}
            }
}

7. Sa se adauge în OnCreate listenere pentru butoanele Call și Hang-up

  findViewById<Button>(R.id.call).setOnClickListener {
            outgoingCall()
            findViewById<EditText>(R.id.remote_address).isEnabled = false
            findViewById<Button>(R.id.hang_up).isEnabled = true
            it.isEnabled = false
  }

   private fun outgoingCall() {  
        val remoteSipUri = findViewById<EditText>(R.id.remote_address).text.toString()
        val remoteAddress = Factory.instance().createAddress("sip:$remoteSipUri")
        remoteAddress ?: return 
        val params = core.createCallParams(null)
        params ?: return 
        params.mediaEncryption = MediaEncryption.None
        // initiate call, but status will be in coreListener:onCallStateChanged
        core.inviteAddressWithParams(remoteAddress, params)
    }

  findViewById<Button>(R.id.hang_up).setOnClickListener {

            findViewById<EditText>(R.id.remote_address).isEnabled = true
            findViewById<Button>(R.id.call).isEnabled = true

            if (core.callsNb != 0) {
                val call = if (core.currentCall != null) core.currentCall else core.calls[0]
                if(call != null)
                    call.terminate()
            }
  }

În acest moment pot fi testate apeluri către roboți de SIP, cum ar fi:

  • 904@mouselike.org
  • thetestcall@sip.linphone.org

8. Să se implementeze callback-urile din coreListener pentru starea apelului:

override fun onCallStateChanged(core: Core, call: Call, state: Call.State?, message: String) {
            when (state) {
                Call.State.IncomingReceived -> {
                    findViewById<Button>(R.id.hang_up).isEnabled = true
                    findViewById<Button>(R.id.answer).isEnabled = true
                }
                Call.State.Connected -> {
                    findViewById<Button>(R.id.mute_mic).isEnabled = true
                    findViewById<Button>(R.id.toggle_speaker).isEnabled = true
                    Toast.makeText(this@MainActivity, "remote party answered",  Toast.LENGTH_LONG).show()
                }
                Call.State.Released -> {
                    findViewById<Button>(R.id.hang_up).isEnabled = false
                    findViewById<Button>(R.id.answer).isEnabled = false
                    findViewById<Button>(R.id.mute_mic).isEnabled = false
                    findViewById<Button>(R.id.toggle_speaker).isEnabled = false
                    findViewById<EditText>(R.id.remote_address).text.clear()
                    findViewById<Button>(R.id.call).isEnabled = true
                }

                else -> {}
            }
        }

Pentru a accepta apeluri, trebuie activat butonul Answer cu un apel la core:

        findViewById<Button>(R.id.answer).setOnClickListener {
            core.currentCall?.accept()
        }

În acest moment pot fi testate apeluri între diverse instanțe ale laboratorului, sau comunica cu un alt client SIP.

9. Să se analizeze conversația SIP la nivel pachet folosind un apel de voce către o adresă SIP de test (thetestcall@sip.linphone.org). Pentru a captura pachetele în device este nevoie de root (telefon rutat, imagine root-ată).

Aveți root pe device

Aveți root pe device

    student@eim-lab:/opt/android-sdk-linux/platform-tools$ ./adb -s 192.168.56.101:5555 shell

În consola sistemului de operare Android, se folosește utilitarul tcpdump pentru monitorizarea traficului de pachete.

Binarele pentru acest utilitar, precompilate pentru sisteme de operare Android, folosind arhitecturi ARM, pot fi descărcate de pe Android TCP Dump.

În situația în care este necesar ca acest utilitar să fie instalat pe alte arhitecturi (de exemplu, Genymotion folosește x86), binarul acestuia poate fi obținut folosind utilitarul build-android-tcpdump care însă are nevoie de NDK precum și de alte programe (flex, bison).

Transferul binarului tcpdump de pe mașina fizică pe dispozitivul mobil (rootat) sau pe emulator se face astfel:

    student@eim-lab:/android/sdk/platform-tools$ ./adb -s 192.168.65.101:5555 push tcpdump /data/bin

Notă:

Utilitarul tcpdump se instalează în /data/bin, apoi se conferă drepturi de execuție pentru binar:

root@android:/data/bin# chmod 777 tcpdump


Monitorizarea propriu-zisă a pachetelor UDP pe interfața de rețea eth1 poate fi realizată prin intermediul următoarei comenzi:

root@android:/# ./tcpdump -s0 -ni eth1 -w /sdcard/DCIM/sip.pcap 'udp'

Se pornește apelul audio și după ce se termină mesajul, se oprește.

Programul tcpdump este terminat prin Ctrl-C.

student@eim-lab:/opt/android-sdk-linux/platform-tools$ ./adb -s 192.168.56.101:5555 pull /sdcard/DCIM/sip.pcap
student@eim-lab:/opt/android-sdk-linux/platform-tools$ wireshark sip.pcap

Nu aveți root pe device

Va trebui să capturați pachetele de tip UDP din mașina de dezvoltare care funcționează ca un ruter pentru device-ul virtual. Dacă folosiți un device real, și acesta va trimite pachetele VoiP/SIP prin interfeța 4G, atunci nu vor putea fi capturate în mașina de dezvoltare. Pentru pachetele trimise de pe device prin WiFi se poate face o captură numai dacă WiFi nu folosește WPA și pachetele zboară in clear (nu sunt criptate prin aer).

Linux
$ ip ro 
default via 192.168.100.1 dev enp0s31f6 proto dhcp src 192.168.100.107 metric 100 
172.17.17.0/24 dev ztrf26yfym proto kernel scope link src 172.17.17.18 
172.18.0.0/16 dev docker0 proto kernel scope link src 172.18.0.1 linkdown 
192.168.56.0/24 dev vboxnet0 proto kernel scope link src 192.168.56.1 linkdown 
192.168.100.0/24 dev enp0s31f6 proto kernel scope link src 192.168.100.107 metric 100 

interfața default a mașinii de dezvoltare este enp0s31f6, așadar aici pot fi capturate cu wireshark pachetele forwardate pentru device-ul virtual.

Windows Windows: TODO - instalare wireshark și captură fie din aer, fie dacă rulăm AVD/geny de pe interfața default
OSX OSX: TODO - utilitarul airport, instalare wireshark

Se obține dump-ul fie din dispozitivul virtual, fie din masina de dezvoltare și se analizează folosind wireshark instalat local.

  • Să se identifice operația REGISTER. Ce port se utilizează? Care este adresa serverului?

  • Să se găsească, în răspunsul de confirmare, adresele NAT prin care trece conversația, odată ce a fost acceptată cererea.

  • Să se identifice operația INVITE. Apar retransmisii?

  • Ce fel de codificare este utilizată pentru semnalul audio?

  • Ce parametri are fluxul de voce (protocol, dimensiune pachet, rata pachetelor)?

  • Ce adrese sunt folosite pentru traficul de voce și cum au fost negociate?


Note

Pornirea monitorizării (pornirea utilitarului tcpdump) trebuie realizată anterior operației de înregistrare. Similar, oprirea monitorizării trebuie realizată ulterior operației de deînregistrare. În acest fel, pot fi surprinse toate operațiile.


10. (opțional) Pentru a trimite coduri numerice DTMF (Dual Tone Multi Frequency) se creează un buton și un câmp text editabil asociat. Transmiterea unui astfel de caracter se realizează prin intermediul metodei sendDtmf() a obiectului core.currentCall cu valorile întregi 0-9, sau 10 pentru * și 11 pentru #. Folosind o adresa de test (thetestcall@sip.linphone.org sau 904@mouselike.org) să se testeze codurile și navigarea prin meniuri.

Să se implementeze metoda asociată clasei ascultător corespunzătoare operației de apăsare a butonului respectiv.

Indicații de Rezolvare
  findViewById<Button>(R.id.dtmfsend).setOnClickListener {
              val keypress = (findViewById<EditText>(R.id.dtmfedit)).text.toString()
              if(keypress.isEmpty()){
                  Toast.makeText(this@MainActivity, "Need phone key character 0-9, +, #",  Toast.LENGTH_LONG).show()
                  return@setOnClickListener
              }

              val call = if (core.currentCall != null)
                    core.currentCall
                else if( core.calls.size > 0)
                    core.calls[0]
                else null
              if(call != null)
                  call.sendDtmf(keypress[0])
  }

11. (Opțional) TODO Implementare instant messenging. Activitatea InstantMessagingActivity poate fi lansată din activitatea principală, doar ulterior operației de înregistrare. Aceasta primește ca argument, în intenția cu care este lansată în execuție, adresa SIP cu care se va desfașura sesiunea de mesagerie instantanee.

12. Să se încarce modificările realizate în cadrul depozitului 'Laborator10' de pe contul Github personal, folosind un mesaj sugestiv.

student@eim-lab:~/Laborator10$ git add * # dar ștergeți credențialele 
student@eim-lab:~/Laborator10$ git commit -m "implemented taks for laboratory 10"
student@eim-lab:~/Laborator10$ git push Laborator10_perfectstudent master

Resurse Utile

Introduction to SIP - A Beginners' Tutorial as part of Internet Multimedia
How VoIP Works?
Session Initiation Protocol (Tutorial's Point)
Session Initiation Protocol - Wikipedia
Linphone app
WebRTC

Laborator 11. Utilizarea Serviciilor de Localizare

Datele cu privire la localizare îmbunătățesc experiența utilizatorului, întrucât unele informații furnizate de aplicații pot fi contextualizate în funcție de regiunea în care acesta se găsește în mod curent. O astfel de oportunitate poate fi exploatată cu atât mai mult în cadrul dispozitivelor mobile, care dispun de componente specializate pentru determinarea automată a poziției geografice curente (senzor pentru GPS, folosirea informațiilor furnizate de celula de telefonie mobilă).

În cadrul SDK-ului Android, sunt implementate API-uri pentru proiectarea și dezvoltarea unor aplicații care pun la dispoziția utilizatorilor informații cu privire la locația în care se află, disponibile prin intermediul unor metode, fără a fi necesară interacțiunea propriu-zisă cu componentele responsabile cu determinarea acestor date. Totodată, există posibilitatea de a identifica punctele de interes care se găsesc în proximitatea utilizatorului, la un moment dat.

Există și alte API-uri pentru localizare, geoconding, hărți, indicații de navigație, fiecare cu avantajele și dezavantajele asociate. În acest laborator se vor folosi serviciile Google, care sunt cotate printre cele mai rapide, diverse, dar și scumpe pentru volum mare de apeluri.

Astfel, funcționalitățile oferite pentru dezvoltatori sunt:

  1. furnizarea de servicii integrate pentru localizare, având următoarele caracteristici: nivelul de detaliu este determinat în funcție de specificațiile utilizatorului (precizie înaltă sau consum scăzut de energie), disponibilitate imediată a celei mai recente locații disponibile, optimizarea nivelului de utilizare al bateriei, luându-se în considerare solicitările existente raportat la senzorii care pot fi utilizați, flexibilitatea în gama de servicii oferite (utilizarea în interfața grafică a aplicației, cu un nivel de detaliu ridicat sau folosirea de către servicii, cu un nivel de detaliu scăzut);
  2. oferirea unei liste cu punctele de interes din proximitate, raportat la un anumit domeniu (locații turistice, tipuri de organizații), acestea putând fi marcate cu ajutorul unor controale grafice dedicate; pentru fiecare punct de interes pot fi obținute informații suplimentare (descrieri, conținut multimedia), acestea putând fi furnizate și de utilizator, fiind stocate ulterior într-o bază de date Google; se poate determina astfel și locația curentă împreună cu alte repere din zonă; denumirile specifice ale locurilor respective precum și adresele corespunzătoare pot fi completate facil prin indicarea unor sugestii ce conțin variantele disponibile;
  3. transmiterea de notificări legate de restricția zonală (eng. geofencing), prin indicarea unor coordonate aflate în proximitate anumitor locații: pot fi gestionate mai multe arealuri geografice de acet tip concomitent, fără a avea un impact semnificativ asupra consumului de energie (actualizările cu privire la locația curentă sunt realizate în funcție de distanța față de zona marcată precum și de tipul de activitate - staționar sau în mișcare: mers, alergat, în vehicul, cu bicicleta); sunt oferite informații atât cu privire la intrarea în arealul geografic cât și cu privire la ieșirea din acesta;
  4. determinarea activității pe care utilizatorul o desfășoară în mod curent (staționar sau în mișcare: mers, alergat, în vehicul, cu bicicleta), fără un consum de baterie important (folosind senzori de putere mică); o astfel de funcționalitate este foarte utilă în contextul integrării cu aplicațiile care necesită actualizări cu privire la locația curentă, frecvența cu care sunt solicitate acest set de date fiind determinată de tipul de activitate aflat în desfășurare.

Note

Este recomandat ca informațiile legate de localizarea dispozitivului mobil să se realizeze folosind API-ul pus la dispoziție de Google Play Services, în detrimentul mecanismelor precedente pentru localizare (disponibile în pachetul android.location).


Configurare

1. În cadrul Consolei Google API, se activează API-ul Google Maps Android API, generându-se totodată și o cheie Android prin care aplicația care rulează pe dispozitivul mobil va putea să acceseze o astfel de funcționalitate.


Note

Trebuie să fiți autentificați folosind numele de utilizator și parola contului Google, altfel va trebui să vă creați un astfel de cont.

În situația în care nu a fost creat un proiect Google API în prealabil, trebuie realizat acest lucru, prin selectarea opțiunii Select a project.

Se va afișa o fereastră din care poate fi selectat proiectul dorit (împărțite în categoriile Recent, respectiv All). În situația în care nu există nici un proiect, acesta poate fi creat, prin accesarea pictogramei corespunzătoare semnului +.

Pentru fiecare proiect trebuie să se precizeze următorii parametri:

  • denumirea;
  • identificatorul pentru proiect (este generat în mod automat, însă poate fi configurat suplimentar de către utilizator).

În situația în care există un singur proiect, acesta va fi selectat în mod implicit ca proiect curent.


  • În secțiunea LibraryGoogle APIs, în categoria Google Maps APIs, se accesează opțiunea Google Maps Android API, activându-se acest serviciu (prin accesarea butonului Enable).

Acest API nu va putea fi însă utilizat în situația în care nu sunt create credențialele (o cheie pentru API), necesare pentru a putea accesa orice serviciu Google.

  • Credențialele pot fi obținute:

    • prin accesarea butonului Google Maps Android APICreate credentials, care implică următoarele etape:

      1. indicarea tipului de API folosit (în cazul de față Google Maps Android API)
      2. generarea (automată) propriu-zisă a cheii, urmată de apăsarea butonului Done;
      În secțiunea Credentials va putea fi vizualizată cheia creată, împreună cu denumirea sa. Pentru cheile care nu sunt restricționate, va fi vizibil semnul :!: care indică faptul că aceasta poate fi utilizată din orice context, ceea ce poate implica o breșă de securitate
      Prin accesarea cheii respective, aceasta poate fi restricționată în sensul în care se precizează contextul în care este folosită cheia (în cazul de față, aplicații Android - Android apps), denumirea pachetului corespunzător aplicației Android din care cheia respectivă poate fi folosită precum și certificatul digital SHA-1 corespunzător mașinii de pe care este instalată aplicația Android pe dispozitivul mobil.
      Se va indica și comanda care va trebui rulată pentru generarea certificatului digital respectiv; folosind utilitarul Java keytool, se generează semnătura digitală a mașinii de pe care se va dezvolta aplicația Android (pentru a putea utiliza acest utilitar, calea căte Java trebuie să se găsească în variabila de mediu $PATH, respectiv %PATH).
      Linux
    ```shell
    student@eg-106:~$ export PATH=$PATH:/usr/local/java/jdk1.8.0_131/bin
    student@eg-106:~$ keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
    ```
    
    Windows
    ```shell
    C:\Users\Student> set PATH=%PATH%;C:\Program Files\Java\jdk_1.8.0_131\bin
    C:\Users\Student> keytool -list -v -keystore "%USERPROFILE%\.android\debug.keystore" -alias androiddebugkey -storepass android -keypass android
    ```
    
    Vor fi furnizate mai multe tipuri de amprente digitale, pentru
    cheia publică de tip Android fiind necesară cea de tip SHA-1
    
    ```
    Alias name: androiddebugkey
    Creation date: Mar 5, 2015
    Entry type: PrivateKeyEntry
    Certificate chain length: 1
    Certificate[1]:
    Owner: CN=Android Debug, O=Android, C=US
    Issuer: CN=Android Debug, O=Android, C=US
    Serial number: 4a38a96a
    Valid from: Thu Mar 05 13:17:44 EET 2015 until: Sat Feb 25 13:17:44 EET 2045
    Certificate fingerprints:
             MD5:  FC:1F:95:45:78:ED:50:C6:EE:8E:02:0A:3D:A5:80:D3
             SHA1: C7:02:98:BB:AD:1C:6E:D1:3A:35:50:8B:88:78:B6:D3:B7:9F:66:C0
             SHA256: B3:D9:98:33:92:71:2D:CE:65:19:89:73:2A:64:3C:97:B9:37:A1:93:8C:
    50:4F:E1:13:C4:21:C7:08:94:AC:A5
             Signature algorithm name: SHA256withRSA
             Version: 3
    
    Extensions:
    
    #1: ObjectId: 2.5.29.14 Criticality=false
    SubjectKeyIdentifier [
    KeyIdentifier [
    0000: 99 78 63 24 A0 64 DF A8   67 45 8E 82 C6 8E 53 D1  .xc$.d..gE....S.
    0010: B8 C1 89 75                                        ...u
    ]
    ]
    ```
    
    • Se accesează butonul Add package name and fingerprint pentru a se specifica denumirea pachetului corespunzător aplicației Android care va accesa API-ul respectiv și certificatul SHA-1

      În secțiunea Credentials pot fi vizualizate cheile pentru API generate anterior

    • în secțiunea Credentials, prin accesarea opțiunii Create credentials din care este selectat tipul de cheie necesar (în cazul de față API key); dacă nu se cunoaște tipul de cheie necesar, se poate selecta valoarea Help me choose
      cheia respectivă va fi generată în mod automat, existând posibilitatea ca aceasta să fie restricționată, pentru a nu fi accesată din orice context

2. Pe dispozitivul mobil (fizic sau virtual) pe care se va rula aplicația care accesează serviciul de localizare, trebuie să se găsească cea mai recentă versiune de Google Play Services, asociindu-se totodată contul de utilizator Google pentru care s-a generat cheia publică.

3. Se instalează SDK-ul Google Play Services, necesar accesării serviciului de localizare prin intermediul unei aplicații Android.

  • Linux
    student@eg-106:~$ cd /opt/android-sdk-linux/tools
    student@eg-106:/opt/android-sdk-linux/tools$ sudo ./android
    
  • Windows - se deschide un terminal cu drepturi de administrator
    C:\Users\Student> cd "..\..\Program Files (x86)\Android\android-sdk\tools"
    C:\Program Files (x86)\Android\android-sdk\tools>android.bat
    

Astfel, se instalează următoarele pachete:

  • din secțiunea Android 4.1.2 (API 16), pachetul Google APIs;
  • din secțiunea Extras, pachetul Google Play Services.

Posibilitatea de instalare a acestor pachete există și din mediul integrat de dezvoltare Android Studio, prin accesarea opțiunii ToolsAndroidSDK Manager, secțiunea SDK Tools.

Hidden Biblioteca pentru accesarea funcționalității oferite de serviciul de localizare se găsește la `/extras/google/google_play_services/libproject/google-play-services_lib`.

În mediul integrat de dezvoltare Eclipse, se realizează o referință către biblioteca Google Play Services, astfel descărcată.

  • se accesează FileImportAndroidExisting Android Code Into Workspace

  • se indică locația unde se găsește instalată biblioteca Google Play Services, creându-se o copie a acestuia în spațiul de lucru (se bifează opțiunea Copy projects into workspace)

  • API key 2024 AIzaSyDTihXRHSZDmzDF5hP7VkmPXOzejoil8nU

4. În mediul integrat de dezvoltare Android Studio, se creează un proiect corespunzător unei aplicații Android, având următoarele proprietăți:

  • denumirea pachetului care identifică aplicația Android în mod unic trebuie să fie aceeași cu cea precizată în momentul în care a fost generată cheia publică;
  • în fișierul build.gradle să se specifice dependința către biblioteca Google Play Services (com.google.android.gms:play-services), în secțiunea dependencies:

    dependencies {
      ...
      compile 'com.google.android.gms:play-services:10.2.4'
    }
    
  • în fișierul AndroidManifest.xml

    • se indică permisiunile necesare:
      <uses-permission
        android:name="android.permission.ACCESS_COARSE_LOCATION" />
      <uses-permission
        android:name="android.permission.ACCESS_FINE_LOCATION" />    
      <uses-permission
        android:name="android.permission.ACCESS_NETWORK_STATE" />
      <uses-permission
        android:name="android.permission.INTERNET" />
      <uses-permission
        android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />
      <uses-permission
        android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
      
      • android.permission.ACCESS_COARSE_LOCATION - obține locația utilizatorului folosind informațiile preluate prin rețele fără fir și datele corespunzătoare celulei în care se găsește dispozitivul mobil;
      • android.permission.ACCESS_FINE_LOCATION - procură locația utilizatorului prin intermediul coordonatelor obținute de la sistemul de poziționare (GPS - eng. Global Positioning System);
      • android.permission.ACCESS_NETWORK_STATE - verifică starea conectivității în rețea, astfel încât să se determine dacă este posibil ca datele să fie descărcate sau nu;
      • android.permission.INTERNET - determină starea conectivității la Internet;
      • com.google.android.providers.gsf.permission.READ_GSERVICES - preia informațiile puse la dispoziție prin intermediul Google Play Services;
      • android.permission.WRITE_EXTERNAL_STORAGE - utilizează un spațiu de stocare pentru informațiile legate de hărți;
    • pentru redarea hărților, precum și pentru operațiile de tip zoom, este necesară folosirea bibliotecii OpenGL
        <uses-feature
        android:glEsVersion="0x00020000"
        android:required="true" /> 
      
    • în secțiunea <application> ... </application> se indică:
      • cheia publică utilizată pentru accesarea funcționalității legată de serviciile de localizare
          <metadata
          android:name="com.google.android.maps.v2.API_KEY"
          android:value="AIzaSyARiJhQ-Lj6HnzQwq7MqAvjWQtNkjVcprs" />
        
      • versiunea folosită pentru biblioteca Google Play Services (preluată din cadrul proiectului referit)
          <metadata
          android:name="com.google.android.gms.version"
          android:value="@integer/google_play_services_version" />
        
    • pentru a se asigura faptul că funcționalitatea nu va putea fi accesată decât prin intermediul aplicației Android, se va defini o permisiune, definindu-se o protecție la nivel de semnătură:
      <permission
        android:name="ro.pub.cs.systems.eim.lab10.googlemaps.permission.MAPS_RECEIVE"
        android:protectionLevel="signature" />
      <uses-permission
        android:name="ro.pub.cs.systems.eim.lab10.googlemaps.permission.MAPS_RECEIVE" />
      
  • se precizează regulile pentru obfuscatorul Proguard (în fișierul proguard-rules.pro din directorul app), astfel încât acesta să nu elimine clasele necesare:
    -keep class * extends java.util.ListResourceBundle {
      protected Object[][] getContents();
    }
    -keep public class com.google.android.gms.common.internal.safeparcel.SafeParcelable {
      public static final *** NULL;
    }
    -keepnames @com.google.android.gms.common.annotation.KeepName class *
    -keepclassmembernames class * {
      @com.google.android.gms.common.annotation.KeepName *;
    }
    -keepnames class * implements android.os.Parcelable {
      public static final ** CREATOR;
    }
    

Dispozitiv Fizic

Pentru accesarea funcționalităților legate de locație pe dispozitivul fizic este necesar să se activeze opțiunea Location din secțiunea de configurări (SettingsPersonal).

Valoarea configurației Location trebuie să aibă valoarea On, pentru ca serviciile de localizare să poată fi utilizate. De asemenea, sunt indicate aplicațiile Android care au folosit serviciile de localizare.

Se poate controla acuratețea informațiilor furnizate, raportat la consumul de energie, prin intermediul opțiunilor disponibile în secțiunea Location Mode din secțiunea de configurări (SettingsPersonalLocationMode).

  • High accuracy - locația este determinată folosind toate resursele disponibile (sistemul global de poziționare GPS, rețelele mobile și fără fir);
  • Battery saving - locația este determinată folosind doar informațiile furnizate de rețelele mobile și fără fir;
  • Device only - locația este determinată folosind doar informațiile furnizate de sistemul global de poziționare GPS.


Note

Se recomandă ca determinarea locației să se realizeze folosind toate resursele disponibile la un moment dat de timp pentru o precizie cât mai mare (cu un consum de energie corespunzător).


Dispozitiv Virtual

Genymotion

Pentru accesarea funcționalităților legate de locație pe dispozitivul virtual Genymotion este necesar să se activeze serviciul GPS, accesibil din meniul lateral sau folosind combinația de taste Ctrl + 2 (se selectează valoarea On).

Alte informații care pot fi configurate sunt:

  • latitudinea - exprimată în grade;
  • longitudinea - exprimată în grade;
  • altitudinea - exprimată în metri;
  • acuratețea (nivelul de precizie) - exprimat în număr de metri cu care să se aproximeze locația exactă;
  • direcția - exprimată în grade.

De asemenea, este implementată și funcționalitatea prin intermediul căreia poate fi vizualizată poziția precizată în cadrul unei hărți Google.

AVD

Pentru accesarea funcționalităților legate de locație pe dispozitivul virtual AVD este necesar ca în secțiunea de configurări corespunzătoare (SettingsLocation access) să se specifice următorii parametri:

  • Access to my location: On - permite aplicațiilor Android să acceseze locația curentă, pe baza informațiilor furnizate din sursele disponibile;
  • GPS Sattelites: activat - se utilizează sistemul de poziționare globală GPS pentru determinarea locației curente;
  • Wi-Fi & mobile network location: activat - se utilizează o estimare pentru locația curentă pe baza serviciilor oferite de Google; totodată, vor fi transmise informații anonime în acest sens.

Controlul poziției curente poate fi realizat prin intermediul perspectivei Android Debug Monitor din Android Studio, unde, în secțiunea Emulator ControlLocation Control se stabilesc valorile pentru latitudine și longitudine (în panoul Manual, în format decimal sau sexagesimal), după care se apasă butonul Send. Informații cu privire la locațiile disponibile pot fi precizate și sub forma unor fișiere gpx sau kml, care pot fi încărcate.

Gestiunea unei Hărți Google

Harta Google este implementată în SDK-ul Android:

  • prin intermediul unei componente grafice de tipul MapView, în acest caz fiind necesar ca metodele care controlează ciclul de viață al aplicației Android să propage evenimentele corespunzătoare și către acest element;
  • în cadrul unui fragment, de tipul MapFragment, acesta tratând și evenimentele corespunzătoare ciclului de viață al aplicației Android.

Note

Obiectele MapView șiMapFragment sunt disponibile începând cu nivelul de API 12, asigurarea compatibilității cu versiunile anterioare fiind realizată prin intermediul bibliotecilor de suport.


Astfel, integrarea unei hărți Google se poate implementa prin specificarea resursei aferente în fișierul XML care descrie interfața grafică.

<fragment
  android:id="@+id/google_map"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  class="com.google.android.gms.maps.MapFragment" />

Pe baza controalelor grafice MapView sau MapFragment, se poate obține o instanță a unui obiect GoogleMap, prin intermediul căruia sunt invocate toate funcționalitățile pentru operații legate de localizarea pe hartă.

De regulă, inițializarea este realizată pe una dintre metodele onStart() sau onResume(), după ce în prealabil au fost încărcate toate controalele grafice pentru interacțiunea cu utilizatorul.


Note

Se recomandă să se folosească metoda asincronă getMapAsync(OnMapReadyCallback) care garantează faptul că obiectul furnizat este nenul. Metoda onMapReady() a clasei ascultător 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.

Orice operație care implică un obiect de tipul GoogleMap trebuie realizată pe firul de execuție al interfeței grafice (principal), în caz contrar generându-se o excepție.


GoogleMap googleMap = null;

* ...

if (googleMap == null) {
  ((MapFragment)getFragmentManager().findFragmentById(R.id.google_map)).getMapAsync(new OnMapReadyCallback() {
    @Override
    public void onMapReady(GoogleMap readyGoogleMap) {
      googleMap = readyGoogleMap;
    }
  });
}

Funcționalitățile pe care le pune la dispoziție un obiect de tipul GoogleMap sunt:

1. gestiunea reperelor de pe harta Google, prin intermediul elementelor MarkerOptions, pentru care se pot preciza următoarele informații:

  • coordonatele GPS, sub forma unui obiect LatLng (care încapsulează informații precum latitudinea și longitudinea, de tip double), prin metoda position(LatLng)
    marker.position(new LatLng(
      Double.parseDouble(latitudeContent), 
      Double.parseDouble(longitudeContent)
      )
    );
    `
    
  • titlul, prin metoda title(String); marker.title(nameContent);
  • pictograma, prin metoda icon(BitmapDescriptor), putând fi obținută prin intermediul clasei BitmapDescriptorFactory:
    • în formatul standard, disponibil în mai multe culori (metoda defaultMarker(float)); marker.icon(BitmapDescriptorFactory.defaultMarker(Utilities.getDefaultMarker(markerTypeSpinner.getSelectedItemPosition())));
    • într-un format definit de utilizator, în funcție de resursele disponibile în cadrul aplicației Android:
  • descrierea, prin metoda snippet(String); marker.snippet(descriptionContent);
  • vizibilitatea, prin metoda visible(boolean). marker.visible(true);

Un astfel de obiect este atașat unei hărți Google prin intermediul metodei addMarker(MarkerOptions). googleMap.addMarker(marker);

Alte elemente grafice care pot fi vizualizate sunt:

Toate aceste controale pot fi înlăturate în momentul în care este folosită metoda clear().

2. poziționarea la anumite coordonate GPS (latitudine, longitudine) este realizată prin actualizarea locației la care se găsește camera prin care este vizualizată harta Google, funcționalitate implementată de clasa CameraUpdate; un astfel de obiect este obținut de regulă prin metoda statică newCameraPosition(CameraPosition) din clasa fabrică CameraUpdateFactory, prin care pot fi controlate și alte proprietăți (nivelul de detaliere, vizualizarea unui anumit areal geografic); metoda animateCamera(CameraUpdate) realizează transferul dintre coordonatele vechi și coordonatele noi prin intermediul unei animații.

CameraPosition cameraPosition = new CameraPosition.Builder().target(new LatLng(latitude, longitude))
  .build();
googleMap.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition)); 

Metoda animateCamera() este supraîncărcată, astfel încât să se poată preciza:

  • durata propriu-zisă a animației (exprimată în milisecunde);
  • un obiect ascultător (de tipul GoogleMap.CancelableCallback) care indică momentul în care animația a fost terminată (onFinish()) sau a fost întreruptă (onCancel()).

În situația în care este în desfășurare o animație, aceasta poate fi oprită printr-un apel al metodei stopAnimation().

3. marcarea locației curente este realizată prin intermediul metodei setMyLocationEnabled(boolean); de asemenea, este disponibil un control prin intermediul căruia utilizatorul poate activa sau dezactiva această funcționalitate;

4. vizualizarea unor informații suplimentare:

Tipurile de hărți Google implementate sunt:

  • GoogleMap.MAP_TYPE_NORMAL - harta politică;
  • GoogleMap.MAP_TYPE_TERRAIN - harta fizică (nu include și drumuri);
  • GoogleMap.MAP_TYPE_SATELLITE - vedere din satelit;
  • GoogleMap.MAP_TYPE_HYBRID - combinație hibridă.

Specificarea unui tip de hartă se realizează prin intermediul metodei setMapType().

Gestiunea unei hărți Google sub formă de imagine este realizată prin intermediul metodei snapshot(GoogleMap.SnapshotReadyCallback, al cărui obiect ascultător furnizează resursa grafică (în format Bitmap) în momentul în care este disponibilă (se apelează automat metoda onSnapshotReady(Bitmap).

Funcționalitatea pe care o oferă harta Google utilizatorului poate fi controlată și prin intermediul obiectului asociat de tip UiSettings, obținut prin apelul metodei getUiSettings():

Pentru interacțiunea cu utilizatorul au fost definite mai multe clase ascultător, ale căror metode semnalează declanșarea unor evenimente specifice:

Gestiunea Locației Curente printr-un Client Google API

API-ul Android pune la dispoziția utilizatorilor un furnizor integrat de servicii de localizare, prin care aceștia pot specifica anumiți parametrii de configurare, cum ar fi nivelul de precizie și gradul de utilizare al bateriei.

Funcționalitatea legată de gestiunea locației curente (ca de altfel toate funcționalitățile legate de biblioteca Google Play Services) este disponibilă prin intermediul unui obiect de tip GoogleApiClient, a cărui instanță este obținută de regulă pe metoda onCreate() a aplicației Android, eliberarea resurselor corespunzătoare acesteia fiind făcută pe metoda onDestroy().

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);

  * ...

  googleApiClient = new GoogleApiClient.Builder(this)
    .addConnectionCallbacks(this)
    .addOnConnectionFailedListener(this)
    .addApi(LocationServices.API)
    .build();
}

@Override
protected void onDestroy() {

  * ...

  googleApiClient = null;
  super.onDestroy();
}

Pentru clientul Google API, s-a indicat funcționalitatea pentru care va fi accesat (LocationServices.API) prin intermediul metodei addApi(), precum și clasele ascultător pentru evenimentele care pot fi generate legat de acesta (metodele corespunzătoare fiind asincrone):

  • GoogleApiClient.ConnectionCallbacks gestionează operațiile de tip conectare / deconectare la serviciu, metodele implementate fiind:
    • onConnected(Bundle) - apelată în momentul în care clientul Google API s-a conectat cu succes la funcționalitatea dorită;
    • onConnectionSuspended(int) - apelată în momentul în care clientul Google API este deconectat (temporar) de la funcționalitatea solicitată, indicându-se și motivul care a generat un astfel de comportament:
      • CAUSE_NETWORK_LOST - deconectare de la Internet;
      • CAUSE_SERVICE_DISCONNECTED - oprirea serviciului corespunzător.
  • GoogleApiClient.OnConnectionFailedListener controlează situațiile în care nu este posibilă realizarea unei legături, metoda aferentă, onConnectionFailed(ConnectionResult), specificând rezultatul ce conține codul de eroare.

Note

Este recomandat ca metodele ascultător care controlează starea conexiunii clientului Google API la serviciul de localizare să fie implementate în aceeași clasă cu activitatea principală, aceasta implementând interfețele necesare.


Operațiile de conectare / deconectare a clientului Google API la serviciu trebuie realizate în contextul metodelor care controlează ciclul de viață al aplicației Android, astfel:

  • metoda connect() se apelează în cadrul metodei onStart();
  • metoda disconnect() se apelează în cadrul metodei onStop().
@Override
protected void onStart() {
  super.onStart();
  googleApiClient.connect();
  
  * ...

}

@Override
protected void onStop() {

  * ...

  if (googleApiClient != null && googleApiClient.isConnected()) {
    googleApiClient.disconnect();
  }
  super.onStop();
}

Momentul în care sunt disponibile informațiile cu privire la cea mai recentă locație a dispozitivului mobil este determinat de conectarea cu succes a clientului Google API, motiv pentru care se obișnuiește ca aceste date să fie interogate pe metoda onConnected(Bundle) a intefeței GoogleApiClient.ConnectionCallbacks, apelată în mod automat la producerea evenimentului respectiv. În acest sens, se apelează metoda getLastLocation(GoogleApiClient) din clasa FusedLocationProviderApi.

<note>Se furnizează o locație al cărui grad de precizie este determinat în funcție de permisiunile pe care le deține aplicația Android.


@Override
public void onConnected(Bundle connectionHint) {
  Log.i(Constants.TAG, "onConnected() callback method has been invoked");
  lastLocation = LocationServices.FusedLocationApi.getLastLocation(googleApiClient);
  
  * ...

}

Informațiile referitoare la locația curentă sunt descrise sub forma unui obiect Location, care conține mai multe informații, precum:

  • acuratețea (exprimată în metri);
  • altitudinea (exprimată în metri, având drept referință elipsoidul WGS 84);
  • palierul (exprimat în grade);
  • latitudine și longitudine (exprimată în grade);
  • furnizor;
  • viteză (exprimată în m/s);
  • momentul de timp (exprimat în milisecunde, raportat la 1 ianuarie 1970).

De asemenea, pentru acest tip de obiect pot fi asociate informații suplimentare, sub forma unui Bundle, în câmpul extras.

Totodată, pot fi obținute actualizări periodice cu privire la locația curentă, pe baza furnizorilor disponibili (transfer de date în rețeaua GSM / fără fir, sistemul global de poziționare GPS) funcționalitate utilă în momentul în care se dorește să se identifice activitatea pe care o desfășoară utilizatorul pentru a contextualiza conținutul oferit în funcție de aceasta.

Acuratețea datelor generate poate fi controlată și prin intermediul configurațiilor conținute în solicitarea corespunzătoare, exprimată prin intermediul unui obiect de tip LocationRequest:

  • setExpiration(long) - durata solicitării (exprimată în milisecunde), după care nu mai sunt furnizate actualizări cu privire la locația curentă (momentul de timp la care sunt raportate este cel în care este apelată metoda, nu cel în care este realizată solicitarea propriu-zisă);
  • setExpirationTime(long) - momentul de timp după care nu mai sunt furnizate actualizări cu privire la locația curentă, exprimat în milisecunde raportat la perioada în care dispozitivul mobil a fost pornit;
  • setInterval(long) - indică intervalul de timp (exprimat în milisecunde) la care se dorește să se primească actualizările cu privire la locația curentă; acesta poate fi însă:
    • mai mic, dacă dispozitivul mobil are probleme legate de conectivitate;
    • mai mare, dacă există alte aplicații care au stabilit alte rate de transfer.
  • setFastestInterval(long) - precizează intervalul la care aplicația Android poate să gestioneze actualizările cu privire la locația curentă, în situația în care există și alte aplicații care au specificat alte rate de transfer, pentru a preîntâmpina situații cum ar fi imposibilitatea de actualizare corespunzătoare a interfeței grafice sau lipsa de spațiu de stocare disponibil pentru informațiile respective;

Note

Frecvența cu care sunt transmise actualizările periodice cu privire la modificarea locației curentă este dată de cea mai rapidă rată de transfer specificată de toate aplicațiile care accesează o astfel de funcționalitate.



Note

În situația în care aplicația Android realizează procesări complexe, care implică o perioadă de timp considerabilă, este recomandat ca valoarea transmisă metodei setFastestInterval() să fie corespunzătoare, astfel încât să nu fie furnizate valori care nu pot fi utilizate.


  • setMaxWaitTime(long) - specifică perioada de așteptare maximă pentru transmiterea actualizărilor periodice referitoare la locația curentă; astfel, mai multe informații de acest tip pot fi livrate împreună, cu frecvența indicată de acest interval, optimizând consumul de baterie (actualizările periodice sunt primite la intervale de maxWaitTime, numărul de seturi de date fiind maxWaitTime / interval);
  • setNumUpdates(int) - determină numărul de actualizări necesare, în caz contrar fiind furnizate valori de acest tip continuu, între apelurile metodelor requestLocationUpdates() și removeLocationUpdates();
  • setPriority() - stabilește prioritatea solicitării, oferind informații cu privire la furnizorul de servicii care va fi utilizat:
    • PRIORITY_BALANCED_POWER_ACCURACY - gradul de acuratețe este relativ (aproximativ 100 de metri), consumul de energie fiind moderat;
    • PRIORITY_HIGH_ACCURACY - gradul de acuratețe este ridicat (aproximativ 10 metri), pe baza informațiilor provenite de la sistemul global de poziționare (GPS), cu un consum mare de energie;
    • PRIORITY_LOW_POWER - gradul de acuratețe este scăzut (aproximativ 10 kilometri), sursele folosite fiind rețeaua mobilă / fără fir, cu un consum mic de energie;
    • PRIORITY_NO_POWER - utilizat atunci când se dorește transmiterea de actualizări periodice cu privire la locația curentă, fără un impact semnificativ asupra consumului de energie (de regulă, astfel de informații sunt preluate de la alte aplicații);
  • setSmallestDisplacement(int) - indică distanța minimă dintre locații pentru care se primește actualizare periodică (implicit, are valoarea 0).

De regulă, instanțierea unui obiect de tip LocationRequest precum și precizarea parametrilor ce caracterizează solicitările cu privire la actualizările periodice sunt realizate o singură dată, în momentul în care aplicația Android este pornită (pe metoda onCreate()), urmând ca eliberarea resurselor să fie realizată atunci când aplicația Android este oprită (pe metoda onDestroy()).

locationRequest = new LocationRequest();
locationRequest.setInterval(Constants.LOCATION_REQUEST_INTERVAL);
locationRequest.setFastestInterval(Constants.LOCATION_REQUEST_FASTEST_INTERVAL);
locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);

O aplicație Android primește actualizări periodice cu privire la locația curentă între momentele de timp la care specifică explicit acest lucru, prin apelul metodelor corespunzătoare:

Întrucât aceste metode primesc un parametru de tip GoogleApiClient, este necesar ca acesta să fie nenul și să fie conectat. Din acest motiv, o practică curentă vizează realizarea suplimentară a acestor verificări anterior invocării lor propriu-zise.

protected void startLocationUpdates() {
  LocationServices.FusedLocationApi.requestLocationUpdates(
    googleApiClient,
    locationRequest,
    this
  );
  locationUpdatesStatus = true;
  googleMap.setMyLocationEnabled(true);
  if (lastLocation != null) {
    navigateToLocation(lastLocation);
  }
  
  * ...
  
}
    
protected void stopLocationUpdates() {
  LocationServices.FusedLocationApi.removeLocationUpdates(
    googleApiClient,
    this
  );
  locationUpdatesStatus = false;
  googleMap.setMyLocationEnabled(false);

  * ...

}

Pentru ca impactul asupra consumului de energie să fie optim, se recomandă ca pe metodele care controlează ciclul de viață al unei aplicații Android să se gestioneze corespunzător starea transmiterii de actualizări periodice, în intervalele de timp în care aceasta nu este activă / vizibilă.

@Override
protected void onStart() {
  super.onStart();

  * ...
  
  if (googleApiClient != null && googleApiClient.isConnected() && locationUpdatesStatus) {
    startLocationUpdates();
  }
}
    
@Override
protected void onStop() {
  stopLocationUpdates();
        
  * ...
  
  super.onStop();
}

Metodele care gestionează starea transmiterii de actualizări periodice cu privire la locația curentă primesc ca parametru și un obiect ascultător de tip LocationListener care notifică utilizatorul în momentul în care sunt disponibile informațiile propriu-zise: metoda onLocationChanged(Location), apelată în mod automat, oferă informații cu privire la poziția din momentul de timp respectiv.

@Override
public void onLocationChanged(Location location) {
  lastLocation = location;
  navigateToLocation(lastLocation);
}

Aplicația Android trebuie să aibă un comportament consistent în situația în care se produc modificări de configurație, astfel încât diferiții parametrii trebuie salvați și încărcați pe metodele corespunzătoare (onSaveInstanceState(Bundle), respectiv onRestoreInstanceState(Bundle)):

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
  saveValues(savedInstanceState);
  super.onSaveInstanceState(savedInstanceState);
}
    
protected void saveValues(Bundle state) {
  state.putBoolean(Constants.LOCATION_UPDATES_STATUS, locationUpdatesStatus);
  state.putParcelable(Constants.LAST_LOCATION, lastLocation);
}
    
@Override
public void onRestoreInstanceState(Bundle savedInstanceState) {
  super.onRestoreInstanceState(savedInstanceState);
  restoreValues(savedInstanceState);
}
    
protected void restoreValues(Bundle state) {
  if (state.keySet().contains(Constants.LAST_LOCATION)) {
    lastLocation = state.getParcelable(Constants.LAST_LOCATION);
  }
  if (state.keySet().contains(Constants.LOCATION_UPDATES_STATUS)) {
    locationUpdatesStatus = state.getBoolean(Constants.LOCATION_UPDATES_STATUS);
  }
}

Informațiile de interes sunt starea referitoare la transmiterea actualizărilor periodice, respectiv la cea mai recentă locație.

Codificare Geografică Inversă (Geocoding), opțional

Acest exercițiu este opțional, cere activarea facturării (Billing) în contul google pentru care activati Geocoding API. <spoiler> În Android, clasa Geocoder permite realizarea de conversii dintre coordonate GPS (latitudine / longitudine) și adresa poștală, operație denumită codificare geografică inversă.

Metodele getFromLocation() / getFromLocationName(), disponibile în mai multe forme, furnizează o listă de obiecte de tip Address, care încapsulează, pe lângă datele propriu-zise, dispuse sub formă de rânduri distincte și alte informații precum latitudine, longitudine, localitate, cod poștal, telefon, URL, împrejurimi:


Note

Toate aceste metode primesc un parametru care indică numărul de rezultate care se doresc a fi furnizate. De regulă, acesta are valoarea 1.


Metodele care realizează codificarea geografică inversă sunt sincrone, iar procesările pe care le realizează pot să dureze un interval de timp considerabil. Din acest motiv, este recomandat ca invocarea acestora să nu se facă pe firul de execuție al interfeței grafice (principal) întrucât poate afecta experiența utilizatorului, ci pe un fir de execuție care rulează în fundal, de tip IntentService (utilizarea clasei AsyncTask nu este indicată în această situație întrucât comportamentul său în cazul producerii unor întreruperi nu corespunde funcționalității dorite). Un astfel de obiect pornește în momentul în care este necesar să se realizeze o operație (fiind invocat prin intermediul unei intenții, la care pot fi atașate informații suplimentare), realizată pe un fir de execuție dedicat, fiind oprit în momentul în care nu mai este necesar să realizeze alte procesări.

Acest serviciu asociat unei intenții trebuie să fie specificat în fișierul AndroidManifest.xml, în secțiunea <application> ... </application>:

<manifest ...>
  <!-- other elements -->
  <application ...>
    <!-- other elements -->
    <service
      android:name=".service.GetLocationAddressIntentService"
      android:exported="false"/>
   </application>
</manifest>

Nu este necesar să se specifice și un filtru de intenții, de vreme ce serviciul va fi lansat în execuție explicit, prin transmiterea în cadrul intenției corespunzătoare a denumirii clasei care îl implementează.

Obiectele care vor fi transmise serviciului prin intermediul intenției sunt:

  • un obiect de tip ResultReceiver, prin intermediul căruia este furnizat rezultatul, atunci când acesta este disponibil; `private class AddressResultReceiver extends ResultReceiver { public AddressResultReceiver(Handler handler) { super(handler); }

    @Override protected void onReceiveResult(int resultCode, Bundle bundle) { String address = bundle.getString(Constants.RESULT); addressTextView.setText(address);

    switch(resultCode) {
      case Constants.RESULT_SUCCESS:
        Toast.makeText(GoogleMapsActivity.this, "An address was found", Toast.LENGTH_SHORT).show();
    break;
      case Constants.RESULT_FAILURE:
        Toast.makeText(GoogleMapsActivity.this, "An address was not found", Toast.LENGTH_SHORT).show();
    break;                  
    }
    
    getAddressLocationStatus = false;
    getLocationAddressButton.setEnabled(true);
    

    } } Se observă că atunci când rezultatul este disponibil, se apelează în mod automat metodaonReceiveResult()care primește ca parametrii un cod de rezultat numeric (succes sau eșec, definit de utilizator) și un obiect de tipBundle` în care sunt plasate informații suplimentare (adresa propriu-zisă sau mesajul de eroare - în funcție de rezultat -, vizualizată într-un control grafic).

  • locația care se dorește a fi rezolvată (obiect de tip Location, incluzând informații de tip latitudine și longitudine).

Mecanismul prin care se pornește un serviciu prin intermediul unei intenții este similar cu cel prin care se pornește o activitate prin intermediul unei intenții:

  1. se instanțiază un obiect de tip Intent specificând clasa corespunzătoare serviciului care se dorește a fi lansat în execuție;
  2. se plasează informațiile în obiectul Bundle disponibil în secțiunea extra.
Intent intent = new Intent(this, GetLocationAddressIntentService.class);
intent.putExtra(Constants.RESULT_RECEIVER, addressResultReceiver);
intent.putExtra(Constants.LOCATION, lastLocation);
startService(intent);

Procesarea pe care o realizează serviciul lansat în execuție prin intermediul unei intenții este plasată în cadrul metodei onHandleIntent(Intent), apelată în mod automat.

@Override
protected void onHandleIntent(Intent intent) {
  String errorMessage = null;
        
  resultReceiver = intent.getParcelableExtra(Constants.RESULT_RECEIVER);
  if (resultReceiver == null) {
    errorMessage = "No result receiver was provided to handle the information";
    Log.e(Constants.TAG, "An exception has occurred: " + errorMessage);
    return;
  }
        
  Location location = intent.getParcelableExtra(Constants.LOCATION);
  if (location == null) {
    errorMessage = "No location data was provided";
    Log.e(Constants.TAG, "An exception has occurred: " + errorMessage);
    handleResult(Constants.RESULT_FAILURE, errorMessage);
    return;
  }
        
  Geocoder geocoder = new Geocoder(this, Locale.getDefault());
        
  List<Address> addressList = null;
        
  try {
    addressList = geocoder.getFromLocation(
      location.getLatitude(),
      location.getLongitude(),
      Constants.NUMBER_OF_ADDRESSES);
  } catch (IOException ioException) {
    errorMessage = "The background geocoding service is not available";
    Log.e(Constants.TAG, "An exception has occurred: " + ioException.getMessage());
  } catch (IllegalArgumentException illegalArgumentException) {
    errorMessage = "The latitude / longitude values that were provided are invalid " + location.getLatitude() + " / " + location.getLongitude();
    Log.e(Constants.TAG, "An exception has occurred: " + illegalArgumentException.getMessage());
  }

  if (errorMessage != null && !errorMessage.isEmpty()) {
    handleResult(Constants.RESULT_FAILURE, errorMessage);
    return;
  }

  if (addressList == null || addressList.isEmpty()) {
    errorMessage = "The geocoder could not find an address for the given latitude / longitude";
    Log.e(Constants.TAG, "An exception has occurred: " + errorMessage);
    handleResult(Constants.RESULT_FAILURE, errorMessage);
    return;
  }
        
  StringBuffer result = new StringBuffer();
        
  for (Address address: addressList) {
    for (int k = 0; k < address.getMaxAddressLineIndex(); k++) {
      result.append(address.getAddressLine(k) + System.getProperty("line.separator"));
    }
    result.append(System.getProperty("line.separator"));
  }
  handleResult(Constants.RESULT_SUCCESS, result.toString());
}

Operațiile realizate de serviciu, pe firul de execuție separat, sunt:

  1. preluarea informațiilor necesare (obiect de tip ResultReceiver, locația care se dorește a fi rezolvată) printr-un obiect Bundle, din cadrul secțiunii extra a intenției prin care a fost invocat; în situația în care acestea nu pot fi obținute, se generează un mesaj de eroare;
  2. instanțierea unui obiect de tip Geocoder, folosind contextul (serviciul) și un obiect Locale, care conține informații cu privire la modul de prezentare a unor informații în funcție de zona geografică;
  3. invocarea metodelor getFromLocation() / getFromLocationName() (furnizând informațiile necesare ca parametri) și gestionând corespunzător tipurile de eroare ce pot fi generate:
    1. serviciul Geocoding nu este disponibil (se aruncă o excepție de tip IOException);
    2. informațiile cu privire la coordonatele GPS (latitudine / longitudine) nu sunt corecte (se aruncă o excepție de tipul IllegalArgumentException);
  4. se semnalează situația în care nu a putut fi identificată nici o adresă poștală asociată datelor specificate;
  5. pentru fiecare adresă poștală furnizată, se concatenează rândurile distincte:
    1. numărul de rânduri conținute este întors de metoda getMaxAddressLineIndex();
    2. un rând de la o anumită poziție este dat de metoda getAddressLine(int);
  6. se transmite codul de rezultat (numeric - succes sau eșec) precum și rezultatul propriu-zis (adresa sau mesajul de eroare) către obiectul de tip ResultReceiver, prin intermediul metodei send(int, Bundle) care face ca la nivelul acestui obiect să se apeleze în mod automat metoda onReceiveResult(); informațiile vor fi plasate în cadrul unui obiect Bundle, ca pereche (cheie, valoare): private void handleResult(int resultCode, String message) { Bundle bundle = new Bundle(); bundle.putString(Constants.RESULT, message); resultReceiver.send(resultCode, bundle); }

</spoiler>

Implementarea Zonelor de Restricție Geografică (Geofencing)

În Android, în contextul transmiterii de actualizări periodice cu privire la locația curentă, există posibilitatea ca un utilizator să fie notificat cu privire la acțiunile legate de o anumită zonă de restricție geografică, definită ca arie circulară, caracterizată printr-un centru, dat de coordonate GPS (latitudine / longitudine) și de o rază.

Este impusă restricția de a gestiona simultan maxim 100 de zone de restricție geografică active la un moment dat (fiecare restricție geografică are o perioadă de valabilitate).

Evenimentele generate în legătură cu o zonă de restricție geografică sunt legate de intrare, respectiv de ieșirea utilizatorului din acest spațiu, însă notificările pot fi temporizate o anumită perioadă. Acestea trebuie procesate pe un fir de execuție dedicat, a cărui execuție trebuie să fie limitată la procesările legate de un anumit tip de eceniment. În acest sens, va fi utilizat un obiect de tip IntentService, instanțiat în momentul în care aplicația Android este creată (pe metoda onCreate()), resursele aferente fiind eliberate în momentul în care aplicația Android este distrusă (pe metoda onDestroy()). Acesta va fi reutilizat atât pentru operația de tip adăugare cât și pentru operația de tip ștergere a unei zone de restricție geografică.

Intent intent = new Intent(this, GeofenceTrackerIntentService.class);
geofenceTrackerPendingIntent = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

Disponibilitatea unui serviciu care să realizeze procesări legate de zonele de restricție geografică trebuie menționată în cadrul fișierului AndroidManifest.xml:

<manifest ...>
  <!-- other elements -->
  <application ...>
    <!-- other elements -->
    <service
      android:name=".service.GeofenceTrackerIntentService"
      android:exported="false"/>
   </application>
</manifest>

Metoda onHandleIntent(Intent) a serviciului lansat în execuție prin intermediul unei intenții va procesa evenimentul legat de zona de restricție geografică (GeofencingEvent) care îi este transmis:

  1. se verifică situația în care evenimentul legat de zona de restricție geografică conține o eroare (informație furnizată de metoda hasError()), aceasta fiind procesată în mod corespunzător (codul de eroare este furnizat de metoda getErrorCode():
    1. GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE - serviciul de restricționare geografică nu este disponibil;
    2. GEOFENCE_TOO_MANY_GEOFENCES - au fost definite mai multe zone de restricție geografică (restricționate la maxim 100);
    3. GEOFENCE_TOO_MANY_PENDING_INTENTS - există mai multe servicii care procesează evenimente legate de restricționarea geografică;
    4. alt tip de eroare.
  2. este obținut tipul de tranziție, prin intermediul metodei getGeofenceTransition()
    1. Geofence.GEOFENCE_TRANSITION_ENTER - utilizatorul a intrat în zona de restricție geografică;
    2. Geofence.GEOFENCE_TRANSITION_EXIT - utilizatorul a ieșit din zona de restricție geografică.
  3. se obține lista cu zonele de restricție geografică care au determinat declanșarea evenimentelor respective, prin apelarea metodei getTriggeringGeofences(); pentru fiecare obiect Geofence implicat, se obține identificatorul (generat de regulă aleator), concatenându-se la mesajul ce conține detaliile tranziției;
  4. este creată o notificare, la accesarea căreia se va lansa o activitate Android în care va putea fi vizualizat mesajul cu detaliile tranziției.
@Override
protected void onHandleIntent(Intent intent) {
  GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent);
  if (geofencingEvent.hasError()) {
    String errorMessage = null;
    switch(geofencingEvent.getErrorCode()) {
      case GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE:
        errorMessage = Constants.GEOFENCE_NOT_AVAILABLE_ERROR;
        break;
      case GeofenceStatusCodes.GEOFENCE_TOO_MANY_GEOFENCES:
    errorMessage = Constants.GEOFENCE_TOO_MANY_GEOFENCES_ERROR;
        break;
      case GeofenceStatusCodes.GEOFENCE_TOO_MANY_PENDING_INTENTS:
    errorMessage = Constants.GEOFENCE_TOO_MANY_PENDING_INTENTS_ERROR;
        break;
      default:
    errorMessage = Constants.GEOFENCE_UNKNOWN_ERROR;
        break;
    }
    Log.e(Constants.TAG, "An exception has occurred: " + errorMessage);
    return;
  }
  int geofenceTransition = geofencingEvent.getGeofenceTransition();
  if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER ||
    geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) {
    List<Geofence> triggeringGeofences = geofencingEvent.getTriggeringGeofences();
    StringBuffer transitionStringDetails = null;
    switch(geofenceTransition) {
      case Geofence.GEOFENCE_TRANSITION_ENTER:
        transitionStringDetails = new StringBuffer(Constants.GEOFENCE_TRANSITION_ENTER);
    break;
      case Geofence.GEOFENCE_TRANSITION_EXIT:
    transitionStringDetails = new StringBuffer(Constants.GEOFENCE_TRANSITION_EXIT);
    break;
      default:
        transitionStringDetails = new StringBuffer(Constants.GEOFENCE_TRANSITION_UNKNOWN);
    break;
    }
    transitionStringDetails.append(": ");
    for (Geofence geofence: triggeringGeofences) {
      transitionStringDetails.append(geofence.getRequestId() + ", ");
    }
    String transitionString = transitionStringDetails.toString();
    if (transitionString.endsWith(", ")) {
      transitionString = transitionString.substring(0, transitionString.length() - 2);
    }
    sendNotification(transitionString);
    Log.i(Constants.TAG, "The geofence tansaction has been processed: " + transitionString);
  } else {
    Log.e(Constants.TAG, "An exception has occurred: " + Constants.GEOFENCE_TRANSITION_UNKNOWN + " " + geofenceTransition);
  }
}

Transmiterea propriu-zisă a notificării implică invocarea unei activități prin intermediul unui obiect de tip PendingIntent. Aceasta este atașată unei ierarhii, fiind transmisă prin plasarea sa pe stiva de activități.

<manifest ...>
  <!-- other elements -->
  <application ...>
    <!-- other elements -->
    <activity
      android:name=".graphicuserinterface.GoogleMapsGeofenceEventActivity"
      android:parentActivityName=".graphicuserinterface.GoogleMapsActivity">
      <metadata
        android:name="android.support.PARENT_ACTIVITY"
        android:value=".graphicuserinterface.GoogleMapsActivity"/>
    </activity>
  </application>
</manifest>
private void sendNotification(String notificationDetails) {
  Intent notificationIntent = new Intent(getApplicationContext(), GoogleMapsGeofenceEventActivity.class);
  notificationIntent.putExtra(Constants.NOTIFICATION_DETAILS, notificationDetails);
        
  TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
  stackBuilder.addParentStack(GoogleMapsGeofenceEventActivity.class);
  stackBuilder.addNextIntent(notificationIntent);

  PendingIntent notificationPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);

  NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
  builder.setSmallIcon(R.drawable.ic_launcher)
    .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher))
    .setColor(Color.RED)
    .setContentTitle(Constants.GEOFENCE_TRANSITION_EVENT)
    .setContentText(notificationDetails)
    .setContentIntent(notificationPendingIntent);
  builder.setAutoCancel(true);

  NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
  notificationManager.notify(0, builder.build());
}

Operațiile care pot fi realizate legate de o zonă de restricție geografică sunt:

1. adăugarea unei zone de restricție geografică.

O zonă de restricție geografică este construit prin intermediul unei clase Geofence.Builder, în care se specifică parametrii acesteia:

  • setCircularRegion(double, double, float) - se indică coordonatele zonei de restricție geografice:
    • centru - latitudine + longitudine;
    • raza;
  • setExpirationDuration(long) - se precizează durata de timp (exprimată în milisecunde) după care zona de restricția geografică nu va mai fi activă;
  • setLoiteringDelay(int) - se exprimă o durată de timp (exprimată în milisecunde) în care este temporizată transmiterea de notificări, în situația în care se produc evenimente legate de zona de restricție geografică, cu durata mai mică decât cea indicată;
  • setNotificationResponsiveness(int) - se stabilește durata de timp (exprimată în milisecunde) după care va fi transmisă notificarea;
  • setRequestId(String) - se asociază un identificator unic, prin care zona de restricție geografică va putea fi referită în cadrul aplicației Android;
  • setTransitionTypes(int) - se asociază tipurile de tranziții pentru care se vor transmite notificări (de regulă, Geofence.GEOFENCE_TRANSITION_ENTER și Geofence.GEOFENCE_TRANSITION_EXIT).

O solicitare legată de o zonă de restricție geografică, conținută de un obiect GeofencingRequest, este construită prin apelul metodelor addGeofence(Geofence) / addGeofences(List<Geofence>), respectiv build() din cadrul clasei ajutătoare GeofencingRequest.Builder.

Operația de adăugare este realizată prin apelul metodei addGeofences(GoogleApiClient, GeofencingRequest, PendingIntent) din clasa GeofencingApi. Rezultatul acestei operații este furnizat prin intermediul unei clase ascultător ResultCallback<T>, pentru care se implementează metoda onResult(T). Aceasta trebuie precizată explicit prin metoda setREsultCallback(ResultCallback<T>), aplicabilă obiectului de tip PendingResult, construit anterior.

private void addGeofence(String latitude, String longitude, String radius) {
  if (googleApiClient == null || !googleApiClient.isConnected()) {
    Toast.makeText(
      GoogleMapsActivity.this,
      "Google API Client is null or not connected!",
      Toast.LENGTH_SHORT
    ).show();
    return;
  }
  if (latitude == null || latitude.isEmpty() ||
    longitude == null || longitude.isEmpty() ||
    radius == null || radius.isEmpty()) {
    Toast.makeText(
      GoogleMapsActivity.this,
      "All fields (gps coordinates, radius) should be filled!",
      Toast.LENGTH_SHORT
    ).show();
    return;
  }
  geofenceList.add(new Geofence.Builder()
    .setRequestId(Utilities.generateGeofenceIdentifier(Constants.GEOFENCE_IDENTIFIER_LENGTH))
    .setCircularRegion(
      Double.parseDouble(latitude),
      Double.parseDouble(longitude),
      Float.parseFloat(radius)
    )
    .setExpirationDuration(Constants.GEOFENCE_EXPIRATION_IN_MILLISECONDS)
    .setTransitionTypes(
      Geofence.GEOFENCE_TRANSITION_ENTER |
      Geofence.GEOFENCE_TRANSITION_EXIT
    )
    .build());
  GeofencingRequest.Builder builder = new GeofencingRequest.Builder();
  builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER);
  builder.addGeofences(geofenceList);
  GeofencingRequest geofencingRequest = builder.build();
  LocationServices.GeofencingApi.addGeofences(
    googleApiClient,
    geofencingRequest,
    geofenceTrackerPendingIntent
  ).setResultCallback(GoogleMapsActivity.this);
}

2. ștergerea unei zone de restricție geografică:

Operația de ștergere este realizată prin apelul metodei removeGeofences(GoogleApiClient, PendingIntent) din clasa GeofencingApi, aceasta referindu-se la toate zonele de restricție geografică. Rezultatul acestei operații este furnizat prin intermediul unei clase ascultător ResultCallback<T>, pentru care se implementează metoda onResult(T). Aceasta trebuie precizată explicit prin metoda setREsultCallback(ResultCallback<T>), aplicabilă obiectului de tip PendingResult, construit anterior.

private void removeGeofence() {
  if (googleApiClient == null || !googleApiClient.isConnected()) {
    Toast.makeText(
      GoogleMapsActivity.this,
      "Google API Client is null or not connected!",
      Toast.LENGTH_SHORT
    ).show();
    return;
  }
  latitudeEditText.setText(new String());
  longitudeEditText.setText(new String());
  radiusEditText.setText(new String());
  geofenceList.clear();
  LocationServices.GeofencingApi.removeGeofences(
    googleApiClient,
    geofenceTrackerPendingIntent
  ).setResultCallback(GoogleMapsActivity.this);
}

Se poate observa faptul că ambele operații au nevoie de un client Google API nenul și care să fie conectat, motiv pentru care anterior sunt realizate verificările de rigoare, cu semnalarea eventualelor erori.

Zonele de restricție geografică sunt menținute în cadrul unei liste, actualizată corespunzător pentru fiecare dintre operațiile de adăugare / ștergere.

Metoda onResult(Status), care furnizează rezultatul operațiilor de adăugare / ștergere a unei zone de restricție geografică, conține informații suplimentare cu privire la situația curentă:

  1. isSuccess() - operația a fost realizată cu success sau cu eșec;
  2. getStatusCode() - codul de stare, în situația în care s-a produs o eroare.
@Override
public void onResult(Status status) {
  if (status.isSuccess()) {
    geofenceStatus = !geofenceStatus;
  } else {
    String errorMessage = null;
    switch(status.getStatusCode()) {
      case GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE:
        errorMessage = Constants.GEOFENCE_NOT_AVAILABLE_ERROR;
        break;
      case GeofenceStatusCodes.GEOFENCE_TOO_MANY_GEOFENCES:
        errorMessage = Constants.GEOFENCE_TOO_MANY_GEOFENCES_ERROR;
        break;
      case GeofenceStatusCodes.GEOFENCE_TOO_MANY_PENDING_INTENTS:
        errorMessage = Constants.GEOFENCE_TOO_MANY_PENDING_INTENTS_ERROR;
        break;
      default:
    errorMessage = Constants.GEOFENCE_UNKNOWN_ERROR;
        break;
    }
    Log.e(Constants.TAG, "An exception has occurred while turning the geofencing on/off: " + status.getStatusCode() + " " + errorMessage);
  }
}

În situația în care aplicația este întreruptă, informațiile legate de zonele geografice trebuie gestionate corespunzător, fiind recomandat ca persistența să fie realizată prin intermediul unui obiect de tip SharedPreferences.

  1. atunci când aplicația Android nu mai este vizibilă, se salvează datele și sunt șterse toate zonele de restricție geografică, astfel încât să nu mai fie transmise notificări;
  2. atunci când aplicația Android este vizibilă, se încarcă datele și sunt adăugate toate zonele de restricție geografică, în cazul în care acestea au fost definite anterior.

Activitate de Laborator

1. Să se acceseze Google Developer's Console, după ce a fost realizată autentificarea cu datele contului Google (nume de utilizator, parolă):

  • se obține un număr de proiect (dacă această operație nu a fost realizată anterior);
  • se activează serviciul Google Maps Android API;
  • se generează o semnătură digitală de tip SHA-1 folosind utilitarul Java keytool;
  • se creează o cheie publică pentru transmiterea de mesaje provenind de la un dispozitiv mobil, pe baza semnăturii digitale și a pachetului care identifică în mod unic aplicația respectivă.

Note

Pentru toate aplicațiile Android va trebui completată cheia publică în fișierul AndroidManifest.xml.


Mai multe detalii pot fi obținute în secțiunea Configurare.

2. În contul Github personal, să se creeze un depozit denumit 'Laborator11'. Inițial, acesta trebuie să fie gol (nu trebuie să bifați nici adăugarea unui fișier README.md, nici a fișierului .gitignore sau a a fișierului LICENSE).

3. Să se cloneze în directorul de pe discul local conținutul depozitului la distanță de la .

În urma acestei operații, directorul Laborator11 va trebui să se conțină directoarele labtasks și solutions.

    student@eg-106:~$ git clone https:*github.com/eim-lab/Laborator11.git

4. Să se încarce conținutul descărcat în cadrul depozitului Laborator11 de pe contul Github personal.

student@eg-106:~$ cd Laborator11
student@eg-106:~/Laborator11$ git remote add Laborator11_perfectstudent https://github.com/perfectstudent/Laborator11
student@eg-106:~/Laborator11$ git push Laborator11_perfectstudent master

5. Să se configureaze mașina pe care va rula aplicația:

  • în cazul în care se utilizează un emulator, se configurează astfel încât acesta să aibă instalat Google Play Services;
  • în cazul în care se utilizează un dispozitiv fizic, acesta trebuie să ruleze un sistem de operare Android cu o versiune ulterioară 2.2, având asociat un cont Google.

6. Să se importe în mediul integrat de dezvoltare Android Studio proiectul GoogleMapsPlaces din directorul labtasks.

Se dorește să se implementeze o aplicație care să navigheze către o locație specificată prin intermediul coordonatelor GPS (latitudine / longitudine) și pentru care se dorește plasarea unui reper pe hartă, însoțit de o denumire.

Reperele vor fi stocate în cadrul unei liste (obiect de tip Spinner), astfel încât la selecția unui element din cadrul acesteia, se va vizualiza obiectivul geografic marcat anterior.

Se cere să se implementeze funcționalitățile pentru adăugarea unui reper și ștergerea tuturor reperelor de pe hartă:

a) pentru adăugarea unui reper:

  1. se obțin informațiile legate de latitudine, longitudine și denumire, din câmpurile text corespunzătoare și se verifică să fie completate (în caz contrar generându-se un mesaj de eroare);
  2. se realizează navigarea către locația respectivă;
  3. se instanțiază un obiect de tip MarkerOptions, desemnând reperul care va fi plasat pe harta Google;
    `MarkerOptions marker = new MarkerOptions()
      .position(new LatLng(
        Double.parseDouble(latitudeContent), 
        Double.parseDouble(longitudeContent)
      ))
      .title(nameContent);
    marker.icon(BitmapDescriptorFactory.defaultMarker(Utilities.getDefaultMarker(markerTypeSpinner.getSelectedItemPosition())));
    
  4. se adaugă reperul pe harta Google;
  5. se adaugă reperul în lista de locații (places), notificându-se și adaptorul corespunzător obiectului de tip Spinner (placesAdapter) de această modificare, astfel încât acesta să fie actualizat corespunzător.

b) pentru ștergerea tuturor reperelor de pe hartă:

  1. se verifică să fie completate repere pe harta Google (în caz contrar generându-se un mesaj de eroare);
  2. se șterg toate reperele pe harta Google;
  3. se șterg toate reperele din lista de locații (places), notificându-se și adaptorul corespunzător obiectului de tip Spinner (placesAdapter) de această modificare, astfel încât acesta să fie actualizat corespunzător.

7. Să se importe în mediul integrat de dezvoltare Android Studio proiectul GoogleMapsLocationUpdate din directorul labtasks.

Se dorește să se implementeze o aplicație pentru care să se implementeze posibilitatea de actualizare periodică a poziției curente pe hartă, în funcție de starea unui buton, prin care se controlează pornirea / oprirea acestui serviciu.

  1. când serviciul este activat, se vizualizează pe hartă doar locația curentă, completându-se în mod automat informații precum latitudinea și longitudinea, actualizându-se corespunzător poziția;
  2. când serviciul este dezactivat, utilizatorul are posibilitatea de a controla poziția care se vizualizează pe hartă.

De asemenea, se dorește să se poată selecta tipul de hartă care să fie afișat.

Se cere să se implementeze metodele pentru pornirea și oprirea serviciului de actualizare periodică a locației de pe hartă:

a) metoda startLocationUpdates() din clasa GoogleMapsActivity:

  1. se apelează metoda requestLocationUpdates() din clasa

    FusedLocationProviderApi`
    LocationServices.FusedLocationApi.requestLocationUpdates(
      googleApiClient,
      locationRequest,
      this
    );
    
  2. se actualizează starea serviciului de transmitere periodice a locației curente (locationUpdatesStatus);

  3. se vizualizează poziția curentă pe harta Google (se apelează metoda setMyLocationEnabled(true));

  4. se modifică textul și culoarea butonului locationUpdateStatusButton;

  5. se navighează la locația curentă;

  6. se dezactivează controalele grafice latitudeEditText, longitudeEditText, navigateToLocationButton.

b) metoda stopLocationUpdates() din clasa GoogleMapsActivity

  1. se apelează metoda removeLocationUpdates() din clasa FusedLocationProviderApi
    LocationServices.FusedLocationApi.removeLocationUpdates(
      googleApiClient,
      this
    );
    
  2. se actualizează starea serviciului de transmitere periodice a locației curente (locationUpdatesStatus);
  3. nu se vizualizează poziția curentă pe harta Google (se apelează metoda setMyLocationEnabled(false));
  4. se modifică textul și culoarea butonului locationUpdateStatusButton;
  5. se activează controalele grafice latitudeEditText, longitudeEditText, navigateToLocationButton, acestea având un conținut vid.

8. (opțional - necesită Billing pentru Geocoding API) <spoiler> Să se importe în mediul integrat de dezvoltare Android Studio proiectul GoogleMapsGeocoding din directorul labtasks.

Se dorește să se implementeze o aplicație care să realizeze procesul de codificare geografică inversă: dându-se un set de coordonate GPS, se dorește să se determine adresa poștală corespunzătoare.

Se cere să se implementeze procesul de conversie propriu-zis, în cadrul serviciului GetLocationAddressIntentService, pe metoda onHandleIntent().

  1. se instanțiază un obiect de tip Geocoder
    Geocoder geocoder = new Geocoder(this, Locale.getDefault());
    
  2. se obține lista de adrese prin invocarea metodei getFromLocation() care primește ca parametri:
    1. latitudinea;
    2. longitudinea;
    3. numărul de adrese întoarse (Constants.NUMBER_OF_ADDRESSES);
  3. se tratează corespunzător tipurile de execepții ce pot fi generate (IOException, IllegalArgumentException) precum și situația în care nu este furnizat nici un rezultat;
  4. se parcurge lista de adrese: pentru fiecare adresă în parte se concatenează rândurile distincte, concatenându-se toate rezultatele obținute;
    1. numărul de linii dintr-o adresă este furnizat de metoda getMaxAddressLineIndex();
    2. un rând de la o anumită poziție se obține prin intermediul metodei getAddressLine();
  5. se transmite rezultatul către activitatea principală (se apelează metoda handleResult() cu codul numeric de rezultat (Constants.RESULT_SUCCESS, Constants.RESULT_FAILURE) și rezultatul obținut, respectiv mesajul de eroare, după caz.

9. Să se importe în mediul integrat de dezvoltare Android Studio proiectul GoogleMapsGeofencing din directorul labtasks.

Se dorește să se implementeze o aplicație care să monitorizeze activitatea unui dispozitiv mobil raportat la o zonă de restricție geografică.

Serviciul referitor la monitorizarea unei anumite locații poate fi activat sau dezactivat, specificându-se de fiecare dată coordonatele zonei față de care sunt realizate comparațiile.

Aplicația va afișa pe hartă în permanență locația curentă prin actualizările periodice care sunt transmise.

Notificările vor fi generate:

  1. când utilizatorul intră în zona de restricție geografică

    google_maps_geofencing_02.png google_maps_geofencing_03.png
  2. când utilizatorul iese din zona de restricție geografică

    google_maps_geofencing_04.png google_maps_geofencing_05.png

Detaliile cu privire la evenimentul produs vor putea fi vizualizate în cadrul unei activități dedicate.

Se cere să se analizeze evenimentul legat de zona de restricție geografică în cadrul serviciului GeofenceTrackerIntentService, pe metoda onHandleIntent().

  1. se obține evenimentul legat de zona de restricție geografică, din cadrul intenției cu care a fost lansat în execuție serviciul GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent);
  2. se verifică dacă evenimentul legat de zona de restricție geografică conține vreo eroare (prin apelul metodei hasError()), aceasta fiind tratată corespunzător (se jurnalizează eroarea și metoda se termină):
    1. GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE
    2. GeofenceStatusCodes.GEOFENCE_TOO_MANY_GEOFENCES
    3. GeofenceStatusCodes.GEOFENCE_TOO_MANY_PENDING_INTENTS.
  3. se obține tipul de tranziție geografică int geofenceTransition = geofencingEvent.getGeofenceTransition();
  4. se construiește un mesaj explicativ conținând
    1. tipul tranziției: Geofence.GEOFENCE_TRANSITION_ENTER / Geofence.GEOFENCE_TRANSITION_EXIT;
    2. identificatorii restricțiilor legate de poziționarea geografică:
      1. se obține lista tuturor restricțiilor legate de poziționarea geografică List<Geofence> triggeringGeofences = geofencingEvent.getTriggeringGeofences();
      2. se parcurge lista și pentru fiecare obiect se obține identificatorul unic (getRequestId());
  5. se transmite mesajul explicativ prin intermediul unei notificări (ca parametru al metodei sendNotification()).

10. Să se încarce modificările realizate în cadrul depozitului 'Laborator11' de pe contul Github personal, folosind un mesaj sugestiv.

student@eg-106:~/Laborator11$ git add *
student@eg-106:~/Laborator11$ git commit -m "implemented taks for laboratory 10"
student@eg-106:~/Laborator11$ git push Laborator11_perfectstudent master

Regulament


Disciplina Elemente de Informatică Mobilă este inclusă în planul de învățământ al specializării C2: Sisteme Incorporate, fiind destinată studenților anului IV din cadrul Facultății de Automatică și Calculatoare a Universității "Politehnica" București, programul de studii Calculatoare.

Sunt prevăzute, săptămânal, 3 ore de curs, desfășurate în sala XX??? și 2 ore de laborator, desfășurate în sala XX???. În urma promovării acestei discipline vor fi obținute 5 puncte de credit transferabile (ECTS).

Notare


Nota la disciplina Elemente de Informatică Mobilă este formată din:

  • test final (30%)
  • activitate pe parcursul semestrului (70% + 10% bonus lab + 10% bonus curs)
Final 	ACTIVITATE PE PARCURS
        Test curs 	Colocvii 	Bonus lab 	Bonus curs
 30%    20%         50% 	    10% 	    10% 

Pentru promovarea disciplinei, este necesar să se întrunească, în ordine, următoarele condiții:

  1. maximum 3 absențe la laborator
  2. obținerea unui punctaj de minim, oricare dintre:
    • 2a. fie 25p din cele 60p pentru colocvii + bonus lab (50p + 10p)
    • 2b. fie 40p din cele 80p pentru punctajul de colocvii + bonus lab + partial (50p + 10p + 20p)
  3. obținerea unui punctaj general (≥ 50%)

Test final

Testul final este scris și se desfașoară în sesiunea de iarnă. El poate fi susținut de studenții cu unul dintre minime obținute pe parcurs. Nu este permis accesul cu dispozitive electronice de comunicare sau cu materiale scrise (closed book).

Test parțial

La mijlocul semestrului va fi programat un test din materialul predat în prealabil. Nu este permis accesul cu dispozitive electronice de comunicare sau cu materiale scrise (closed book).

Colocvii

In săptămânile ~7 și 14, în cadrul orelor de laborator, se vor desfășura două colocvii la care prezența este obligatorie. Colocviul este un test practic, desfășurat pe parcursul unui interval de timp limitat, în care se solicită rezolvarea unui set de sarcini punctate independent. Pe tot parcursul colocviului, este permisă consultarea documentației, inclusiv în format electronic. Este strict interzisă folosirea oricăror mijloace de comunicație pentru a colabora cu colegii sau cu alte persoane.

Punctajul total maxim pentru cele două colocvii este de 50p, acesta este distribuit 20p primului colocviu, 30p celui de-al doilea. Nu este posibilă refacerea colocviilor în sesiunile de restanțe.

Bonus laborator

Pe parcursul celor 2 ore de laborator veți avea acces (doar din XXX???) la un chestionar de 3 întrebări / 3 minute care verifică parcurgerea materialului pregătitor acelei săptămâni (întrebările au un singur răspuns corect). Pentru a avea acces la chestionare, trebuie să folosiți conturile de Moodle pe site-ul wi-fi.cs.pub.ro/eim

Scorul obținut la aceste chestionare va adăuga încă 10p (1 punct la nota finală).

Bonus curs

Un sistem similar de bonusuri bazat pe același cont va fi disponibil si pentru curs. La unele cursuri veti primi bilete cu coduri de acces care vă permit accesul la un chestionar online cu întrebări din cursul la care ați participat. Întrebările pot avea mai multe răspunsuri corecte, și nu se punctează parțial decât în cazul în care nu marcați vreunul dintre răspunsurile incorecte (De exemplu dacă răspunsurile a și c sunt corecte, și bifați doar a, veți primi 50% din întrebare; dacă bifați și f, veți primi 0 pentru acea întrebare).

Scorul obținut la aceste chestionare va adăuga încă 10% (1 punct la nota finală).

Restanță

Exemenele din sesiunea de restanță se desfășoară oral, și sunt pentru punctajul corespunzător examenului(30%), nu pentru punctajele obținute pe parcurs. Nu se pot reface în restanță: punctajele de laborator, testul parțial, punctajele de bonus de la laborator/curs.

Organizare


  • Laboratorul se desfășoară pe semigrupe, are alocate 2 ore săptămânal și se desfășoară în sala EG206/EG103c.

  • Laboratoarele sunt obligatorii (cf. Regulamentului UPB), dar un maximum de 3 absențe sunt acceptabile.

  • Este permis transferul de studenți între semigrupe în săptămânile 1-2 ale semestrului (printr-o cerere în persoană către asistentul de laborator), la schimb, atâta timp cât nu se depășește numărul de 15 studenți per interval orar. Din săptămâna 2 nu se mai permit nici un fel de modificări în formația de studiu. Studenții care refac laboratorul în acest an universitar pot veni la orice laborator, în funcție de orarul celorlalte materii.

  • Adresarea între cadrul didactic şi studenţi se va face la persoana a II-a singular.

  • Ideal, fiecare laborator va începe la :05 pentru ca impactul asupra studenţilor ce întârzie să fie minim. După acest interval de timp, accesul studenților în sală nu va mai fi permis.

  • Fiecare laborator va dispune de un suport electronic care se va publica pe site cu o zi înainte de ora în cauză. Acesta va consta dintr-un document (in format wiki) în care noţiunile sunt expuse amănunţit şi din unul sau mai multe (schelete de) proiecte ce vor fi utilizate pentru rezolvarea sarcinilor ce vizează fixarea cunoştinţelor expuse teoretic.

  • Este OBLIGATORIU ca studenţii să parcurgă suportul de laborator înainte de desfăşurarea orei - in special sectiunile marcate ca fiind obligatorii -, astfel încât să fie familiarizaţi cu problematica acestuia.

  • În prima parte a laboratorului (maxim 20 de minute) se vor prezenta noţiunile cu care urmează să se lucreze în cadrul orei şi care vor fi evaluate in cadrul colocviilor.

  • În a doua parte a laboratorului, se vor rezolva sarcini în legătură cu tematica în cauză.

  • Rezolvările la sarcinile propuse în cadrul laboratorului vor fi publicate pe contul de github al disciplinei.

  • BYOD Sunteţi încurajaţi ca la orele de laborator să veniţi cu laptopurile personale și/sau cu dispozitivele mobile personale. Astfel, veți avea configurate pe mașinile proprii mediile de lucru necesare rezolvării sarcinilor de la orele de laborator, care pot fi continuate și acasă.

  • În situația în care veți lucra folosind calculatoarele din laborator, se recomandă încărcarea codului sursă aferent proiectului Android în contul personal de GitHub, astfel încât să se poată relua procesul de implementare de pe orice mașină.

Frauda Academică


Se consideră fraudă academică orice incercare de colaborare cu colegii sau cu alte persoane, inclusiv prin intermediul mijloacelor de comunicare electronice, in timpul colocviilor sau al examenului scris.

Într-o astfel de situație, se va anula întregul punctaj aferent activității de pe parcursul semestrului, ceea ce atrage dupa sine refacerea disciplinei în anul universitar următor.

Calendar

Împărțirea pe Semigrupe

  • În cadrul unei ore de laborator pot participa maxim 16 studenți.
  • Transferurile între intervale orare sunt permise atâta vreme cât nu este depășită capacitatea laboratorului, la schimb cu un alt student, pentru a se asigura o echilibrare a încărcării. Mutări ale studenților sunt permise până în săptămâna a 2-a, după care formațiile de lucru rămân definitive. Transferurile se fac în persoană, cu acordul asistentului, în măsura spațiului disponibil.
  • Marcarea persoanelor care și-au schimbat semigrupa din care făceau parte se face prin font italic.
  • Restanțierii pot participa la orele de laborator în oricare dintre intervale, fără a depăși însă limita impusă cu privire la numărul de locuri.
  • Folosiți acest document pentru a va alege semigrupa - negociați cu colegii rocadele

Echipa

Curs

Dragoș Niculescu

Laborator

  • Alex Aldoiu
  • Mădălina Barbu
  • Vlad Bădoiu
  • Bogdan Dobrin
  • Bianca Hulubei
  • Dragoș Niculescu
  • Nic Nițu

Test practic 1

Exemplu de colocviu rezolvat.

Test practic 2

Exemplu de colocviu rezolvat.

comanda adb se regăsește acolo unde ați instalat SDK-ul, de exemplu la: /opt/Android/SDK/platform-tools/adb

USB setup

  • Dacă laptopul Linux nu vede telefonul la comanda adb devices

  • udev rules

adb over wifi

using CLI

  • adb over wifi

  • Connect the device and the computer to the same Wi-Fi network

  • Plug the device to the computer with a USB cable to configure the connection

  • On the computer command line type: adb tcpip 5555

  • On the computer command line type: adb shell ip addr show wlan0 and copy the IP address after the "inet" until the "/". You can also go inside the Settings of the device to retrieve the IP address in Settings → About → Status.

  • On the computer command line type: adb connect ip-address-of-device:5555

  • You can disconnect the USB cable from the device and check with adb devices that the device is still detected.

using Android Studio menus

  • Din meniul de device-uri, se selectează "Pair Devices Using WiFi"

imagine AVD cu root și GMS

  • Unzip lineage-gms-root.zip at location where studio keeps its images, for example
    /opt/Android/SDK/system-images/android-35/lineage-gms/x86_64

  • Create virtual device

Android Studio -> Device Manager -> +(Add New Device, small phone) -> Create Virtual Device -> Next -> x86 Images -> Refresh -> Under release name it shows “API 35” but it doesn’t have the download icon ; under Target it says ‘Android API 35 (LineageOS)’

  • Give it a unique name, say Lineageos-EIM1

  • Advanced - choose low specs: 2 CPU, 512M of RAM

  • Setup – use 3 button navigation

  • In Android, go to Settings/About Device/Build number – click it 5 times to become a developer

  • Settings/search for root to find the option for rooted debugging; enable root

  • From developement machine

adb root 
adb shell
  • After first boot and setup, user data and apps are stored in $HOME/.android/avd/EIM_lineageos_1.avd/
  • create a second machine if needed

Documentație examen

Părți din următoarele materiale:

  • Karim Yaghmour, Embedded Android
  • Jochen Schiller, Mobile Communications
  • Andrew Tanenbaum, Rețele de Calculatoare
  • Henry Sinnreich, Alan B. Johnston, Internet Communications Using SIP
  • John Krumm, Ubiquitous Computing Fundamentals
  • Stuart Chesire, Zeroconf The Definitive Guide

Android intro

* Yaghmour, ch 2 (p25-75) 

Generalități despre radio

* material doar pe slide-uri

Metode de acces la mediu

* Schiller sec 2.5 Multiplexing (p41-46)
* Schiller ch 3 Medium Access Control (p69-77, 89-90)
  * până la 3.4.4 inclusiv
  * 3.5 și 3.6  fără <del>3.5.1</del> (p82-87, 89-90)
* Tanenbaum CDMA p148-152

Rețele celulare

* 2G, 3G, 4G  
  * Schiller ch 4.1-4.1.3.1(p96-107), 4.1.5-4.1.7(p113-119), Fig 4.16(p127-128)
  * Schiller ch 4.4.1,4.4.2 (p141-143), 4.4.4, 4.4.5(fără protocoale), 4.4.6 (p149-156)
* material extra doar pe slide-uri
* piața din România (nu se cere la examen)

WiFi

  * Gast ch3, (pp55-67) 
  * Schiller  ch7.3 (p207-211, 214-220, 222-239) 
     * fără <del>7.3.3, 7.3.4.3</del>
     * până la 7.4  

Mobilitatea și nivelul rețea

* Mobile IP
  * Schiller, ch8 (p303-323)
    * până la 8.1.9 
  * Schiller 351-360, 366-371
    * fără <del>9.2.3 - 9.2.7</del>   
* Zeroconf
  * Cheshire, cap 2, 3, 4 (pp11-69) 

Mobilitatea și nivelul transport

* review TCP
    * Tanenbaum (pp499-506)
    * doar 6.5.9, 6.5.10, 6.5.11  
* material exclusiv pe slide-uri 

VoIP & SIP

* VoIP
  * Sinnreich ch 18 (p301-315)

* SIP 
  * Sinnreich ch 6 (p97-132) 

Locație

  * Krumm, cap 7 (p286-303)
  * material exclusiv pe slide-uri    

PDF cu toate matrialelele concatenate (access restricționat).