Laborator 04. Structura unei Aplicații (II)
Intenții
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.
În programarea Android, un principiu de bază este de folosi intenții pentru a propaga acțiuni, chiar și în cadrul aceleiași aplicații, în detrimentul încărcării clasei corespunzătoare. În acest fel, se asigură cuplarea slabă a componentelor, oferind flexibilitate în cazul înlocuirii acestora, permițând totodată extinderea funcționalității cu ușurință.
Structura unei Intenții
În structura unei intenții pot fi identificate mai multe elemente, precizate în cadrul secțiunii <intent-filter>
, prin intermediul cărora se declară faptul că o componentă a unei aplicații poate realiza o anumită acțiune pentru un anumit set de date (sau pe care un ascultător al unei intenții cu difuzare îl poate procesa):
- acțiunea (
action
) care trebuie executată este indicată prin proprietateaandroid:name
, cele mai frecvent utilizate valori fiind:MAIN
,VIEW
,DIAL
,CALL
,ANSWER
,SEND
,SENDTO
,INSERT
,EDIT
,DELETE
,SEARCH
,WEB_SEARCH
; fiecare filtru de intenție trebuie să indice cel puțin o acțiune; este recomandat să se utilizeze convențiile folosite în Java pentru identificarea pachetelor pentru denumirile acțiunilor;
data
din <intent-filter>
informații legate de categoria MIME ale datelor procesate (mimeType
), de locația la care se găsesc (path
, host
, port
), de schema utilizată (scheme
).
- categoria (
category
), indicată tot prin proprietateaandroid:name
oferă informații suplimentare cu privire la acțiunea care trebuie realizată; fiecare filtru de intenție poate specifica mai multe categorii, putând fi utilizate valori definite de utilizator sau valori preexistente în sistemul de operare Android:ALTERNATIVE
- acțiunea ar trebui să fie disponibilă ca o alternativă la activitatea implicită pentru tipul de date respectiv;SELECTED_ALTERNATIVE
- acțiunea poate fi selectată, dintr-o listă, ca o alternativă la activitatea implicită pentru tipul de date respectiv;BROWSABLE
- indică o acțiune disponibilă din cadrul navigatorului; pentru ca o activitate sau un serviciu să poată fi invocate din cadrul navigatorului trebuie să specifice în mod obligatoriu această categorie;DEFAULT
- utilizat pentru ca activitatea sau serviciul să fie utilizate în mod implicit pentru tipul de date specificat în filtrul de intenții; de asemenea, este necesar pentru activitățile sau serviciile care se doresc a fi lansate în execuție prin intermediul unor intenții explicite;HOME
- este folosit pentru a indica o alternativă la ecranul principal, dacă nu este indicată nici o acțiune;LAUNCHER
- atunci când este specificat, face ca activitatea să fie inclusă în meniul de aplicații care pot fi lansate în execuție direct de către utilizator, prin accesarea lor;- alte valori:
INFO
,PREFERENCE
,CAR_DOCK
,DESK_DOCK
,CAR_MODE
,APP_MAKET
.
- datele (
data
) reprezință informațiile care vor fi procesate, fiind exprimate de obicei sub forma unui URI, fie că este vorba despre un număr de telefon (prefixat detel:
), despre datele unei persoane din lista de contacte (prefixate decontent://contacts/people
), despre coordonate geografice (prefixate degeo:
) sau o adresă Internet (prefixată dehttp://www.
); pot fi specificate o serie de proprietăți (în orice combinație) pentru a indica datele suportate de componenta respectivă:android:host
- o adresă (accesibilă prin rețea - poate fi indicată denumirea sau adresa IP a gazdei) la care se găsesc datele ce pot fi procesate de componentă;android:mimeType
- tipul de date;android:path
- locația la care se găsesc datele;android:port
- portul pe care se permite conexiunea pentru accesarea datelor;android:scheme
- un protocol prin intermediul căruia pot fi accesate datele (spre exemplu,file
,http
,mailto
,content
,tel
).
- tipul (
type
) referă în mod explicit clasificarea MIME al datelor (deși aceasta poate fi dedus în mod automat din conținutul propriu-zis al datelor respective); - componenta (
component
) specifică în mod explicit denumirea unei clase care va fi invocată de intenție (deși aceasta putea fi dedusă în mod automat din denumirea acțiunii și categoria ei, datele și tipul lor);
- extra (
extra
) este un obiect de tipBundle
care conține informații suplimentare cu privire la componenta respectivă; informațiile conținute într-o intenție pot fi obținute folosindintent.getExtras()
, în timp ce specificarea unor informații care vor fi atașate la o intenție va fi realizată printr-un apel al metodeiintent.putExtras(Bundle)
.
Dintre aceste componente, esențiale sunt acțiunea și datele:
- acțiunea este transmisă ca parametru al constructorului clasei
Intent
; - datele sunt transmise prin intermediul metodei
setData()
(pentru acțiunile care necesită date); fiind vorba despre un URI, acesta va fi construit ca rezultat alUri.parse(…)
.
După ce a fost construit obiectul de tip Intent
prin specificarea acestor parametri, acțiunea poate fi executată prin transmiterea acestuia ca parametru al metodei startActivity()
sau startService()
, disponibile în clasa android.content.Context
.
startActivity(intent); |
startService(intent); |
Terminarea unei activități (moment în care se realizează revenirea la activitatea părinte) este realizată prin apelul metodei finish()
.
Controlul fluxului de activități prin intenții
Intenții construite prin precizarea clasei încărcate
În fișierul AndroidManifest.xml
, orice activitate definește în cadrul elementului <intent-filter>
, denumirea unei acțiuni care va putea fi folosită de o intenție pentru a o invoca.
<activity android:name="ro.pub.cs.systems.pdsd.lab04.MainActivity" android:label="@string/app_name" > <intent-filter> <action android:name="ro.pub.cs.systems.pdsd.lab04.intent.action.MainActivity" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
<pachet-aplicație>.intent.action.ACȚIUNE
.
<activity>
în fișierul AndroidManifest.xml
.
category
din <intent-filter>
valoarea android.intent.category.DEFAULT
.
Pentru ca o funcționalitatea expusă de o activitate să poată fi invocată (în mod anonim) și din contextul altor componente ale sistemului de operare Android, pentru tipul de acțiune și pentru tipurile de date precizate, în cadrul secțiunii <intent-filter>
trebuie precizat atributul android:label
(șir de caractere care conține o descriere a funcționalității implementate), indicându-se ca tip de categorie valorile ALTERNATIVE
, SELECTED_ALTERNATIVE
sau ambele.
O activitate este în principiu invocată de o intenție care poate fi identificată prin apelul metodei getIntent()
. Rezultatul acestei metode poate fi inclusiv null
, în cazul în care activitatea nu a fost pornită prin intermediul unei intenții.
Prin intermediul unei intenții, o aplicație poate invoca atât o activitate din cadrul său, cât și o activitate aparținând altei aplicații.
- în situația în care se apelează o activitate din cadrul aceleiași aplicații, se poate folosi folosi metoda
startActivity(new Intent(this, AnotherActivity.class));
- dacă se dorește rularea unei activități existente în cadrul altei aplicații, aceasta va trebui referită prin numele său complet, inclusiv denumirea pachetului care o identifică
startActivity(new Intent("ro.pub.cs.systems.pdsd.lab04.AnotherActivity"));
De remarcat faptul că în situația în care este pornită o activitate din cadrul aceleiași aplicații Android, obiectul de tip Intent
primește ca parametru și contextul curent (this
), în timp ce în cazul în care este lansată în execuție o activitate din cadrul altei aplicații Android acest parametru este omis.
În momentul în care este invocată metoda startActivity()
, activitatea respectivă este lansată în execuție (prin apelul metodelor onCreate()
, onStart()
, onResume()
) și plasată în vârful stivei care conține toate componentele care au rulate anterior, fără a fi fost terminate. În momentul în care se apelează metoda finish()
(sau se apasă butonul Back), activitatea este încheiată (prin apelul metodelor onPause()
, onStop()
, onDestroy()
), fiind scoasă din stivă, restaurându-se activitatea anterioară.
Intenții construite prin precizarea acțiunii
O intenție poate fi definită și prin intermediul unei acțiuni care se dorește a fi realizată, pentru care pot fi atașate opțional și anumite date. Utilizatorul care folosește un astfel de mecanism nu cunoaște activitatea (sau aplicația Android) care va fi lansată în execuție pentru realizarea acțiunii respective. Pentru a putea îndeplini o astfel de solicitare, sistemul de operare Android trebuie să identifice, la momentul rulării, activitatea care este cea mai adecvată pentru a rezolva acțiunea dorită. În acest fel, pot fi utilizate funcționalități deja implementate în cadrul sistemului de operare Android, fără a cunoaște în prealabil aplicația responsabilă de aceasta.
action
din intent-category
aceeași valoare care este transmisă ca parametru constructorului clasei Intent
, la execuția intenției în cauză utilizatorului i se va prezenta o listă de opțiuni dintre care poate alege. Dacă la realizarea selecției va fi precizată și opțiunea Use by default for this action
, preferințele vor fi salvate astfel încât în continuare vor fi utilizate fără a se mai solicita intervenția utilizatorului în acest sens.
Procesul de rezolvare a unei intenții (eng. intent resolution) se face prin intermediul analizei tuturor ascultătorilor înregistrați pentru a procesa mesaje cu difuzare (eng. broascast receivers).
Cele mai frecvent utilizate acțiuni implicite ale unui obiect de tip Intent
sunt:
- vizualizarea conținutului specificat în secțiunea
data
asociată intenției, sub forma unui URI, de către aplicații Android diferite, în funcție de schema (protocolul) utilizat (http
- navigator,tel
- aplicația pentru formarea unui număr de telefon,geo
- Google Maps,content
- aplicația pentru gestiunea contactelor din agenda telefonică):Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse("http://ocw.cs.pub.ro/pdsd"));
- căutarea unor informații pe Internet folosind un motor de căutare, termenul căutat fiind indicat în secțiunea
extra
asociată intenției, fiind identificată prin cheiaSearchManager.QUERY
:Intent intent = new Intent(Intent.ACTION_WEB_SEARCH); intent.setData(Uri.parse("http://www.google.ro"));
- invocarea aplicației Android pentru formarea unui număr de telefon (interfața grafică a aplicației Android poate fi populată deja cu numărul de telefon furnizat în secțiunea de date a intenției asociate, în cadrul unui URI); aplicația Android nativă poate normaliza majoritatea schemelor de numere de telefon (cod de țară, prefix, număr de telefon propriu-zis):
Intent intent = new Intent(Intent.ACTION_DIAL);
- invocarea aplicației pentru formarea unui număr de telefon și realizarea propriu-zisă a apelului telefonic, folosind valoarea furnizată în secțiunea de date a intenției asociate (în cadrul unui URI); se recomandă să fie folosită pentru activitățile care înlocuiesc aplicația Android nativă pentru formarea unui număr de telefon:
Intent intent = new Intent(Intent.ACTION_CALL); intent.setData(Uri.parse("tel:0214029466"));
AndroidManifest.xml
trebuie specificată explicit permisiunea pentru o astfel de acțiune<uses-permission android:name=“android.permission.CALL_PHONE”>
.
- vizualizarea unei locații pe hartă pentru care s-au specificat coordonatele GPS
Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse("geo:44.436877,26.048029?z=100&q=Education"));
- selectarea unei valori din cadrul furnizorului de conținut indicat în cadrul secțiunii de date asociate intenției, sub forma unui URI; de regulă, este lansată în execuție ca subacvititate, fiind necesar să furnizeze un URI către valoarea care a fost accesată, atunci când este terminată
Intent intent = new Intent(Intent.ACTION_PICK); intent.setData(Uri.parse("content://contacts/people"));
Alte acțiuni implicite utilizate sunt:
ACTION_ALL_APPS
- lansează în execuție o activitate care afișează toate aplicațiile Android instalate pe dispozitivul mobil; implicit, această acțiune este tratată de aplicația nativă care listează aplicațiile Android în meniul de aplicații din care pot fi rulate de utilizator prin accesarea pictogramei asociate;ACTION_ANSWER
- lansează în execuție o activitate care gestionează apelurile primite;ACTION_BUG_REPORT
- lansează în execuție o activitate prin intermediul căruia poate fi raportată funcționarea anormală a unei aplicații Android;ACTION_CALL_BUTTON
- lansează în execuție o activitate responsabilă cu formarea numărului de telefon; de regulă, o astfel de acțiune este generată în momentul în care utilizatorul accesează un buton dedicat;ACTION_DELETE
- lansează în execuție o activitate care permite ștergerea informațiilor specificate în secțiuneadata
asociată intenției, sub forma unui URI;ACTION_EDIT
- lansează în execuție o activitate care permite modificarea informațiilor specificate în secțiuneadata
asociată intenției, sub forma unui URI;ACTION_INSERT
- lansează în execuție o activitate care permite adăugarea unor informații în cursorul specificat în secțiunea de secțiuneadata
asociată intenției, sub forma unui URI (în cazul în care este rulată ca subactivitate, trebuie să furnizeze URI-ul informațiilor adăugate);ACTION_SEARCH
- lansează în execuție o activitate care implementează o activitate de căutare; termenul care este căutat trebuie să fie specificat în secțiuneaextra
a activității, fiind identificat prin cheiaSearchManager.QUERY
;ACTION_SEARCH_LONG_PRESS
- lansează în execuție o activitate care implementează o activitate de căutare, fiind generată în momentul în care este detectat un eveniment de tip apăsare prelungită a unui buton dedicat (implicit, lansează în execuție o activitate pentru a realiza o căutare vocală);ACTION_SENDTO
- lansează în execuție o activitate pentru a transmite anumite informații către un contact indicat în secțiuneadata
asociată intenției;ACTION_SEND
- lansează în execuție o activitate care transmite informațiile conținute în cadrul intenției către un contact care va fi selectat ulterior (aflat pe un alt dispozitiv mobil):- tipul MIME trebuie indicat prin intermediul metodei
setType()
; - informațiile propriu-zise trebnuie conținute în secțiunea
extra
asociată intenției, fiind identificate prin cheileEXTRA_TEXT
sauEXTRA_STREAM
, în funcție de tipul respectiv (pentru aplicațiile de poștă electronică sunt suportate și cheileEXTRA_EMAIL
,EXTRA_CC
,EXTRA_BCC
,EXTRA_SUBJECT
).
Totuși, un utilizator nu poate avea garanția că acțiunea pe care o transmite ca parametru al unei intenții va putea fi rezolvată, întrucât există posibilitatea să nu existe nici o activitate asociată acesteia sau ca aplicația ce ar fi putut să o execute să nu fie instalată în contextul sistemului de operare Android. Din acest motiv, o practică curentă este de a verifica dacă o acțiune poate fi rezolvată înainte de a apela metoda startActivity()
. Astfel, procesul de gestiune a pachetelor poate fi interogat (prin intermediul metodei resolveActivity()
) dacă există o activitate ce poate executa o acțiune și în caz afirmativ, care este aceasta.
Intent applicationIntent = new Intent(...); PackageManager packageManager = new PackageManager(); ComponentName componentName = applicationIntent .resolveActivity(packageManager); if (componentName == null) { Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://search?q=pname:...")); if (marketIntent.resolveActivity(packageManager) != null) { startActivity(marketIntent); } else { Toast.makeText(getApplicationContext(), "Google Play Store is not available", Toast.LENGTH_LONG).show(); } } else { startActivity(intent); }
În situația în care nu este identificată nici o activitate asociată acțiunii respective, utilizatorul poate dezactiva componenta grafică asociată până în momentul în care aceasta devine disponibilă, prin descărcarea aplicației Android corespunzătoare din Google Play Store.
Prin intermediul clasei PackageManager
poate fi obținută lista tuturor acțiunilor care pot fi realizate pentru un set de date, atașat unei intenții, invocându-se metoda queryIntentActivities()
:
Intent applicationIntent = new Intent(); intent.setData(...); intent.addCategory(Intent.CATEGORY_SELECTED_ALTERNATIVE); PackageManager packageManager = new PackageManager(); int flags = PackageManager.MATCH_DEFAULT_ONLY; List<ResolveInfo> availableActions = packageManager.queryIntentActivities(applicationIntent, flags); for (ResolveInfo availableAction: availableActions) { Log.d(Constants.TAG, "An available action for the data is "+getResources().getString(availableAction.labelRes)); }
Procesul de rezolvare a unei intenții pe baza unei acțiuni implică următoarele etape:
- se construiește o listă cu toate filtrele de intenții asociate componentelor din aplicațiile Android existente;
- sunt eliminate toate filtrele de intenții care nu corespund acțiunii sau categoriei intenției care se dorește a fi rezolvată:
- verificările în privința acțiunii sunt realizate numai în situația în care filtrul de intenție specifică o acțiune; sunt eliminate acele filtre de intenții pentru care nici una dintre acțiunile pe care le include nu corespund acțiunii intenției care se dorește a fi rezolvată;
- verificările în privința categorie sunt realizate numai în situația în care filtrul de intenție specifică o categorie sau în cazul în care nu specifică nici o categorie, dacă nici intenția care se dorește a fi rezolvată nu include nici o categorie; sunt eliminate acele filtre de intenții care nu includ toate categoriile pe care le conține și intenția care se dorește a fi rezolvată (putând conține totuși și categorii suplimentare);
- fiecare parte a URI-ului datelor corespunzătoare intenției care se dorește a fi rezolvată este comparată cu secțiunea
data
din filtrul de intenție; gazda, autoritatea, tipul MIME, calea, portul, schema), orice neconcordanță conducând la eliminarea acestuia din listă (în situația în care filtrul de intenții nu specifică proprietăți în secțiuneadata
, acesta va fi considerat compatibil cu intenția care se dorește a fi rezolvată; - în situația în care, ca urmare a acestui proces, există mai multe componente rămase în listă, utilizatorul va trebui să aleagă dintre toate aceste posibilități.
În cazul în care o componentă a unei activități este lansată în execuție prin intermediul unei intenții, aceasta trebuie să identifice acțiunea pe care trebuie să o realizeze și datele pe care trebuie sp le proceseze. În acest sens, clasa Intent
pune la dispoziție metodele getAction()
, getData()
și getExtras()
.
@Override protected void onCreate(Bundle state) { super.onCreate(state); setContentView(R.layout.activity_main); Intent intent = getIntent(); if (intent != null) { String action = intent.getAction(); Uri data = intent.getData(); Bundle extras = intent.getExtras(); } }
onNewIntent()
: @Override public void onNewIntent(Intent newIntent) { super.onNewIntent(newIntent); // ... }
startNextMatchingActivity()
: Intent intent = getIntent(); if (intent != null) { startNextMatchingActivity(intent); }
În acest mod, o componentă poate indica condiții suplimentare cu privire la tratarea unei anumite acțiuni, în situația în care acestea nu pot fi exprimate în cadrul filtrului de intenții, pentru a putea fi luate în considerare în cadrul procesului automat de identificare a componentei care deservește o intenție.
Intenții construite prin intermediul unui URI
De asemenea, un obiect de tip Intent
poate fi creat și prin intermediul unui URI care identifică în mod unic o anumită activitate:
Uri uri = Uri.parse("myprotocol://mynamespace/myactivity"); Intent intent = new Intent(Intent.ACTION_VIEW, uri); intent.putExtra("someKey", someValue); startActivity(intent);
Pentru a putea fi apelată folosind acest mecanism, activitatea va trebui să definească elementul data
în cadrul <intent-filter>
:
<activity android:name="ro.pub.cs.systems.pdsd.lab04.AnotherActivity" android:label="@string/app_name" > <intent-filter> <action android:name="ro.pub.cs.systems.pdsd.lab04.intent.action.AnotherActivity" /> <category android:name="android.intent.category.DEFAULT" /> <data android:scheme="myprotocol" android:host="mynamespace" /> </intent-filter> </activity>
De remarcat este faptul că în structura URI-ului, partea de după schemă://protocol/
poate conține orice șir de caractere, rolul său fiind strict acela de a respecta forma unui astfel de obiect (estetic), fără a influența în vreo formă funcționalitatea acestuia.
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()
.
Bundle
în momentul în care se apelează metoda putExtras()
, perechile (cheie, valoare) vor fi transferate din cadrul parametrului metodei în obiectul deja existent.
Construirea unui obiect de tip Bundle
care să fie transmis ca parametru al metodei putExtras()
poate fi evitată prin utilizarea metodei putExtra()
apelată pe obiectul Intent
, primind ca parametrii denumirea cheii prin care datele vor fi identificate și o valoare având un tip compatibil cu android.os.Parcelable
. Obținerea datelor se realizează apelând metoda pereche getExtra()
căreia i se transmite denumirea cheii ce identifică în mod unic informațiile respective. De asemenea, sunt implementate și metode specifice pentru fiecare tip de dată (put<type>Extra()
, respectiv get<type>Extra()
).
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.
La nivelul activității părinte, vor trebui implementate:
- metoda
startActivityForResult()
va primi ca parametrii obiectul de tipIntent
precum și un cod de cerere (de tip întreg), utilizat pentru a identifica în mod unic activitatea copil care a transmis un rezultat; - metoda
onActivityResult()
care va fi apelată în mod automat în momentul în care activitatea copil a fost terminată; parametrii pe care îi furnizează aceasta sunt:- codul de cerere (prin care se distinge între instanțe diferite ale activității copil);
- codul de rezultat transmis activității părinte (poate avea valorile
RESULT_OK
sauRESULT_CANCELED
); - un obiect
Intent
prin intermediul căruia pot fi furnizate date suplimentare.
final private static int ANOTHER_ACTIVITY_REQUEST_CODE = 2015; @Override protected void onCreate(Bundle state) { super.onCreate(state); setContentView(R.layout.activity_main); Intent intent = new Intent("ro.pub.cs.systems.pdsd.lab04.AnotherActivity"); intent.putExtra("ro.pub.cs.systems.pdsd.lab04.someKey", someValue); startActivityForResult(intent, ANOTHER_ACTIVITY_REQUEST_CODE); // start another activities with their own request codes } 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(); // process information from data ... } 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()
.
@Override protected void onCreate(Bundle state) { super.onCreate(state); setContentView(R.layout.activity_another); // intent from parent Intent intentFromParent = getIntent(); Bundle data = intentFromParent.getExtras(); // process information from data ... // intent to parent Intent intentToParent = new Intent(); intent.putExtra("ro.pub.cs.systems.pdsd.lab04.anotherKey", anotherValue); setResult(RESULT_OK, intentToParent); finish(); }
În cazul folosirii unor intenții în care activitățile sunt invocate prin intermediul unor URI-uri, datele vor putea fi concatenate direct în cadrul acestuia (fără a utiliza un obiect de tip Bundle
), restricția constând în faptul că pot fi utilizate numai șiruri de caractere:
- în activitatea părinte
Uri uri = Uri.parse("myprotocol://mynamespace/myactivity?someKey=someValue&..."); Intent intent = new Intent(Intent.ACTION_VIEW, uri); startActivity(intent);
- în activitatea copil
Uri uri = getIntent().getData(); String someValue = uri.getQueryParameter("someKey");
Gestiunea evenimentelor cu difuzare prin intermediul intențiilor
Intențiile reprezintă și un mecanism de comunicație inter-proces, asigurând transferul unor mesaje structurate. Astfel, intențiile pot fi distribuite către toate componentele de la nivelul sistemului de operare Android, pentru a notifica producerea unui eveniment (legat de starea dispozitivului mobil sau a unor aplicații), fiind procesate în cadrul unor obiecte ascultător dedicate tipului de mesaj respectiv.
LocalBroadcastManager
(a cărui instanță se obține prin intermediul metodei statice getInstance()
ce primește ca parametru contextul aplicației curente). Acesta operează într-un mod similar, implementând metodele sendBroadcast()
și registerReceiver()
. În plus, dispune de o metodă ce permite trimiterea sincronă a notificărilor, apelul acesteia fiind blocant până la momentul în care toți ascultătorii le-au primit.
Pentru o aplicație Android, în momentul rulării, pot fi activate / dezactivate oricare dintre componente (deci inclusiv ascultătorii pentru intențiile cu difuzare) prin intermediul metodei setComponentEnabledSetting()
din cadrul clasei PackageManager
. Un astfel de comportament este util pentru a optimiza performanțele aplicației atunci când o anumită funcționalitate nu este necesară.
PackageManager packageManager = getPackageManager(); ComponentName someEventBroadcastReceiver = new ComponentName(this, SomeEventBroadcastReceiver.class); packageManager.setComponentEnabledSetting(someEventBroadcastReceiver , PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP); packageManager.setComponentEnabledSetting(someEventBroadcastReceiver , PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
Trimiterea unei intenții cu difuzare
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.
final public static String SOME_ACTION = "ro.pub.cs.systems.pdsd.lab04.SomeAction.SOME_ACTION"; Intent intent = new Intent(SOME_ACTION); intent.putExtra("ro.pub.cs.systems.pdsd.lab04.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ă:- AndroidManifest.xml
<manifest ... > <application ... > <receiver android:name=".SomeEventBroadcastReceiver"> <intent-filter> <action android:name="ro.pub.cs.systems.pdsd.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); @Override protected void onResume() { super.onResume(); registerReceiver(someEventBroadcastReceiver, intentFilter); } @Override protected void onPause() { super.onPause(); unregisterReceiver(someEventBroadcastReceiver); }
onResume()
și de a-l deînregistra pe metoda onPause()
, astfel încât acesta să nu reacționeze decât atunci când activitatea este vizibilă.
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ă:
- SomeEventBroadcastReceiver.java
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) { // ... } }
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.
onReceive()
să se termine în maximum 5 secunde, în caz contrar fiind afișată o fereastră de dialog pentru a determina oprirea sa forțată.
Tipuri particulare de intenții cu difuzare
Există și tipuri particulare de intenții cu difuzare:
- intenții cu difuzare ordonate, utile în situația în care o intenție cu difuzare trebuie să fie procesată secvențial de mai mulți ascultători, fiecare dintre aceștia având posibilitatea de a modifica intenția respectivă;
- intenții cu difuzare persistente, care mențin valoarea care a fost transmisă cel mai recent.
Intenții cu difuzare ordonate
Pentru ca o intenție cu difuzare să poate fi procesată de mai mulți ascultători într-o anumită ordine, ar trebui să fie transmisă prin intermediul metodei sendOrderedBroadcast()
căreia i se poate atașa (opțional) și o anumită permisiune, pe care clasa ascultător trebuie să o dețină:
final public static String SOME_ORDERED_ACTION = "ro.pub.cs.systems.pdsd.lab04.SomeOrderedAction.SOME_ORDERED_ACTION"; Intent intent = new Intent(SOME_ORDERED_ACTION); intent.putExtra("ro.pub.cs.systems.pdsd.lab04.someKey", someValue); sendOrderedBroadcast(intent, "ro.pub.cs.systems.pdsd.lab04.SOME_PERMISSION");
Ordinea în care va fi procesată intenția de clasele ascultător înregistrate în acest sens este dată de prioritatea pe care acestea o precizează în filtrul de intenții, convenția fiind aceea că aceasta este direct proporțională cu valoarea (ascultătorii cu prioritate mai mare vor procesa intenția cu difuzare înaintea ascultătorilor cu prioritate mai mică):
- AndroidManifest.xml
<manifest ... > <application ... > <receiver android:name=".SomeEventOrderedBroadcastReceiver" android:permission="ro.pub.cs.systems.pdsd.lab04.SOME_PERMISSION"> <intent-filter android:permission="100"> <action android:name="ro.pub.cs.systems.pdsd.lab04.SomeOrderedAction.SOME_ORDERED_ACTION" /> </intent-filter> </receiver> </application> </manifest>
Frecvent, intențiile cu difuzare ordonate sunt folosite atunci când se dorește transmiterea unor rezultate înapoi, către aplicația Android care le-a transmis, după ce toți ascultătorii au realizat procesarea acestora. În acest sens, metoda sendOrderedBroadcast()
va primi suplimentar ca parametrii și obiectul de tip BroadcastReceiver
care va procesa ultimul intenția cu difuzare, un obiect de tip Handler
care va primi rezultatul final (se transmite valoarea null
dacă se dorește ca acesta să fie aplicația care a transmis intenția), rezultatul, secțiunile data
/ extra
ale intenției pe care se poate opera.
Intenții cu difuzare persistente
Pentru ca o intenție cu difuzare să își mențină cea mai recentă valoare, ar trebui să fie transmisă prin intermediul metodei sendStickyBroadcast()
.
final public static String SOME_STICKY_ACTION = "ro.pub.cs.systems.pdsd.lab04.SomeStickyAction.SOME_STICKY_ACTION"; Intent intent = new Intent(SOME_STICKY_ACTION); intent.putExtra("ro.pub.cs.systems.pdsd.lab04.someKey", someValue); sendStickyBroadcast(intent);
AndroidManifest.xml
trebuie specificată explicit permisiunea pentru o astfel de acțiune<uses-permission android:name=“android.permission.BROADCAST_STICKY”>
.
În momentul în care menținerea stării nu mai este necesară, se poate apela metoda removeStickyBroadcast()
.
Obținerea celei mai recente valori a intenției cu difuzare poate fi obținută chiar fără a preciza un obiect ascultător, doar prin indicarea filtrului de intenții:
IntentFilter intentFilter = new IntentFilter(SOME_STICKY_ACTION); Intent intent = registerReceiver(null, intentFilter);
Gestiunea intențiilor cu difuzare native
Cele mai multe servicii de sistem transmit intenții cu difuzare pentru a semnala faptul că s-au produs anumite modificări la nivelul stării dispozitivului mobil sau al aplicațiilor (primirea unui apel telefonic / mesaj, schimbarea nivelului de încărcare al bateriei, conectivitatea la Internet).
ACȚIUNE | DESCRIERE |
---|---|
ACTION_BATTERY_CHANGED | acțiune transmisă în momentul în care se modifică nivelul de încărcare al bateriei; starea bateriei este disponibilă în secțiunea extra , prin intermediul cheii EXTRA_STATUS , putând avea valorile:♦ BatteryManager.BATTERY_STATUS_CHARGING ♦ BatteryManager.BATTERY_STATUS_FULL |
ACTION_BATTERY_LOW | acțiune transmisă în momentul în care nivelul de încărcare al bateriei este scăzut, impunându-se încărcarea acesteia |
ACTION_BATTERY_OKAY | acțiune transmisă în momentul în care nivelul de încărcare al bateriei este acceptabil |
ACTION_BATTERY_CONNECTED | acțiune transmisă în momentul în care bateria este conectată la o sursă de energie externă |
ACTION_BATTERY_DISCONNECTED | acțiune transmisă în momentul în care bateria este deconectată de la o sursă de energie externă |
ACTION_BOOT_COMPLETED | acțiune transmisă în momentul în care a fost realizată complet secvența de pornire a dispozitivului mobil (aplicația poate primi o astfel de intenție cu difuzare dacă deține permisiunea RECEIVE_BOOT_COMPLETED ) |
ACTION_CAMERA_BUTTON | acțiune transmisă în momentul în momentul în care este accesat butonul pentru pornirea camerei foto |
ACTION_DATE_CHANGED / ACTION_TIME_CHANGED | acțiuni transmise în momentul în care data calendaristică sau timpul sunt modificate manual (nu datorită progresului său natural) |
ACTION_DOCK_EVENT | acțiune transmisă în momentul în care dispozitivul mobil este ancorat, printr-un dispozitiv de birou sau de mașină, stare plasată în secțiunea extra prin intermediul cheii ETRA_DOCK_STATE |
ACTION_MEDIA_EJECT | acțiune transmisă în momentul în care este îndepărtat un mediu de stocare extern (util în situația în care aplicația scrie / citește de pe acesta, pentru a salva conținutul și pentru a le închide) |
ACTION_MEDIA_MOUNTED / ACTION_MEDIA_UNMOUNTED | acțiuni transmise de fiecare dată când dispozitive de stocare externe sunt adăugate sau îndepărtate cu succes |
ACTION_NEW_OUTGOING_CALL | acțiune transmisă în momentul în care urmează să fie format un număr de telefon, a cărui valoare este plasată în secțiunea extra , prin intermediul cheii EXTRA_PHONE_NUMBER (aplicația poate primi o astfel de intenție cu difuzare dacă deține permisiunea PROCESS_OUTGOING_CALLS |
ACTION_SCREEN_OFF / ACTION_SCREEN_ON | acțiuni transmise în momentul în care ecranul este închis, respectiv este deschis |
ACTION_TIMEZONE_CHANGED | acțiune transmisă în momentul în care zona de timp a telefonului este modificată, a cărui valoare (identificator) este plasată în secțiunea extra prin intermediul cheii time-zone |
În cazul unei aplicații Android, foarte importante sunt și modificările în privința conectivității la Internet (inclusiv parametrii precum lățimea de bandă, latența) întrucât acestea pot fi semnificative în privința luării unor decizii legate de realizarea anumitor actualizări sau de descărcarea unor fișiere având dimensiuni mari. O astfel de funcționalitate poate fi definită prin implementarea unui obiect ascultător, care procesează acțiunea android.net.conn.CONNECTIVITY_CHANGE
(ConnectivityManager.CONNECTIVITY_ACTION
). Se transmise o intenție cu difuzie nepersistentă care nu conține informații suplimentare cu privire la schimbarea stării.
ConnectivityManager connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); boolean isConnected = networkInfo.isConnectedOrConnecting(); boolean isMobile = (networkInfo.getType() == ConnectivityManager.TYPE_MOBILE);
Fragmente
Un fragment este o componentă Android atomică, încapsulând o interfață grafică ce poate fi reutilizată, definind un ciclu de viață propriu. O activitate poate fi formată din unul sau mai multe fragmente, în funcție de cerințele aplicației la un moment dat de timp.
Deși reprezintă un modul independent, un fragment este string legat de activitatea din care face parte. Un fragment poate fi (re)utilizat în cadrul mai multor activități, putând fi plasat în numeroase moduri în funcție de capabilitățile ecranului dispozitivului mobil (tip, dimensiune, densitate, rezoluție). Operațiile de atașare și detașare a unui fragment la o activitate se realizează de regulă programatic, în cadrul codului sursă.
Fragmentele au fost introduse în Android începând cu nivelul de API 11 (Honeycomb), odată cu apariția tabletelor dotate cu un astfel de sistem de operare. Ulterior, au fost incluse și în bibliotecile de suport, astfel încât această funcționalitate să fie disponibilă pentru toate dispozitivele mobile echipate cu un sistem de operare având minim versiunea 1.6.
Componentele unui Fragment
Un fragment este o clasă derivată din android.app.Fragment
, care:
- definește o interfața grafică (opțional);
- implementează o anumită funcționalitate.
Pentru a construi interfața grafică corespunzătoare unui fragment, în codul sursă se va suprascrie metoda onCreateView()
ce utilizează:
- un obiect de tip
LayoutInflator
pentru a încărca fișierul XML; - un obiect de tip
ViewGroup
ce referă containerul din care face parte fragmentul (de obicei, acesta aparține activității părinte); - un obiect de tip
Bundle
utilizat pentru restaurarea stării în situația fragmentul este reluat (după ce anterior a fost întrerupt temporar).
- SomeFragment.java
package ro.pub.cs.systems.pdsd.lab04; import android.app.Fragment; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; public class SomeFragment extends Fragment { @Override public View onCreateView(LayoutInflater layoutInflater, ViewGroup container, Bundle state) { return layoutInflater.inflate(R.layout.some_fragment, container, false); } }
Se observă că metoda inflate()
din clasa LayoutInflater
primește următorii parametrii:
- un întreg, reprezentând identificatorul către o resursă de tip mecanism de control al conținutului;
- un obiect de tip
android.view.ViewGroup
; - o valoare de tip adevărat/fals, care indică dacă parametrul anterior este folosit ca părinte pentru interfața grafică încărcată sau dacă acesta este utilizat numai pentru a-i prelua parametrii de tip
LayoutParams
, spre a afișa în mod corect componentele conținute.
Pentru fiecare fragment trebuie să existe în directorul /res/layout
un fișier XML corespunzător, descriind interfața grafică similar cu modul în care se realiza acest lucru în cadrul unei activități (exceptând atributul tools:context
din cadrul elementului rădăcină de tip layout):
- some_fragment.xml
<?xml version="1.0" encoding="UTF-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <ImageView android:id="@+id/some_image_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/some_image" /> <TextView android:id="@+id/some_text_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/some_image_description" /> </LinearLayout>
De asemenea, există posibilitatea de a defini o interfață grafică programatic, în codul sursă, folosind un obiect de tipul android.view.ViewGroup
, însă o astfel de abordare nu este recomandată, nefiind scalabilă.
În fișierul AndroidManifest.xml
NU este necesar să existe nici o referință la fragmente, întrucât existența acestora este limitată la activitatea în care sunt conținute.
Ciclul de Viață al unui Fragment
Ca și în cazul unei activități, ciclul de viață al unui fragment poate fi controlat prin intermediul unor metode care sunt apelate în mod automat. În cazul în care sunt suprascrise, acestea trebuie să apeleze în mod necesar metodele din clasa părinte. Pe lângă metodele apelate în momentul în care fragmentul este creat, pornit, reluat, întrerupt, oprit și distrus, există și metode specifice care semnalizează atașarea și detașarea sa în cadrul unei activități, crearea și distrugerea ierarhiei de controale grafice precum și crearea activității părinte.
- la creare sunt apelate metodele:
onAttach()
,onCreate()
,onCreateView()
șionActivityCreated()
- în momentul în care devine vizibil, interacționând cu utilizatorul, sunt apelate
onStart()
șionResume()
- în momentul în care nu mai este vizibil, nemaiinteracționând cu utilizatorul, sunt apelate
onPause()
șionStop()
- la distrugere sunt apelate metodele
onDestroyView()
,onDestroy()
șionDetach()
Unele dintre aceste metode sunt aceleași ca în cazul activităților, având aceeași funcționalitate, în timp ce altele sunt specifice:
onAttach(Activity)
- apelată în momentul în care fragmentul este atașat la activitate; de regulă, în această metodă se reține referința către activitate;onCreateView(LayoutInflator, ViewGroup, Bundle)
- apelată pentru a crea interfața grafică a fragmentului, fie prin încărcarea din fișierul XML corespunzător (folosind metodainflate()
din clasaLayoutInflater
), fie prin construirea programatică; interfața grafică (obiect de tipandroid.view.View
) trebuie întoarsă ca rezultat al acestei metode; pentru fragmentele care nu dețin o interfață grafică se întoarcenull
;onActivityCreated(Bundle)
- apelată în momentul în care s-a terminat metodaonCreate()
a activității care conține fragmentul; în cadrul acestei metode este finalizat procesul de inițializare al fragmentului, fiind executate operații pentru care este necesar ca activitatea să fie creată;onDestroyView()
- apelată pentru a elibera resursele aferente controalelor din cadrul fragmentului, atunci când interfața grafică este descărcată;onDetach()
- apelată în momentul în care fragmentul este detașat de la activitate
onCreate()
, onCreateView()
, respectiv onActivityCreated()
.
Se observă că ciclul de viață al unui fragment se desfășoară între momentele în care acesta este atașat la activitatea din care face parte, respectiv este detașat din cadrul acesteia, metodele corespunzătoare fiind onAttach()
și onDetach()
.
Metoda onAttach()
este apelată înainte de a se crea interfața grafică a fragmentului, fragmentul însuși și chiar activitatea din care acesta face parte. În cadrul său, de regulă se obține o referință către activitate pentru a se realiza alte operații de inițializare.
Se consideră că un fragment există între metodele onCreate()
și onDestroy()
.
În cadrul metodei onCreate()
se recomandă să fie instanțiate orice obiecte care vor fi utilizate în cadrul fragmentului. Tot aici sunt realizate și alte operații de inițializare.
onCreate()
.
În cazul unui fragment, interfața grafică este construită, respectiv distrusă în cadrul metodelor onCreateView()
și onDestroyView()
.
În cadrul metodei onCreateView()
se recomandă să se încarce interfața grafică din fișierul XML asociat (respectiv să se creeze programatic), obținându-se referințe către controalele grafice respective și asociindu-li-se obiecte de tip ascultător pentru diferite tipuri de evenimente din cadrul interacțiunii cu utilizatorul. Tot acum pot fi pornite diferite servicii sau cronometre.
onCreateView()
să întoarcă un obiect de tip android.view.View
reprezentând interfața grafică a fragmentului respectiv.
În cazul în care este necesar ca fragmentul să acceseze obiecte din interfața grafică a activității din care face parte, astfel de operații nu pot fi realizate decât după ce a fost apelată metoda onActivityCreated()
, semnificând faptul că procesul de inițializare al acestuia a fost terminat. În caz contrar, este destul de probabil să se genereze o excepție de tipul NullPointerException
.
Stările unui fragment sunt strâns legate de cele ale activității din care face parte. În plus, un fragment poate fi atașat sau detașat în mod dinamic unei activități, astfel încât acesta poate parcurge stările de creat, vizibil și activ de mai multe ori în timpul ciclului de viață al containerului din care face parte.
Gestiunea tranzițiilor între diferitele stări din cadrul ciclului de viață al unui fragment este foarte importantă pentru a se asigura o experiență adecvată a utilizatorilor. Diferitele treceri între stările de inactivitate și activitate ale fragmentului trebuie să se realizeze în mod transparent pentru utilizatorii aplicației Android. De aceea, este foarte important ca să se asigure persistența controalelor grafice și a celorlalte date, salvându-se starea atunci când fragmentul este întrerupt temporar și oprit, restaurarea fiind realizată atunci când acesta este (re)pornit și reluat.
- SomeFragment.java
package ro.pub.cs.systems.pdsd.lab04; import android.app.Activity; import android.app.Fragment; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; public class SomeFragment extends Fragment { @Override public void onAttach(Activity activity) { super.onAttach(activity); // ... } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // ... } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.some_fragment, container, false); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); // ... } @Override public void onStart() { super.onStart(); // ... } @Override public void onResume() { super.onResume(); // ... } @Override public void onPause(){ super.onPause(); // ... } @Override public void onSaveInstanceState(Bundle savedInstanceState) { super.onSaveInstanceState(savedInstanceState); // ... } @Override public void onStop() { super.onStop(); // ... } @Override public void onDestroyView() { super.onDestroyView(); // ... } @Override public void onDestroy() { super.onDestroy(); // ... } @Override public void onDetach() { super.onDetach(); // ... } }
Gestiunea Fragmentelor din cadrul unei Activități
Fiecare activitate dispune de un proces de gestiune a fragmentelor conține, acesta fiind o instanță a clasei FragmentManager
, putând fi obținut din cadrul contextului asociat și punând la dispoziția utilizatorilor metode pentru adăugarea și scoaterea, respectiv înlocuirea unui fragment din cadrul său.
FragmentManager fragmentManager = getFragmentManager();
Adăugarea unui Fragment
Operația de adăugare a unui fragment poate fi realizată:
- static, în cadrul fișierului XML, prin intermediul elementului
<fragment>
, în situația în care sunt definite interfețe grafice pentru fiecare dintre tipurile de suprafețe de afișare suportate;<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android: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=".MainActivity" > <fragment android:name="ro.pub.cs.systems.pdsd.labo04.Fragment1" android:id="@+id/fragment1" android:layout_weight="1" android:layout_width="match_parent" android:layout_height="wrap_content" /> <fragment android:name="ro.pub.cs.systems.pdsd.labo04.Fragment2" android:id="@+id/fragment2" android:layout_weight="1" android:layout_width="match_parent" android:layout_height="wrap_content" /> </LinearLayout>
- dinamic (programatic), în cadrul codului sursă, prin definirea unor containere (obiecte de tip
FrameLayout
, care pot conține un sigur element vizibil) în cadrul interfețele grafice în care pot fi plasate fragmentele la diferite momente de timp.<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android: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=".MainActivity" > <FrameLayout android:id="@+id/frame1" android:layout_weight="1" android:layout_width="match_parent" android:layout_height="wrap_content" /> <FrameLayout android:id="@+id/frame2" android:layout_weight="1" android:layout_width="match_parent" android:layout_height="wrap_content" /> </LinearLayout>
În metoda
onCreate()
corespunzătoare activității, se creează un obiect de tipFragmentManager
pentru care se definesc operațiile care trebuie realizate (atomic) în cadrul unei tranzacții (obiectul de tipFragmentTransaction
fiind obținut prin invocarea metodeibeginTransaction()
). O tranzacție poate specifica suplimentar animațiile asociate unei tranziții prcum și dacă se dorește ca tranzacție să fie stocată și pe stivă. Realizarea propriu-zisă a operațiilor indicate se realizează doar atunci când se apelează metodacommit()
.
Pentru adăugarea unui fragment în cadrul unei activități, trebuie să se precizeze, ca parametrii ai metodeiadd()
din clasaFragmentTransaction
identificatorul containerului în care va fi plasat fragmentul, obiectul de tip fragment și (opțional) o etichetă (de tip șir de caractere) prin intermediul căreia acesta poate fi ulterior referit.@Override protected void onCreate(Bundle savedInstanceState) { FragmentManager fragmentManager = getFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); fragmentTransaction.add(android.R.id.frame1, new Fragment1()); fragmentTransaction.add(android.R.id.frame2, new Fragment2(), Constants.FRAGMENT2_TAG); fragmentTransaction.commit(); }
De remarcat faptul că referințele
R.id.frame1
șiR.id.frame2
se referă la conținutul activității.
Pentru a asigura o experiență a utilizatorului consistentă, sistemul de operare Android asigură în mod automat persistența mecanismului de dispunere a framentelor și stiva asociată atunci când activitatea părinte este distrusă și (re)creată din cauza unei modificări a configurației. Din acest motiv, se recomandă ca toate interfețele grafice corespunzătoare diferitelor configurații să conțină toate containerele implicate în tranzacții de fragmente. Altfel, atunci când se va încerca restaurarea unor fragmente (în containere care nu exista în configurația respectivă), se pot produce anumite excepții. Dacă nu se dorește ca un container să fie afișat în cadrul unei interfețe grafice aferente unei configurații, se poate folosi atributul visibility
.
<LinearLayout ... > <FrameLayout android:id=@+id/frame1 ... /> <FrameLayout android:id=@+id/frame2 android:visibility="gone" ... /> </LinearLayout> |
FrameLayout frame1 = (FrameLayout)findViewById(R.id.frame1); FrameLayout frame2 = (FrameLayout)findViewById(R.id.frame2); frame2.setVisibility(Visibility.GONE); |
Scoaterea unui Fragment
Pentru scoaterea unui fragment din cadrul unei activități, trebuie ca anterior să se obțină o referință către acesta, pe baza identificatorului (metoda findFragmentById()
), respectiv pe baza etichetei (metoda findFragmentByTag()
). Ulterior, se apelează metoda remove()
din clasa FragmentTransaction
care primește ca parametru această referință.
FragmentManager fragmentManager = getFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); Fragment fragment1 = fragmentManager.findFragmentById(R.id.fragment1); Fragment fragment2 = fragmentManager.findFragmentByTag(Constants.FRAGMENT2_TAG); fragmentTransaction.remove(fragment1); fragmentTransaction.remove(fragment2); fragmentTransaction.commit();
Metoda findViewById()
poate primi ca parametru și identificatorul containerului care conține fragmentul. În cazul în care aceasta nu are nici un conținut, rezultatul metodei este null
.
Înlocuirea unui Fragment
Pentru înlocuirea unui fragment din cadrul unei activități, trebuie să se precizeze, ca parametrii ai metodei replace()
din clasa FragmentTransaction
identificatorul containerului în care va fi plasat fragmentul, obiectul de tip fragment și (opțional) o etichetă (de tip șir de caractere) prin intermediul căreia acesta poate fi ulterior referit.
FragmentManager fragmentManager = getFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); Fragment fragment2 = new Fragment2(); fragmentTransaction.replace(R.id.frame1, fragment2, Constants.FRAGMENT2_TAG); fragmentTransaction.commit();
Utilizarea Stivei de Fragmente
Prin intermediul fragmentelor, se oferă posibilitatea de a se crea interfețe grafice asociate activităților în mod dinamic. Unele modificări între diferite stări pot fi considerate de utilizatori ca fiind ecrane noi, comportamentul așteptat fiind acela de restaurare a acestora atunci când se accesează butonul Back. Un astfel de comportament presupune realizarea unei tranzacții de fragmente în sens invers. Această funcționalitate poate fi obținută prin utilizarea unei stive de fragmente, în care o tranzacție poate fi identificată prin intermediul unei etichete.
În acest sens, va trebui apelată explicit metoda addToBackStack()
pentru obiectul de tip FragmentTransaction
care se ocupă de atașarea și detașarea fragmentelor de la activitate, înainte de a se apela metoda commit()
.
FragmentManager fragmentManager = getFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); fragmentTransaction.add(R.id.frame1, new Fragment1(), Constants.FRAGMENT1_TAG); Fragment fragment2 = fragmentManager.findFragmentByTag(Constants.FRAGMENT2_TAG); fragmentTransaction.remove(fragment2); fragmentTransaction.addToBackStack(null); fragmentTransaction.commit();
Astfel, atunci când se apelează metoda commit()
, fragmentul 2 este oprit și salvat pe stiva de fragmente (în loc să fie distrus). Când utilizatorul accesează butonul Back fragmentul 1 este distrus și fragemtnul 2 este restaurat de pe stivă și (re)pornit.
Folosirea de Animații la Tranziția dintre Fragmente
Tranzițiile între diferitele stări ale activității care implică operații cu fragmente pot fi realizate:
- folosind metoda
setTransition()
a unui obiect de tipFragmentTransaction
, utilizând oricare dintre variantele predefinite, de tipulFragmentTransaction.TRANSIT_FRAGMENT_…
fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_...);
- folosind metoda
setCustomAnimation()
a unui obiect de tipFragmentTransaction
, pentru utilizarea de animații definite de utilizator; metoda primește două argumente, indicând referințele către fișiere XML care descriu animațiile folosite pentru fragmentele care sunt adăugate, respectiv pentru fragmentele care sunt scoasefragmentTransaction.setCustomAnimations(R.animator.fade_in, R.animator.fade_out);
Interacțiunea dintre fragmente
Elementele grafice dintr-un fragment al unei activități pot fi accesate și din alt fragment al activității, prin obținerea unei referințe către activitatea părinte (folosind metoda getActivity()
) și accesarea controlului respectiv prin intermediul identificatorului său din cadrul fragmentului (folosind metoda findViewById()
).
O astfel de abordare (folosind obiectul de tip Activity
ca intermediar al comunicației) este recomandată pentru a asigura cuplarea slabă și autonomia fragmentelor. Altfel, este permisă comunicația directă prin intermediul obiectului de tip FragmentManager
asociat activității.
În cazul evenimentelor produse la nivelul unui fragment, decizia cu privire la impactul pe care acesta îl are asupra interfeței grafice trebuie să aparțină activității. Astfel, fragmentul va invoca o metodă de callback la nivelul activității care va determina modul în care va fi tratat evenimentul.
public interface OnEventProducedListener { public void onEventProduced(Event event); };
public class SomeFragment extends Fragment { OnEventProducedListener onEventProducedListener; @Override public void onAttach(Activity activity) { super.onAttach(activity); try { onEventProducedListener = (OnEventProducedListener)activity; } catch (ClassCastException classCastException) { Log.e(Constants.TAG, "Parent activity does not implement OnEventProducedListener!"); } } private void onEventProduced(Event event) { onEventProducedListener.onEventProduced(event); } }
public class SomeActivity extends Activity implements OnEventProducedListener { @Override protected void onCreate(Bundle savedStateInstance) { super.onCreate(savedInstanceState); FragmentManager framentManager = new FragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); fragmentTransaction.add(R.id.frame, new SomeFragment(), Constants.SOME_FRAGMENT_TAG); fragmentTransaction.commit(); // ... } @Override public void onEventProduced(Event event) { // ... } }
Servicii
În Android, clasa Service
este utilizată pentru componente a căror funcționalitate implică procesări complexe, de lungă durată, necesitând anumite resurse, fără a fi necesar să pună la dispoziție o interfață grafică sau un mecanism de interacțiune cu utilizatorul. Prin intermediul unui serviciu, se asigură faptul că aplicația Android continuă să se găsească în execuție, chiar și atunci când interfața grafică a acesteia nu este vizibilă.
Astfel, 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).
Un serviciu este de regulă rulat pe firul de execuție principal al aplicației Android. De aceea, în cazul în care operațiile pe care le realizează influențează experiența utilizatorului, acestea trebuie transferate pe alte fire de execuție din fundal (folosind clasele Thread
și AsyncTask
).
Gestiunea unui Serviciu
Un serviciu este o clasă derivată din android.app.Service
, implementând metodele:
onCreate()
- realizând operațiile asociate construirii serviciului respectiv;onBind()
- asigurând asocierea serviciului la o altă componentă a aplicației Android; metoda primește un parametru de tipIntent
, reprezentând intenția prin intermediul căruia a fost lansat în execuție.
- SomeService.java
import android.app.Service; import android.content.Intent; import android.os.IBinder; public class SomeService extends Service { @Override public void onCreate() { super.onCreate(); // ... } @Override public IBinder onBind(Intent intent) { // ... return null; } }
Orice serviciu trebuie să fie înregistrat î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.
- AndroidManifest.xml
<manifest ...> <application ...> <service android:name="ro.pub.cs.systems.pdsd.lab04.SomeService" android:enabled="true" android:permission="ro.pub.cs.systems.pdsd.lab04.SOME_SERVICE_PERMISSION" /> </application> </manifest>
În momentul în care este pornit (printr-un apel al metodei startService()
din cadrul altei componente), un serviciu apelează în mod automat metoda onStartCommand()
. În cadrul acestei metode trebuie realizată procesarea pe care o presupune serviciul respectiv. Având în vedere faptul că această metodă poate fi apelată de mai multe ori pe parcursul ciclului de viață al unui serviciu, tot aici trebuie implementa și comportamentul în cazul în care acesta este repornit. Metoda primește ca parametrii:
- intenția care a invocat serviciul;
- anumite valori prin care poate fi semnalat modul în care a fost pornit:
START_FLAG_REDELIVERY
- serviciul a fost repornit ca urmare a distrugerii sale de către sistemul de operare înainte de se fi terminat corespunzător;START_FLAG_RETRY
- serviciul a fost repornit după o execuție anormală;
- un identificator unic prin care se poate face distincția între apeluri diferite ale aceluiași serviciu.
@Override public int onStartCommand(Intent intent, int flags, int startId) { processInBackground(intent, startId); return Service.START_STICKY; }
Comportamentul serviciului în situația în care este repornit poate fi controlată prin intermediul valorii întregi care este furnizată ca rezultat al metodei onStartCommand()
:
Service.START_STICKY
- mecanism standard, folosit de serviciile care își gestionează propriile stări și care sunt pornite și oprite în funcție de necesități (prin intermediul metodelorstartService()
șistopService()
); prin aceasta, se indică faptul că metodaonStartCommand()
va fi invocată de fiecare dată când serviciul este (re)pornit după ce a fost distrus de sistemul de operare Android (situație în care parametrul de tipIntent
va avea valoareanull
);Service.START_NOT_STICKY
- mecanism utilizat de serviciile utilizate pentru a procesa anumite comenzi, care se opresc singure (printr-un apel al metodeistopSelf()
) atunci când operațiile pe care trebuiau să le realizeze s-au terminat; serviciul este (re)pornit după ce a fost distrus de sistemul de operare Android numai în situația în care între timp au mai fost realizate apeluri ale metodeistartService()
; un astfel de comportament este adecvat pentru procese care sunt realizate periodic;Service.START_REDELIVER_INTENT
- mecanism utilizat atunci când se dorește să se asigure faptul că procesările asociate serviciului au fost terminate; în situația în care serviciul a fost distrus de sistemul de operare Android, este (re)pornit numai în situația în care între timp au fost realizate apeluri ale metodeistartService()
sau procesul a fost oprit ca acesta să invoce metodastopSelf()
- metodaonStartCommand()
va fi apelată folosind ca parametru intenția originală, a cărei procesări nu a fost terminată corespunzător.
În toate aceste cazuri, oprirea serviciului trebuie realizată explicit, prin apelul metodelor:
stopService()
din contextul componentei care l-a pornit;stopSelf()
din contextul serviciului.
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 o acțiune înregistrată ca fiind tratată de serviciul respectiv.
Intent intent = new Intent(SomeService.SOME_SERVICE); 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>)
.
Oprirea unui Serviciu
Un serviciu poate fi oprit:
- de componenta care l-a pornit, printr-un apel al metodei
stopService()
, ce primește ca parametru un obiect de tipIntent
care poate fi creat explicit sau implicit;
startService()
nu sunt imbricate, invocarea metodei stopService()
oprește numai serviciul corespunzător (dacă se află în execuție).
- chiar de el însuși, în momentul în care procesările pe care trebuie să le realizeze s-au terminat, printr-un apel al metodei
stopSelf()
, eliberând resursele pe care sistemul de operare le-ar fi folosit pentru a-l menține în execuție; metoda poate fi apelată:- fără parametri, pentru a forța oprirea imediată;
- transmițând un parametru de tip întreg, reprezentând identificatorul instanței care rulează, pentru a se asigura faptul că procesarea a fost realizată pentru fiecare apel care a fost realizat.
Asocierea unui Serviciu la o Activitate
Un serviciu poate fi atașat la o activitate, astfel încât activitatea menține o referință către serviciu, prin intermediul căreia poate invoca metode așa cum ar face cu orice alt obiect. Un astfel de comportament este util pentru activitățile care necesită interfațarea cu un serviciu.
Acestă funcționalitate este realizată prin intermediul metodei onBind()
care furnizează o referință către serviciu sub forma unui obiect de tipul IBinder
.
- SomeService.java
import android.app.Service; import android.content.Intent; import android.os.IBinder; public class SomeService extends Service { final private IBinder binder = new SomeBinder(); public class SomeBinder extends Binder { SomeService getService() { return SomeService.this; } } @Override public void onCreate() { super.onCreate(); // ... } @Override public IBinder onBind(Intent intent) { return binder; } }
Legătura dintre serviciu și activitate este reprezentată sub forma unui obiect de tipul ServiceConnection
, a cărui implementare presupune definirea metodelor onServiceConnected()
, respectiv onServiceDisconnected()
. Asocierea propriu-zisă dintre serviciu și activitate este realizată de metoda bindService()
care primește ca parametri:
- intenția (creată explicit sau implicit) reprezentând serviciul care se asociază activității;
- un obiect de tip
ServiceConnection
; - anumite valori prin care se controlează modul în care este făcută asocierea între cele două componente ale aplicației Android:
Context.BIND_AUTO_CREATE
- serviciul trebuie creat în momentul în care se realizează asocierea;Context.BIND_ADJUST_WITH_ACTIVITY
- modifică prioritatea serviciului în funcție de importanța activității de care este legat (mai mare atunci când este activă, mai mică atunci când este inactivă);Context.BIND_ABOVE_CLIENT
/Context.BIND_IMPORTANT
- specifică faptul că prioritatea serviciului este mai mare decât a activității asociate;Context.BIND_NOT_FOREGROUND
- asigură faptul că serviciul atașat activității nu va avea niciodată prioritatea necesară pentru a rula pe firul de execuție principal;Context.BIND_WAIVE_PRIORITY
- face ca prioritatea serviciului asociat să nu fie modificată (implicit, prioritatea relativă a serviciului este mărită).
private SomeService someServiceReferrence; private ServiceConnection serviceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName className, IBinder service) { serviceConnection = ((SomeService.SomeBinder)service).getService(); } @Override public void onServiceDisconnected(ComponentName className) { serviceConnection = null; } }; Intent intent = new Intent(this, SomeService.class); bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
După ce serviciul a fost atașat activității, toate metodele și atributele sale publice sunt disponibile prin intermediul parametrului de tip IBinder
din cadrul metodei onServiceConnected()
.
Categorii de Procesări
În funcție de firul de execuție pe care rulează precum și de prioritatea care le este atribuită (în funcție de care sistemul de operare Android poate reclama resursele pe care acestea le utilizează), operațiile complexe pot fi realizate prin intermediul unor:
- servicii care rulează în prim-plan;
- procesări realizate în fundal.
Servicii ce Rulează în Prim-Plan
În situația în care un serviciu interacționează cu utilizatorul, prioritatea acestuia trebuie mărită, astfel încât sistemul de operare Android să nu poată reclama resursele pe care le utilizează.
Un astfel de comportament poate fi obținut prin marcarea serviciului ca rulând în prim-plan, prin invocarea metodei startForeground()
care primește ca parametri:
- identificatorul unei notificări;
- un obiect de tip
Notification
, care va fi afișată cât timp serviciul se află în prim-plan, întrucât se presupune că serviciul trebuie să aibă o reprezentare vizuală cu care utilizatorul să poată interacționa.
Este recomandat ca utilizatorii să poată dezactiva ei înșiși rularea serviciului în prim-plan, de regulă prin intermediul obiectului de tip Notification
. Același lucru trebuie realizat și în momentul în care rularea serviciului în prim-plan nu mai este necesară. În acest sens, trebuie apelată metoda stopForeground(true)
, aceasta anulând în mod automat notificarea asociată.
Procesări Realizate în Fundal
Întrucât responsivitatea aplicației Android reprezintă un criteriu foarte important, asigurarea unei experiențe corespunzătoare a utilizatorului poate fi obținută prin transferul operațiilor complexe de procesare și a operațiilor de intrare/ieșire în fundal, pe un alt fir de execuție. Acest lucru este necesar datorită faptului că pe firul de execuție principal sunt rulate mai multe componente ale aplicației Android (activități, servicii, ascultători ai intențiilor cu difuzare), astfel încât interacțiunea cu utilizatorul poate fi blocată în situația în care procesarea realizată de servicii nu este transferată în fundal.
onReceive()
nu se termină în 5 secunde sunt considerate blocate.
De regulă, sunt plasate pe fire de execuție dedicate operații cu fișiere (scrieri, citiri), căutări în rețea, tranzacții cu baza de date, calcule complexe.
Android oferă mai multe mecanisme pentru gestiunea serviciilor care rulează în fundal:
- utilizarea clasei
AsyncTask
permite definirea unei operații care va fi realizată pe un fir de execuție separat, oferind metode care oferă informații cu privire la progres, ce pot fi consultate de alte fire de execuție; - folosirea clasei
IntentService
; - definirea unei clase derivate din
Loader
; - implementarea unui fir de execuție definit de utilizator, transmiterea valorilor către interfața grafică fiind realizat prin intermediul clasei
Handler
.
Clasa AsyncTask
Prin intermediul clasei AsyncTask
se permite mutarea operațiilor costisitoare din punct de vedere al utilizării procesorului și al memoriei pe fire de execuție rulate în fundal, oferind sincronizarea cu firul de execuție ce randează interfața grafică, acesta fiind notificat cu privire la progresul realizat cât și cu privire la momentul în care procesarea a fost terminată.
AsyncTask
nu este persistentă în cazul (re)pornirii unei activități (situație în care firul de execuție asociat este distrus), se recomandă utilizarea sa pentru procese de fundal care nu durează o perioadă de timp prea mare.
O implementare a clasei AsyncTask
poate fi parametrizată cu tipurile de date care sunt folosite pentru intrare, pentru raportarea progresului și pentru ieșire (rezultate).
Void
pentru toate aceste tipuri.
Este necesară suprascrierea următoarelor metode:
doInBackground()
- metoda care va fi executată pe firul de execuție dedicat, care primește parametrii de intrare de tipul specificat; acesta trebuie să conțină procesările complexe care se doresc a fi realizate fără a interacționa însă cu firul de execuție principal; notificările sunt realizate:- prin intermediul metodei
publishProgress()
, care primește ca parametru progresul realizat, valorile fiind transmise metodeionProgressUpdate()
; - prin întoarcerea unui rezultat, atunci când procesarea este terminată, valoarea fiind transmisă metodei
onPostExecute()
;
onProgressUpdate()
- folosită pentru actualizarea interfeței grafice, cu valorile primite de la metodapublishProgress()
; metoda este sincronizată cu firul de execuție principal (care randează interfața grafică), astfel încât diferitele controale pot fi accesate de aici;onPostExecute()
- apelată în momentul în care procesarea este terminată, primind ca parametru rezultatul întors de metodadoInBackground()
; metoda este sincronizată cu firul de execuție principal (care randează interfața grafică), astfel încât diferitele controale pot fi accesate de aici.
private class SomeAsyncTask extends AsyncTask<String, Integer, String> { @Override protected String doInBackground(String... parameter) { String result = new String(); int progress = 0; for (int k = 1; k <= parameter[0].length(); k++) { progress = k; result += parameter[0].charAt(parameter[0].length() - k); try { Thread.sleep(100); } catch (InterruptedException interruptedException) { Log.e(Constants.TAG, "An exception has occurred: "+interruptedException.getMessage()); } publishProgress(myProgress); } return result; } @Override protected void onProgressUpdate(Integer... progress) { asyncTextView.setText(progress[0].toString()); } @Override protected void onPostExecute(String result) { asyncTextView.setText(result); } }
Rularea unui obiect de tip AsyncTask
se face prin invocarea metodei execute()
care primește ca parametru informațiile care se doresc a fi procesate.
AsyncTask
poate fi rulat o singură dată. Apelurile ulterioare ale metodei execute()
vor genera excepții.
Clasa IntentService
Recursul la clasa IntentService
este adecvată în situația în care se dorește implementarea unor servicii care rulează în fundal, realizând un set de operații la un moment dat de timp, atunci când sunt solicitate.
Un obiect de acest tip este lansat în execuție prin pornirea unui serviciu și transmiterea unui obiect de tip Intent
care conține toți parametrii necesari pentru realizarea sarcinii respective. Toate operațiile solicitate sunt înregistrate și executate succesiv. După ce procesarea a fost finalizată, procesul se oprește singur.
O implementare este o clasă derivată din IntentService
, definind metoda onHandleIntent()
ce primește ca parametru intenția conținând parametrii ce se doresc a fi procesați, execuția sa fiind realizată pe un fir de execuție ce rulează în fundal (câte unul pentru fiecare invocare).
import android.app.IntentService; import android.content.Intent; public class SomeIntentService extends IntentService { public SomeIntentService(String name) { super(name); // ... } @Override public void onCreate() { super.onCreate(); // ... } @Override protected void onHandleIntent(Intent intent) { // ... } }
Clasa Abstractă Loader
Clasa abstractă Loader
implementează practicile recomandate pentru încărcarea asincronă a informațiilor în cadrul unor controale grafice din interfața cu utilizatorul (afișate în activități sau fragmente).
O astfel de abordare este utilizată pentru:
- a încărca date asincron;
- a monitoriza sursele din care sunt încărcate datele, oferind informații cu privire la rezultatele obținute.
De regulă, se folosește o implementare a clasei AsyncTaskLoader
.
Utilizarea firelor de execuție definite de utilizator
Folosirea unor fire de execuție definite de utilizator și sincronizarea manuală cu interfața grafică poate fi necesară în situația în care trebuie realizată o gestiune mai complexă decât cea oferită de clasele AsyncTask
, IntentService
sau Loader
.
În acest sens, este folosită o implementare a clasei Thread
, procesarea pe un fir de execuție separat fiind realizată în cadrul metodei run()
.
private void executeOnSeparateThread() { Thread separateThread = new Thread(new Runnable() { @Override public void run() { // ... } }); separateThread.start(); }
Dacă se dorește actualizarea controalelor din cadrul interfeței grafice, este necesar ca firele de execuție care rulează în fundal să fie sincronizate cu firul de execuție principal anterior acestei operații. Acest lucru poate fi realizat:
- folosind metoda
runOnUiThread()
care forțează codul transmis să fie executat pe același fir de execuție care redă interfața grafică:runOnUiThread(new Runnable() { @Override public void run() { // ... } });
- utilizând un obiect de tip
Handler
pentru a realiza actualizări în contextul firului de execuție în care acesta a fost creatprivate Handler handler = new Handler(); // created on the main thread private void executeOnSeparateThread() { Thread separateThread = new Thread(new Runnable() { @Override public void run() { // do some background processing here handler.post(new Runnable() { @Override public void run() { // access the graphical user interface here } }); } }); separateThread.start(); }
Clasa
Handler
pune la dispoziție și metode pentru a executa anumite metode la un moment dat de timp:
postDelayed()
- realizează o modificare la nivelul interfeței grafice cu o întârziere specificată ca parametru (exprimat în milisecunde);postAtTime()
- realizează o modificare la nivelul interfeței grafice la un moment de timp specificat ca parametru (exprimat ca număr de milisecunde ce au trecut de la 1 ianuarie 1970.
Realizarea de Procesări prin Intermediul Alarmelor
O alarmă reprezintă un mecanism de transmitere a unor intenții la momente predefinite de timp sau periodic, după trecerea unui anumit interval. Acestea există în afara domeniului de existență a unei aplicații Android, astfel încât acestea pot fi utilizate pentru a realiza anumite acțiuni chiar și în situația în care acestea nu mai există. Din acest motiv, alarmele reprezintă o metodă foarte utilă pentru a difuza intenții, pentru a porni servicii sau lansa în execuție activități, fără a fi necesar ca aplicația să se afle în execuție. Se asigură astfel și optimizarea cerințelor legate de resursele utilizate de aplicație.
Cele mai frecvente utilizări ale alarmelor sunt legate de planificarea unor actualizări bazate pe căutări în rețea, programarea unor operații (consumatoare de resurse) la momente de timp în care solicitările sunt mai reduse, organizarea unor noi încercări pentru operații care nu au putut fi realizate anterior.
Operațiile cu alarme sunt realizate prin intermediul serviciului de sistem AlarmManager
care poate fi accesat prin intermediul metodei getSystemService()
care primește ca parametru argumentul Context.ALARM_SERVICE
.
Există mai multe tipuri de alarme:
RTC_WAKEUP
- scoate dispozitivul mobil din starea de latență prin transmiterea unei intenții în așteptare, la un anumit moment de timp specificat;RTC
- transmite o intenție în așteptare, la un anumit moment de timp specificat, fără a scoate dispozitivul mobil din starea de latență;ELAPSED_REALTIME
- transmite o intenție în așteptare, la un anumit interval de timp scurs de la momentul în care dispozitivul mobil a fost pornit, fără a-l scoate din starea de latență;ELAPSED_REALTIME_WAKEUP
- scoate dispozitivul mobil din starea de latență prin transmiterea unei intenții în așteptare, la un anumit interval de timp scurs de la momentul în care acesta a fost pornit.
În funcție de aceste valori, parametrul furnizat metodei set()
a obiectului de tip AlarmManager
reprezintă un moment de timp sau un interval.
În momentul în care se declanșează, obiectul de tip PendingIntent
este distribuit la nivelul tuturor componentelor sistemului de operare.
PendingIntent
o înlocuiește pe cea existentă.
AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE); alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, 1000, PendingIntent.getBroadcast(this, 0, new Intent("ALARM_ACTION"), 0));
Anularea unei alarme se face prin intermediul metodei cancel()
a obiectului de tip AlarmManager
care primește ca parametru intenția în așteptare care nu mai trebuie transmisă.
alarmManager.cancel(PendingIntent.getBroadcast(this, 0, new Intent("ALARM_ACTION"), 0));
Utilizatorul are și posibilitatea de a indica anumite intervale de timp la care alarma este repetată, prin intermediul unor metode definite în clasa AlarmManager
:
setRepeating()
- utilizată când se dorește un control foarte exact asupra intervalului de timp la care alarma este repetată, exprimat la nivel de milisecunde;setInexactRepeating()
- folosit pentru a economisi consumul de baterie realizat la scoaterea dispozitivului mobil din starea de latență de fiecare dată când este necesară realizarea unor sarcini planificate (care nu se suprapun); astfel, sistemul de operare Android va sincroniza mai multe alarme cu modul de repetare inexact, declanșându-le simultan; metoda primește ca parametru una dintre constantele:INTERVAL_FIFTEEN_MINUTES
INTERVAL_HALF_HOUR
INTERVAL_HOUR
INTERVAL_HALF_DAY
INTERVAL_DAY
Ambele metode primesc ca parametrii tipul de alarmă, un moment de timp la care alarma va fi declanșată inițial și o intenție în așteptare care va fi transmisă.
AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE); alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, AlarmManager.INTERVAL_FIFTEEN_MINUTES, AlarmManager.INTERVAL_DAY, PendingIntent.getBroadcast(this, 0, new Intent("ALARM_ACTION"), 0));
Anularea unei alarme recurente se face tot prin intermediul metodei cancel()
a obiectului de tip AlarmManager
care primește ca parametru intenția în așteptare care nu mai trebuie transmisă.
Activitate de Laborator
Se dorește implementarea unei aplicații Android, conținând o activitate conținând două fragmente, 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.
1. În contul Github personal, să se creeze un depozit denumit 'Laborator04'. Acesta trebuie să conțină unui fișier README.md
, un fișier .gitignore
specific unei aplicații Android și un fișier LICENSE
care să descrie condițiile pentru utilizarea aplicației.
2. Să se cloneze într-un director de pe discul local conținutul depozitului la distanță astfel creat. În urma acestei operații, directorul Laborator04 va trebui să se conțină fișierele README.md
, .gitignore
care indică tipurile de fișiere (extensiile) ignorate și LICENSE
.
student@pdsd2015:~$ git clone https://www.github.com/perfectstudent/Laborator04
3. În directorul Laborator04 de pe discul local, să se creeze un proiect Eclipse denumit ContactsManager.
Se utilizează următoarele detalii ale proiectului:
- Application Name - Contacts Manager
- Project Name - ContactsManager
- Package Name -
ro.pub.cs.systems.pdsd.lab04.contactsmanager
- Minimum Required SDK - API 16: Android 4.1 (Jelly Bean)
- Target SDK - API 16: Android 4.1 (Jelly Bean)
- Compile With - API 16: Android 4.1 (Jelly Bean)
- Theme - Holo Light with Dark Action Bar
Ceilalți parametrii de configurare sunt impliciți:
- denumirea activității -
ContactsManagerActivity
; - denumirea fișierului XML din
res/layout
în care va fi construită interfața grafică -activity_contacts_manager
.
4. În fișierul activity_contacts_manager
din directorul res/layout
să se construiască interfața grafică folosind:
- editorul vizual (Graphical Layout)
- editorul XML (manual)
Acesta va fi format din două containere de tip FrameLayout
în care vor fi plasate fragmentele, dispuse vertical.
5. Să se definească două fragmente:
BasicDetailsFragment
cu interfața grafică în fișierulfragment_basic_details.xml
din directorulres/layout
; acesta va fi afișat în permanență în cadrul activității (pe metodaonCreate()
a acesteia se adaugă o instanță a fragmentului la containerulFragmentManager fragmentManager = getFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); fragmentTransaction.add(R.id.containerTop, new BasicDetailsFragment()); fragmentTransaction.commit();
AdditionalDetailsFragment
cu interfața grafică în fișierulfragment_additional_details.xml
din directorulres/layout
; acesta va fi atașat / detașat la activitate la accesarea butonului Show Additional Fields, respectiv Hide Additional Fields.
O clasă asociată unui fragment este derivată din android.app.Fragment
și implementează metoda onCreateView()
pe care se încarcă interfața grafică corespunzătoare:
public class SomeFragment extends Fragment { @Override public View onCreateView(LayoutInflater layoutInflater, ViewGroup container, Bundle state) { return layoutInflater.inflate(R.layout.fragment_some, container, false); } }
6. Să se implementeaze intefețele grafice ale fragmentelor, conținând următoarele controale:
fragment_basic_details.xml
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 fragment nu este afișat, respectiv mesajul Hide Additional Fields în cazul în care celălalt fragment 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ă.
fragment_additional_details.xml
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.
7. Să se implementeaze interacțiunea cu utilizatorul a aplicației.
- în metoda
onActivityCreated()
a fragmentuluiBasicDetailsFragment
se obțin referințe către butoanele Show Additional Details / Hide Additional Details, respectiv Save și Cancel prin intermediul metodeigetActivity().findViewById(R.id….)
; - se implementează o clasă ascultător pentru butoane, care implementează
View.OnClickListener
și (re)definește metodaonClick(View v)
; în funcție de identificatorul butonului care este transmis ca parametru al metodei, sunt realizate următoarele acțiuni:- butonul Show Additional Details / Hide Additional Details - atașează / detașează fragmentul
AdditionalDetailsFragment
la activitate în funcție de existența / inexistența acestuia, modificând corespunzător textul afișat pe buton:FragmentManager fragmentManager = getActivity().getFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); AdditionalDetailsFragment additionalDetailsFragment = (AdditionalDetailsFragment)fragmentManager.findFragmentById(R.id.containerBottom); if (additionalDetailsFragment == null) { fragmentTransaction.add(R.id.containerBottom, new AdditionalDetailsFragment()); ((Button)v).setText(getActivity().getResources().getString(R.string.hide_additional_fields)); fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_ENTER_MASK); } else { fragmentTransaction.remove(additionalDetailsFragment); ((Button)v).setText(getActivity().getResources().getString(R.string.show_additional_fields)); fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_EXIT_MASK); } fragmentTransaction.commit();
- butonul Save - lansează în execuție aplicația Android nativă pentru stocarea unui contact în agenda telefonică, după ce în prealabil au fost preluate informațiile din controalele grafice:
Intent intent = new Intent(ContactsContract.Intents.Insert.ACTION); intent.setType(ContactsContract.RawContacts.CONTENT_TYPE); if (name != null) { intent.putExtra(ContactsContract.Intents.Insert.NAME, name); } if (phone != null) { intent.putExtra(ContactsContract.Intents.Insert.PHONE, phone); } if (email != null) { intent.putExtra(ContactsContract.Intents.Insert.EMAIL, email); } if (address != null) { intent.putExtra(ContactsContract.Intents.Insert.POSTAL, address); } if (jobTitle != null) { intent.putExtra(ContactsContract.Intents.Insert.JOB_TITLE, jobTitle); } if (company != null) { intent.putExtra(ContactsContract.Intents.Insert.COMPANY, company); } ArrayList<ContentValues> contactData = new ArrayList<ContentValues>(); if (website != null) { ContentValues websiteRow = new ContentValues(); websiteRow.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE); websiteRow.put(ContactsContract.CommonDataKinds.Website.URL, website); contactData.add(websiteRow); } if (im != null) { ContentValues imRow = new ContentValues(); imRow.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE); imRow.put(ContactsContract.CommonDataKinds.Im.DATA, im); contactData.add(imRow); } intent.putParcelableArrayListExtra(ContactsContract.Intents.Insert.DATA, contactData); getActivity().startActivity(intent);
Intenția pentru realizarea acestei operații are asociată acțiunea
ContactsContract.Intents.Insert.ACTION
și tipulContactsContract.RawContacts.CONTENT_TYPE
. Informațiile care se doresc a fi completate sunt atașate în câmpulextra
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 elementeContentValues
în care se specifică înregistrări de tipulCommonDataKinds.Website.URL
, respectivCommonDataKinds.Im.DATA
;
Pentru a putea gestiona agenda telefonică, este necesar ca în fișierulAndroidManifest.xml
să fie specificate următoarele permisiuni:<uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.WRITE_CONTACTS" />
- butonul Cancel - termină aplicația Android:
getActivity().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()
.
8. 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ă.
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.
if (phoneNumber.length() > 0) { intent = new Intent("ro.pub.cs.systems.pdsd.lab04.contactsmanager.intent.action.ContactsManagerActivity"); intent.putExtra("ro.pub.cs.systems.pdsd.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(); }
9. 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.pdsd.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- AndroidManifest.xml
<manifest ...> <application ...> <activity android:name=".graphicuserinterface.ContactsManagerActivity" android:label="@string/app_name" > <intent-filter> <action android:name="ro.pub.cs.systems.pdsd.lab04.contactsmanager.intent.action.ContactsManagerActivity" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> </application> </manifest>
- în metoda
onActivityCreated()
asociată fragmentuluiBasicDetailsFragment
este verificată intenția cu care este pornită activitatea părinte, și în cazul în care aceasta nu este nulă, este preluată informația din secțiuneaextra
, identificată prin cheiaro.pub.cs.systems.pdsd.lab04.contactsmanager.PHONE_NUMBER_KEY
, conținutul său fiind plasat în cadrul câmpului text corespunzător:if (intent != null) { String phone = intent.getStringExtra("ro.pub.cs.systems.pdsd.lab04.contactsmanager.PHONE_NUMBER_KEY"); if (phone != null) { phoneEditText.setText(phone); } else { Activity activity = getActivity(); Toast.makeText(activity, activity.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:
getActivity().startActivityForResult(intent, Constants.CONTACTS_MANAGER_REQUEST_CODE);
- Cancel - se transmite înapoi rezultatul
getActivity().setResult(Activity.RESULT_CANCELED, new Intent());
- în metoda
onActivityResult()
asociată activitățiiContactsManagerActivity
, î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; } }
10. Să se încarce modificările realizate în cadrul depozitului 'Laborator04' de pe contul Github personal, folosind un mesaj sugestiv.
student@pdsd2015:~/Laborator04$ git add * student@pdsd2015:~/Laborator04$ git commit -m "implemented taks for laboratory 04" student@pdsd2015:~/Laborator04$ git push origin master
Resurse Utile
Joseph ANNUZZI, Jr, Lauren DARCEY, Shane CONDER, Introduction to Android Application Development - Developer's Library, 4th Edition, Addison-Wesley, 2013 - capitolul 4, subcapitolele Organizing Activity Components with Fragments, Managing Activity Transition with Intents, Working with Services, Receiving and Broadcasting Intents
Bill PHILLIPS, Brian HARDY, Android Programming. The Big Nerd Ranch Guide, Pearson Technology Group, 2013 - capitolele 5, 7, 10, 21, 23, 29, 30
Reto MEIER, Professional Android for Application Development, John Wiley & Sons, 2012 - capitolul 4 (Introducing Fragments), 5 (Introducing Intents)
Ronan SCHWARZ, Phil DUTSON, James STEELE, Nelson TO, Android Developer's Cookbook, Building Applications with the Android SDK, 2nd Edition, Addison Wesley, 2013 - capitolele 2, 7
Wei Meng LEE, Beginning Android 4 Application Development, Wiley, 2012
Satya KOMATINENI, Dave MACLEAN, Pro Android 4, Apress, 2012
Dezvoltarea aplicațiilor pentru Android
Android Programming Tutorials - Core Servlets - secțiunile Intents - part I, II & III
Android Intents - Tutorial