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
Seria | Zi | Ora | Sala | Instructor |
---|---|---|---|---|
C2 + opționali | marți ambele | 10.00-12.00 | EG301 | Dragoș |
C2 + opționali | joi pare | 12.00-14.00 | PR002 | Dragoș |
Laborator
Grupa | Zi | Ora | Sala | Asistent |
---|---|---|---|---|
OPT4 | marți | 08.00-10.00 | EG103b | Ana |
341C2 | marți | 18.00-20.00 | EG103b | Cristian |
OPT1 | miercuri | 08.00-10.00 | EG105 | Iulia |
OPT2 | miercuri | 08.00-10.00 | EG206 | Ana |
343C2 | miercuri | 12.00-14.00 | EG206 | Nic |
341C2 | miercuri | 14.00-16.00 | EG206 | Nic |
343C2 | miercuri | 16.00-18.00 | EG103b | Alex |
342C2 | joi | 14.00-16.00 | EG103b | Vlad |
343C2 | joi | 16.00-18.00 | EG103b | Vlad/Corina |
Kotlin Prj | TBD | Teams | Bianca/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
Android Internals
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
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
Rețele prin satelit
Curs 10 - Rețele prin Satelit PPTX
Poziționare
- 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:
-
Porniți Android Studio.
-
Î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.
-
Asigurați-vă că Phone and Tablet este selectată.
-
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!"
. -
Faceți clic pe Next. Se deschide dialogul New Project. Acesta are câteva câmpuri pentru configurarea proiectului.
-
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.
- 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ă.
- 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
- 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ă.
- 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
)
}
- 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!
- 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 metodaonCreate()
. - În corpul lambda-ului
setContent{}
, apelează lambda-ulDiceRollerTheme{}
și apoi în interiorul lambda-uluiDiceRollerTheme{}
, apelează funcțiaDiceRollerApp()
.
/* 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ă
- În Android Studio, facem clic pe View > Tool Windows > Resource Manager.
- Facem clic pe + > Import Drawables pentru a deschide un browser de fișiere.

- Găsim și selectăm folderul cu cele șase imagini de zaruri și procedăm la încărcarea lor.

- Facem clic pe Next.

-
Apare dialogul Import Drawables și arată unde merg fișierele de resurse în structura de fișiere.
-
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.
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
-
Deschideți Android Studio.
-
În dialogul Welcome to Android Studio, faceți clic pe Start a new Android Studio project.
-
Selectați Empty View Activity (nu implicit). Faceți clic pe Next.
-
Denumiți-vă aplicația cu un nume precum My First app
-
Asigurați-vă că Languages este setată la Java/Kotlin.
-
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).)
- Dublu-click pe folderul app (1) pentru a extinde ierarhia fișierelor app. (Vezi (1) în captura de ecran.)
- 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.
- 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.
- 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ă:
- î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)
}
}
- prin intermediul metodei
onRestoreInstanceState()
, apelată în mod automat între metodeleonStart()
șionResume()
; 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.
- În Android Studio, construiește și rulează aplicația ta pe un dispozitiv fizic sau pe un emulator.
- 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 comandaadb 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-ullabtasks
.
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 Home | 1 | 3 | 2 | ||||||
buton Back | 1 | 2 | 3 | ||||||
buton _OK_in app | nici | una | din tre | met ode | nu | se | ape lea ză | ||
buton lista app | 1 | 3 | 2 | ||||||
apel tele fonic | 1 | 3 | 2 | ||||||
acce ptare | 1 | 2 | |||||||
resp ingere | |||||||||
rotire ecran | 5 | 6 | 1 | 3 | 4 | 2 | 7 |
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 -sadb pull /sdcard/Download .
- pentru a descărca fișiere/directoare din device în mașina de dezvoltareadb push fișier.local /sdcard/Download/
- încărcareadb 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
- 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ă;
- 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 generatR.java
pentru toate componentele din cadrul interfeței grafice care au definit atributulandroid: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:
ATRIBUT | TIP OBIECT | DESCRIERE |
---|---|---|
layout_width | View / ViewGroup | lățimea obiectului |
layout_height | View / ViewGroup | înălțimea obiectului |
layout_marginTop | View / ViewGroup | spațiu suplimentar ce trebuie alocat în partea de sus a obiectului |
layout_marginBottom | View / ViewGroup | spațiu suplimentar ce trebuie alocat în partea de jos a obiectului |
layout_marginLeft | View / ViewGroup | spațiu suplimentar ce trebuie alocat în partea din stânga a obiectului |
layout_marginRight | View / ViewGroup | spațiu suplimentar ce trebuie alocat în partea din dreapta a obiectului |
layout_gravity | View | modul de poziționare a elementelor componente în cadrul unui container |
layout_weight | View | proporția pe care o are controlul, raportată la întregul conținut al containerului |
layout_x | View / ViewGroup | poziția pe coordonata x |
layout_y | View / ViewGroup | poziț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 cudp
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:
- 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"
- definirea unei clase ascultător în codul sursă
- 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); - 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;
- 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;
- 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.
- 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
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:
- 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ățilorlayout_alignParentTop
,layout_alignParentBottom
,layout_alignParentLeft
,layout_alignParentRight
,layout_centerHorizontal
,layout_centerVertical
- 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:
- în fișierul AndroidManifest.xml, în elementul
<activity>
corespunzător, se specifică proprietateaandroid:screenOrientation
cu valorileportrait
, respectivlandscape
; - programatic, în metoda
onCreate()
, se apeleazăsetRequestedOrientation()
cu unul din parametriiActivityInfo.SCREEN_ORIENTATION_PORTRAIT
sauActivityInfo.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
- să se blocheze tranziția între modurile portrait și landscape:
-
în fișierul AndroidManifest.xml
<manifest ...> <application ... > <activity ... android:screenOrientation="portrait" ... /> <!-- ... --> </application> </manifest>
-
programatic
@Override protected void onCreate(Bundle onCreateInstanceState) { super.onCreate(onCreateInstanceState); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); * .. }
-
- 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ă New → Android 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.


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âmpulextra
al unei intenții; - adresa electronică;
- adresa poștală.
- un buton (
- 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 metodeifindViewById(R.id....)
; - se implementează o clasă ascultător pentru butoane, care
implementează
View.OnClickListener
și implementează metodaonClick(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 metodasetVisibility()
a clasei Java împreună cu constanteleView.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:
- 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
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țieiContactsManager
este verificată intenția cu care este pornită, și în cazul în care aceasta nu este nulă, este preluată informația din secțiuneaextra
, identificată prin cheiaro.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());
- 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:
- î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 Run → Edit Configurations..., iar în secțiunea Launch Options de pe panoul General, se selectează opțiunea Launch: Nothing.

- 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:
-
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.
-
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.
-
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()
sauonBind()
). 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 apelareastopSelf()
saustopService()
. 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 unuiIBinder
. 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 fiindfalse
, 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 estetrue
, 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()
, respectivbindService()
), 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țaConstants
). 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 cheiaConstants.DATA
și valoarea dată deConstants.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ă:
- acțiunea corespunzătoare intenției, folosind metoda getAction();
- informațiile transmise conținute în câmpul
extra
, folosind metodele corespunzătoare tipului de date identificat pe baza intenției (getStringExtra(), getIntExtra(), getStringArrayListExtra()).
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:
- 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;
- prin stabilirea unei legături punct la punct, folosind Bluetooth;
- printr-o conexiune de date realizată prin intermediul portului USB.
Pentru tethering, pe telefon, se accesează Settings → Wireless & Networks → Tethering & 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;

- 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:
- un client care se conectează la o anumită adresă, pe un anumit port, pe care le cunoaște în prealabil;
- 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 tipboolean
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 transmiteEOF
, iar metoda întoarce valoareanull
.
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
- Înregistrarea dispozitivelor: Fiecare dispozitiv obține un registration token unic generat de FCM, care este asociat cu aplicația instalată.
- 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).
- Livrarea mesajelor: FCM prioritizează livrarea mesajelor și oferă mecanisme de retry pentru dispozitive offline.
Tipuri de mesaje
- Mesaje de notificare: Conțin un payload specific de notificare (titlu, body, imagine).
- 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.
- 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 deunsubscribe
care opreste primirea mesajelor trimise pe un anumit topic.
Folosirea serviciului FCM in cadrul unei aplicatii este conditionata de:
- 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
- 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
- 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
optiuneaMessaging
- 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 completatiDebug signing certificate SHA-1
.
- Inregistrati aplicatia si continuati la seciunea
Download and add config file
. Descarcati fisierulgoogle-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 fisierulbuild.gradle.kts(Module :app)
, in sectiunea dependencies:implementation(libs.firebase.common.ktx)
siimplementation(libs.firebase.messaging.ktx)
- pentru Kotlin sauimplementation(libs.firebase.common)
siimplementation(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 facesubscribe
la topicul cu numele introdus in EditText - la click pe butonul
UNSUBSCRIBE
, va fi apelata o metoda prin care veti faceunsubscribe
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
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 chatConnectThread
- Responsabil pentru conectarea la un socket BluetoothAcceptThread
- Serverul care asteapta conexiuniConnectedThread
- Threadul pe care vor comunica cele 2 dispozitive Bluetooth
Click pentru a vedea interfata grafica a aplicatiei
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 tipGET /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 webPOST /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 cererePOST
; - o cerere
GET
rămâne în istoricul aplicației de navigare, fapt ce nu este valabil și pentru o cererePOST
; - o cerere
GET
poate fi reținută printre paginile Internet favorite din cadrul programului de navigare, fapt ce nu este valabil și pentru o cererePOST
; - 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 cererePOST
; - 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 cererePOST
; - o cerere
GET
ar trebui să fie folosită doar pentru obținerea unei resurse, fapt ce nu este valabil și pentru o cererePOST
.
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 MIMEAccept-Charset
- setul de caractereAccept-Encoding
- mecanismul de codificareAccept-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 DNSAuthorization
- informații de autentificare în cazul unor operații care necesită drepturi privilegiateCookie
- transmite un cookie primit anteriorDate
- 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 CODURI | SEMNIFICAȚIE | DESCRIERE |
---|---|---|
1xx | Informație | ră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 |
2xx | Succes | răspuns ce indică faptul că cererea a fost primită, înțeleasă, acceptată și procesată cu succes |
3xx | Redirectare | ră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ă |
4xx | Eroare la client | ră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ă) |
5xx | Eroare la server | cod 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 codificareContent-Language
- limbaContent-Length
- dimensiuneaContent-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țiSet-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 metodeiexecute()
a claseiHttpClient
(pe lângă obiectulHttpGet|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 JSON | tip de date | detaliu |
---|---|---|
lat | double | latitudine |
lng | double | longitudine |
magnitude | double | magnitudinea |
depth | double | adâncimea |
src | String | sursa informației |
datetime | String | data ș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:
- 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);
- 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:
- configurarea diferiților parametri (pregătirea mediului de lucru);
- î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);
- 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;
- 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
sauudp
(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 obiectulConnectionInfo
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
- registerService(ServiceInfo) - folosită pentru înregistrarea unui serviciu, care să poată fi disponibil ulterior în cadrul rețelei locale;
- unregisterService(ServiceInfo) sau unregisterAllServices()- folosită pentru deînregistrarea unui serviciu, astfel încât acesta să nu mai poată fi accesat, atunci când nu mai este necesar sau aplicația Android este distrusă.
- 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:
- tip de serviciu necunoscut (cu toate că descoperirea implică filtrarea după un tip de servicii specific);
- descoperirea serviciului oferit de mașina curentă / dispozitivul curent - se realizează comparația dintre denumirea serviciului găsit și denumirea serviciului curent;
- 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 tipJmDNS
.
-
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:
- getInetAddresses() - adresele la care poate fi accesat serviciul;
- getPort() - portul pe care poate fi accesat serviciul.
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:
SendThread
- pentru trimiterea de mesaje;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:
-
î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.
-
pornirea / oprirea operației de descoperire a serviciilor în rețeaua locală;
-
conectarea / deconectarea la un serviciu descoperit sau la un dispozitiv mobil care a accesat serviciul înregistrat;
-
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.101
→ 192.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 (Machine → Settings 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:

- în configurația aferentă fiecărui dispozitiv virtual (Machine → Settings 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:
- se va prelua un mesaj din coada
messageQueue
(de tipBlockingQueue<String>
); metodatake()
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 tipulInterruptedException
care trebuie tratată; - în cazul unui mesaj nenul, sunt realizate următoarele operații:
- se trimite mesajul pe canalul de comunicație corespunzător (se
apelează metoda
println()
a obiectului de tipPrintWriter
); - se construiește un obiect de tip
Message
, format din:- conținut: valoarea propriu-zisă a mesajului (șirul de caractere);
- tip: mesaj transmis (
Constants.MESSAGE_TYPE_SENT
);
- se atașează mesajul istoricului conversației (obiectul
conversationHistory
este de tipulList<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; - se afișează mesajul în fragmentul de tip
ChatConversationFragment
(prin intermediul metodeiappendMessage()
), dacă acesta este asociat activității la momentul respectivif (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); } }
- se trimite mesajul pe canalul de comunicație corespunzător (se
apelează metoda
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:
- se primește mesajul pe canalul de comunicație corespunzător (se
apelează metoda
readLine()
a obiectului de tipBufferedReader
); - în cazul unui mesaj nenul, sunt realizate următoarele operații:
- se construiește un obiect de tip
Message
, format din:- conținut: valoarea propriu-zisă a mesajului (șirul de caractere);
- tip: mesaj transmis (
Constants.MESSAGE_TYPE_RECEIVED
);
- se atașează mesajul istoricului conversației (obiectul
conversationHistory
este de tipulList<Message>
); - se afișează mesajul în fragmentul de tip
ChatConversationFragment
(prin intermediul metodeiappendMessage()
), dacă acesta este asociat activității la momentul respectiv.
- se construiește un obiect de tip
8. Să se examineze folosind tcpdump/wireshark schimbul de mesaje între dispozitive
- asigurați-vă folosind
ping
că telefoanele sunt în contact IP între ele, și cu mașina host (Linux) - 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
. - 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
- pregătiți recoltarea pachetelor folosind comanda
- 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
- rulați serviciul folosind
- 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 laadresa:port
(pe Android sau Linux).
- puteți folosi clienți Linux cu comanda
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:
- tranzacția 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); - agentul intermediar
- trimite înapoi (imediat) un răspuns de tip
100 Trying
către agentul utilizator sursă (pentru ca acesta să nu mai transmită nimic); - caută agentul utilizator destinație folosind un server de
localizare și îi trimite (mai departe) cererea de tip
INVITE
;
- trimite înapoi (imediat) un răspuns de tip
- agentul utilizator destinație transmite, prin intermediul
agentului intermediar, un răspuns de tipul
180 Ringing
, către agentul utilizator sursă;
- un agent utilizator (sursă) trimite o cerere de tip
- 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; - tranzacția 3: orice participant poate transmite un mesaj de tipul
BYE
pentru a termina legătura, fiind necesar ca acesta să fie confirmat prin200 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 tipul2xx
sau s-a transmis unACK
; un dialog stabilit între doi agenți utilizatori continuă până în momentul în care se transmite un mesaj de tipulBYE
;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 tipINVITE
;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
;
- de bază
- 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:
CLASA | TIP | DESCRIERE | ACȚIUNE |
---|---|---|---|
1xx | Provizoriu | Informație | Se precizează starea unui apel înainte ca un rezultat să fie disponibil. |
2xx | Definitiv | Succes | Cererea a fost procesată cu succes. Pentru cereri de tip INVITE se întoarce ACK . Pentru alte tipuri cereri, se oprește retransmiterea acestora. |
3xx | ::: | Redirectare | Se 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 Client | Cererea nu a fost procesată cu succes datorită unei erori la client, fiind necesar ca aceasta să fie reformulată. |
5xx | ::: | Eroare la Server | Cererea 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 Configuration | SIP 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
- va configura motorul Linphone, ai cărui parametri sunt plasați sub forma unor valori asociate unor chei
- 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 defaultOSX
OSX: TODO - utilitarul airport, instalare wiresharkSe 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:
- 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);
- 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;
- 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;
- 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 Library → Google 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 API → Create
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 Javakeytool
, 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
- prin accesarea butonului Google Maps Android API → Create
credentials, care implică următoarele etape:
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 Tools → Android → SDK Manager, secțiunea SDK Tools.
Hidden
Biblioteca pentru accesarea funcționalității oferite de serviciul de localizare se găsește la `În mediul integrat de dezvoltare Eclipse, se realizează o referință către biblioteca Google Play Services, astfel descărcată.
-
se accesează File → Import → Android → Existing 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țiuneadependencies
: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" />
- cheia publică utilizată pentru accesarea funcționalității
legată de serviciile de localizare
- 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 indică permisiunile necesare:
- 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 (Settings → Personal).
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 (Settings → Personal → Location → Mode).
- 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 (Settings → Location 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 Control → Location 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:
- fromAsset(String) -
în directorul
assets
, ce conține resurse externe; - fromBitmap(Bitmap) - dintr-o imagine;
- fromFile(String) - dintr-un fișier aflat la o cale relativă;
- fromPath(String) - dintr-un fișier aflat la o cale absolută;
- fromResource(int) -
în directorul
drawable
, ce conține resurse care pot fi desenate.
- fromAsset(String) -
în directorul
- în formatul standard, disponibil în mai multe culori (metoda
defaultMarker(float));
- 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:
- CircleOptions - formă geometrică de tip cerc;
- GroundOverlayOptions - suprapunerea unei alte imagini;
- PolygonOptions - formă geometrică de tip poligon;
- PolylineOptions - formă geometrică de tip polinie;
- TileOverlay - suprapunerea unei alte imagini, pentru o anumită porțiune.
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:
- setBuildingsEnabled(boolean) - vizualizarea exterioarelor de clădiri în format 3D;
- setContentDescription(String) - descriere;
- setIndoorEnabled(boolean) - vizualizarea configurațiilor interioarelor de clădiri;
- setTrafficEnabled(boolean) - traficul la momentul de timp curent;
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():
- setAllGesturesEnables(boolean) - permite sau nu toate tipurile de operații care pot fi realizate prin intermediul hărții Google;
- setCompassEnabled(boolean) - activează sau dezactivează busola;
- setIndoorLevelPickerEnabled(boolean) - stabilește permisiunile de selectare a unui nivel în cazul unor hărți de interior;
- setMapToolbarEnabled(boolean) - indică configurările de vizualizare pentru bara de unelte;
- setMyLocationButtonEnabled(boolean) - referă vizualizarea controlului grafic pentru centrarea hărții Google în funcție de locația curentă;
- setRotateGesturesEnabled(boolean) - precizează dreptul de utilizare al gesturilor legate de rotirea camerei pentru perspectiva de vizualizare;
- setScrollGesturesEnabled(boolean) - determină posibilitatea de folosire a gesturilor pentru derulare, folosind un deget;
- setTiltGesturesEnabled(boolean) - instaurează politica referitoare la gesturile de derulare, folosind două degete;
- setZoomControlsEnabled(boolean) - desemnează vizualizarea sau nu a unor controale grafice pentru gradul de detaliere a hărții Google;
- setZoomGesturesEnabled(boolean) - controlează utilizarea gesturilor pentru vizualizarea hărții Google folosind un anumit grad de detaliere.
Pentru interacțiunea cu utilizatorul au fost definite mai multe clase ascultător, ale căror metode semnalează declanșarea unor evenimente specifice:
- GoogleMap.OnCameraChangeListener - modificarea poziției camerei prin care este vizualizată harta Google;
- GoogleMap.OnIndoorStateChangeListener - schimbarea stării legate de vizualizarea la nivel de interior al clădirii (clădirea curentă, etajul la care se găsește utilizatorul);
- GoogleMap.OnInfoWindowClickListener - evenimente legate de fereastra ce conține informații suplimentare cu privire la o locație;
- GoogleMap.OnMapClickListener - acțiune de tipul apăsare scurtă a unei poziții de pe harta Google;
- GoogleMap.OnMapLoadedCallback - marcarea momentului în care sunt vizibile toate componentele necesare la un moment dat;
- GoogleMap.OnMapLongClickListener - acțiune de tipul apăsare lungă a unei poziții de pe harta Google;
- GoogleMap.OnMarkerClickListener - descrie interacțiunea de tip apăsare a unui reper de pe harta Google;
- GoogleMap.OnMarkerDragListener - descrie interacțiunea de tip mutare a unui reper de pe harta Google;
- GoogleMap.OnMyLocationButtonClickListener - accesarea butonului pentru centrarea hărții în funcție de locația curentă.
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 metodeionStart()
; - metoda
disconnect()
se apelează în cadrul metodeionStop()
.
@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()
șiremoveLocationUpdates()
; - 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:
- requestLocationUpdates(GoogleApiClient, LocationRequest, LocationListener) - pornește transmitersa actualizărilor periodice;
- removeLocationUpdates(GoogleApiClient, LocationListener) - oprește transmiterea actualizărilor periodice.
Î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:
- getFromLocation(double, double, int) - primește ca parametrii coordonatele GPS (latitudinea / longitudinea);
- getFromLocationName(String, int, double, double, double, double) - primește ca parametrii o descriere a locației precum și o zonă geografică descrisă prin coordonatele GPS (latitudine / longitudine) ale punctelor stânga sus - dreapta jos;
- getFromLocationName(String, int) - primește ca parametru o descriere a locației.
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 metoda
onReceiveResult()care primește ca parametrii un cod de rezultat numeric (succes sau eșec, definit de utilizator) și un obiect de tip
Bundle` î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:
- se instanțiază un obiect de tip
Intent
specificând clasa corespunzătoare serviciului care se dorește a fi lansat în execuție; - se plasează informațiile în obiectul
Bundle
disponibil în secțiuneaextra
.
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:
- preluarea informațiilor necesare (obiect de tip
ResultReceiver
, locația care se dorește a fi rezolvată) printr-un obiectBundle
, din cadrul secțiuniiextra
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; - 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ă; - invocarea metodelor
getFromLocation()
/getFromLocationName()
(furnizând informațiile necesare ca parametri) și gestionând corespunzător tipurile de eroare ce pot fi generate:- serviciul Geocoding nu este disponibil (se aruncă o excepție de
tip
IOException
); - informațiile cu privire la coordonatele GPS (latitudine /
longitudine) nu sunt corecte (se aruncă o excepție de tipul
IllegalArgumentException
);
- serviciul Geocoding nu este disponibil (se aruncă o excepție de
tip
- se semnalează situația în care nu a putut fi identificată nici o adresă poștală asociată datelor specificate;
- pentru fiecare adresă poștală furnizată, se concatenează rândurile
distincte:
- numărul de rânduri conținute este întors de metoda getMaxAddressLineIndex();
- un rând de la o anumită poziție este dat de metoda getAddressLine(int);
- 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 metodaonReceiveResult()
; informațiile vor fi plasate în cadrul unui obiectBundle
, 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:
- 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():
GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE
- serviciul de restricționare geografică nu este disponibil;GEOFENCE_TOO_MANY_GEOFENCES
- au fost definite mai multe zone de restricție geografică (restricționate la maxim 100);GEOFENCE_TOO_MANY_PENDING_INTENTS
- există mai multe servicii care procesează evenimente legate de restricționarea geografică;- alt tip de eroare.
- este obținut tipul de tranziție, prin intermediul metodei
getGeofenceTransition()
Geofence.GEOFENCE_TRANSITION_ENTER
- utilizatorul a intrat în zona de restricție geografică;Geofence.GEOFENCE_TRANSITION_EXIT
- utilizatorul a ieșit din zona de restricție geografică.
- 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; - 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
șiGeofence.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ă:
isSuccess()
- operația a fost realizată cu success sau cu eșec;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
.
- 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;
- 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:
- 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);
- se realizează navigarea către locația respectivă;
- 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())));
- se adaugă reperul pe harta Google;
- se adaugă reperul în lista de locații (
places
), notificându-se și adaptorul corespunzător obiectului de tipSpinner
(placesAdapter
) de această modificare, astfel încât acesta să fie actualizat corespunzător.
b) pentru ștergerea tuturor reperelor de pe hartă:
- se verifică să fie completate repere pe harta Google (în caz contrar generându-se un mesaj de eroare);
- se șterg toate reperele pe harta Google;
- se șterg toate reperele din lista de locații (
places
), notificându-se și adaptorul corespunzător obiectului de tipSpinner
(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.
- 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;
- 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
:
-
se apelează metoda
requestLocationUpdates()
din clasaFusedLocationProviderApi` LocationServices.FusedLocationApi.requestLocationUpdates( googleApiClient, locationRequest, this );
-
se actualizează starea serviciului de transmitere periodice a locației curente (
locationUpdatesStatus
); -
se vizualizează poziția curentă pe harta Google (se apelează metoda
setMyLocationEnabled(true)
); -
se modifică textul și culoarea butonului
locationUpdateStatusButton
; -
se navighează la locația curentă;
-
se dezactivează controalele grafice
latitudeEditText
,longitudeEditText
,navigateToLocationButton
.
b) metoda stopLocationUpdates()
din clasa GoogleMapsActivity
- se apelează metoda
removeLocationUpdates()
din clasaFusedLocationProviderApi
LocationServices.FusedLocationApi.removeLocationUpdates( googleApiClient, this );
- se actualizează starea serviciului de transmitere periodice a
locației curente (
locationUpdatesStatus
); - nu se vizualizează poziția curentă pe harta Google (se apelează
metoda
setMyLocationEnabled(false)
); - se modifică textul și culoarea butonului
locationUpdateStatusButton
; - 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()
.
- se instanțiază un obiect de tip
Geocoder
Geocoder geocoder = new Geocoder(this, Locale.getDefault());
- se obține lista de adrese prin invocarea metodei
getFromLocation()
care primește ca parametri:- latitudinea;
- longitudinea;
- numărul de adrese întoarse (
Constants.NUMBER_OF_ADDRESSES
);
- 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; - se parcurge lista de adrese: pentru fiecare adresă în parte se
concatenează rândurile distincte, concatenându-se toate rezultatele
obținute;
- numărul de linii dintr-o adresă este furnizat de metoda
getMaxAddressLineIndex()
; - un rând de la o anumită poziție se obține prin intermediul
metodei
getAddressLine()
;
- numărul de linii dintr-o adresă este furnizat de metoda
- 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:
-
când utilizatorul intră în zona de restricție geografică
-
când utilizatorul iese din zona de restricție geografică
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()
.
- 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);
- 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ă):GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE
GeofenceStatusCodes.GEOFENCE_TOO_MANY_GEOFENCES
GeofenceStatusCodes.GEOFENCE_TOO_MANY_PENDING_INTENTS
.
- se obține tipul de tranziție geografică
int geofenceTransition = geofencingEvent.getGeofenceTransition();
- se construiește un mesaj explicativ conținând
- tipul tranziției:
Geofence.GEOFENCE_TRANSITION_ENTER
/Geofence.GEOFENCE_TRANSITION_EXIT
; - identificatorii restricțiilor legate de poziționarea geografică:
- se obține lista tuturor restricțiilor legate de poziționarea
geografică
List<Geofence> triggeringGeofences = geofencingEvent.getTriggeringGeofences();
- se parcurge lista și pentru fiecare obiect se obține
identificatorul unic (
getRequestId()
);
- se obține lista tuturor restricțiilor legate de poziționarea
geografică
- tipul tranziției:
- 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:
- maximum 3 absențe la laborator
- 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)
- 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
adb over wifi
using CLI
-
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).