This is an old revision of the document!


Structura unei Aplicații (III)

Servicii

În Android, clasa android.app.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ă.

Întrucât prioritatea unui serviciu este mai mare decât a unei activități inactive, este mai puțin probabil ca acestea să fie distruse atunci când trebuie eliberate resursele sistemului de operare. De altfel, un serviciu poate fi configurat să fie repornit imediat ce este posibil sau chiar pentru a i se asocia o prioritate echivalentă cu a unei activități active (dacă distrugerea serviciului are un impact la nivelul interfeței grafice).

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 tip Intent, 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.eim.lab05.SomeService"
      android:enabled="true"
      android:permission="ro.pub.cs.systems.eim.lab05.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 metodelor startService() și stopService()); prin aceasta, se indică faptul că metoda onStartCommand() 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 tip Intent va avea valoarea null);
  • Service.START_NOT_STICKY - mecanism utilizat de serviciile utilizate pentru a procesa anumite comenzi, care se opresc singure (printr-un apel al metodei stopSelf()) 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 metodei startService(); 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 metodei startService() sau procesul a fost oprit ca acesta să invoce metoda stopSelf() - metoda onStartCommand() 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 tip Intent care poate fi creat explicit sau implicit;

Întrucât apelurile metodei 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ă.

Nu este recomandat să existe mai multe servicii care rulează în prim-plan simultan întrucât acestea nu pot fi distruse de sistemul de operare Android și în situația în care necesarul de memorie devine o problemă critică, performanțele dispozitivului mobil se vor deprecia considerabil.

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.

În Android, o activitate care nu răsunde la un eveniment în decurs de 5 secunde sau un ascultător al unei intenții cu difuzare a cărei metodă 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:

  1. 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;
  2. folosirea clasei IntentService;
  3. definirea unei clase derivate din Loader;
  4. 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ă.

Întrucât clasa 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).

În situația în care nu se dorește transmiterea unor parametrii de intrare / ieșire sau nu este necesară raportarea progresului, se poate utiliza valoarea 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 metodei onProgressUpdate();
    • 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 metoda publishProgress(); 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 metoda doInBackground(); 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.

Fiecare obiect de tip 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:

  1. 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() {
        // ...
      }
    });
  2. utilizând un obiect de tip Handler pentru a realiza actualizări în contextul firului de execuție în care acesta a fost creat
    private 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.

Evenimentele care pot fi produse doar pe parcursul ciclului de viață al unei aplicații Android trebuie tratate prin fire de execuție separate implementate de utilizator și sincronizate (manual) cu interfața grafică. Utilizarea de alarme trebuie limitată doar la evenimente programate în afara ferestrei temporale în care se desfășoară aplicația.

În Android, alarmele rămân active chiar și atunci când dispozitivul mobil se găsește într-o stare de latență (eng. sleep mode), putând fi utilizate pentru a-l scoate din aceasta. Totuși, ele devin inactive în momentul în care dispozitivul mobil este repornit.

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.

Specificarea unei alte alarme folosind același obiect de tip 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));

Impactul pe care alarmele recurente îl au asupra consumului de baterie poate fi semnificativ. De aceea, este recomandat ca frecvența de repetare a alarmei să fie cât mai mică, scoțând dispozitivul mobil din starea de latență numai atunci când este necesar și utilizând tipul de repetare inexact, dacă este posibil.

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

1. În contul Github personal, să se creeze un depozit denumit 'Laborator05'. Inițial, acesta trebuie să fie gol (nu trebuie să bifați nici adăugarea unui fișier README.md, nici a fișierului .gitignore sau a a fișierului LICENSE).

2. Să se cloneze în directorul de pe discul local conținutul depozitului la distanță de la https://www.github.com/eim2016/Laborator05. În urma acestei operații, directorul Laborator05 va trebui să se conțină un director labtasks ce va deține proiectele AndroidStudio respectiv Eclipse, fișierul README.md și un fișier .gitignore care indică tipurile de fișiere (extensiile) ignorate.

student@eim2016:~$ git clone https://www.github.com/eim2016/Laborator05.git

3. Să se încarce conținutul descărcat în cadrul depozitului 'Laborator05' de pe contul Github personal.

student@eim2016:~$ cd Laborator05
student@eim2016:~/Laborator05$ git remote add Laborator05_perfectstudent https://github.com/perfectstudent/Laborator05
student@eim2016:~/Laborator05$ git push Laborator05_perfectstudent master

4. Să se încarce în mediul integrat de dezvoltare preferat (Android Studio sau Eclipse) proiectele StartedService respectiv StartedServiceActivity din directorul labtasks/<IDE>/StartedService.

  • Proiectul StartedService conține codul sursă pentru un serviciu de tip started care transmite mai multe valori, de diferite tipuri (șir de caractere, întreg, vector), temporizate la un anumit interval (dată de valoarea SLEEP_TIME din interfața Constants). Aceste valori sunt transmise prin intermediul unor intenții cu difuzare (eng. broadcast intents), la nivelul întregului sistem de operare Android.
  • Proiectul StartedServiceActivity conține codul sursă pentru o aplicație Android care utilizează un ascultător pentru intenții cu difuzare (eng. BroadcastReceiver), pentru tipurile de mesaje propagate la nivelul sistemului de operare de către serviciu, pe care le afișează în interfața grafică, prin intermediul unui câmp text.

5. În proiectul StartedService, în clasa StartedService din pachetul ro.pub.cs.systems.eim.lab05.startedservice.service, să se completeze metoda onStartCommand() astfel încât aceasta să pornească un fir de execuție în cadrul căruia să fie propagate 3 intenții cu difuzare la nivelul sistemului de operare Android.

Pentru fiecare intenție, se vor specifica:

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

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

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

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

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

Indicații de Rezolvare

Indicații de Rezolvare

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

ProcessingThread.java
package ro.pub.cs.systems.eim.lab05.startedservice.service;
 
import android.content.Context;
import android.content.Intent;
 
import ro.pub.cs.systems.eim.lab05.startedservice.general.Constants;
 
public class ProcessingThread extends Thread {
 
  private Context context;
 
  public ProcessingThread(Context context) {
    this.context = context;
  }
 
  @Override
  public void run() {
    while(true){ 
      sendMessage(Constants.MESSAGE_STRING);
      sleep();
      // ...
    }
  }
 
  private void sleep() {
    try {
      Thread.sleep(Constants.SLEEP_TIME);
    } catch (InterruptedException interruptedException) {
      interruptedException.printStackTrace();
    }
  }
 
  private void sendMessage(int messageType) {
    Intent intent = new Intent();
    switch(messageType) {
      case Constants.MESSAGE_STRING:
         intent.setAction(Constants.ACTION_STRING);
         intent.putExtra(Constants.DATA, Constants.STRING_DATA);
         break;
      // ...
    }
    context.sendBroadcast(intent);
  }
}

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

Indicații de Rezolvare

Indicații de Rezolvare

Intent intent = new Intent();
intent.setComponent(new ComponentName("ro.pub.cs.systems.eim.lab05.startedservice", "ro.pub.cs.systems.eim.lab05.startedservice.service.StartedService"));
startService(intent);

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

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

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

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

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

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

Indicații de Rezolvare

Indicații de Rezolvare

În cadrul metodei onReceive(Context, Intent) din clasa StartedServiceBroadcastReceiver, se verifică:

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

StartedServiceBroadcastReceiver.java
package ro.pub.cs.systems.eim.lab05.startedserviceclient.view;
 
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.TextView;
 
import ro.pub.cs.systems.eim.lab05.startedserviceclient.general.Constants;
 
public class StartedServiceBroadcastReceiver extends BroadcastReceiver {
 
  private TextView messageTextView;
 
  public StartedServiceBroadcastReceiver(TextView messageTextView) {
    this.messageTextView = messageTextView;
  }
 
  @Override
  public void onReceive(Context context, Intent intent) {
    String action = intent.getAction();
    String data = null;
    if (Constants.ACTION_STRING.equals(action)) {
      data = intent.getStringExtra(Constants.DATA);
    }
    // ...
    if (messageTextView != null) {
      messageTextView.setText(messageTextView.getText().toString() + "\n" + data);
    }
  }
}

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

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

Indicații de Rezolvare

Indicații de Rezolvare

startedServiceBroadcastReceiver = new StartedServiceBroadcastReceiver(messageTextView);

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

Indicații de Rezolvare

Indicații de Rezolvare

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

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

Indicații de Rezolvare

Indicații de Rezolvare

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

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

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

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

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

Indicații de Rezolvare

Indicații de Rezolvare

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

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

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

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

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

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

10. Să se încarce în mediul integrat de dezvoltare preferat (Android Studio sau Eclipse) proiectul BoundedServiceActivity din directorul labtasks/<IDE>/BoundedService. Se dorește să se implementeze un serviciu de tip bounded care poate interacționa cu o activitate prin intermediul unei metode care furnizează un șir de caractere ales aleator dintr-o listă de valori.

a) În clasa BoundedService din pachetul ro.pub.cs.systems.eim.lab05.boundedserviceactivity.service, să se implementeze o clasă internă publică, derivată din interfața IBinder, care oferă o referință către serviciu prin intermediul metodei publice getService(). Instanța acestei clase interne va fi furnizată ca rezultat al metodei onBind().

Indicații de Rezolvare

Indicații de Rezolvare

public class BoundedService extends Service {
 
  final private IBinder boundedServiceBinder = new BoundedServiceBinder();
 
  public class BoundedServiceBinder extends Binder {
    public BoundedService getService() {
      return BoundedService.this;
    }
  }
 
  @Override
  public IBinder onBind(Intent intent) {
    return boundedServiceBinder;
  }
}

b) În clasa BoundedService din pachetul ro.pub.cs.systems.eim.lab05.boundedserviceactivity.service, să se implementeze o metodă care întoarce o valoare aleatoare din tabloul de constante Constants.MESSAGES. Aceasta va avea identificatorul de acces public, astfel încât să poată fi accesată din cadrul activității.

c) În clasa BoundedServiceActivity din pachetul ro.pub.cs.systems.eim.lab05.boundedserviceactivity.view, să se instanțieze un obiect de tipul ServiceConnection, care va fi utilizat pentru asocierea serviciului la activitate.

Vor fi suprascrise metodele:

  • onServiceConnected() - se creează referința la serviciu prin intermediul argumentului de tipul IBinder (prin intermediul metodei getService()) și se actualizează variabila care reține starea legăturii dintre serviciu și activitate;
  • onServiceDisconnected() - se distruge referința la serviciu și se actualizează variabila care reține starea legăturii dintre serviciu și activitate.

Indicații de Rezolvare

Indicații de Rezolvare

private ServiceConnection serviceConnection = new ServiceConnection() {
  @Override
  public void onServiceConnected(ComponentName className, IBinder service) {
    BoundedService.BoundedServiceBinder binder = (BoundedService.BoundedServiceBinder)service;
    boundedService = binder.getService();
    boundedServiceStatus = Constants.SERVICE_STATUS_BOUND;
  }
 
  @Override
  public void onServiceDisconnected(ComponentName className) {
    boundedService = null;
    boundedServiceStatus = Constants.SERVICE_STATUS_UNBOUND;
  }
};

d) Să se atașeze, respectiv să se detașeze activitatea la / de serviciu, în cadrul metodelor de callback corespunzătoare stării în care aceasta poate interacționa cu utilizatorul.

  • pe metoda onStart() se apelează metoda bindService(), utilizându-se un obiect de tip Intent explicit (care primește ca argument clasa care urmează să fie invocată);
  • pe metoda onStop() se apelează metoda unbindService(); se actualizează variabila care reține starea legăturii dintre serviciu și activitate.

Indicații de Rezolvare

Indicații de Rezolvare

@Override
protected void onStart() {
  super.onStart();
  Intent intent = new Intent(this, BoundedService.class);
  bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
}
 
@Override
protected void onStop() {
  super.onStop();
  if (boundedServiceStatus == Constants.SERVICE_STATUS_BOUND) {
    unbindService(serviceConnection);
    boundedServiceStatus = Constants.SERVICE_STATUS_UNBOUND;
  }
}

e) Să se implementeze o clasă ascultător pentru evenimentul de tip apăsare corespunzător controlului grafic de tip buton. În cadrul metodei de tratare a evenimentului, să se apeleze metoda din cadrul serviciului care furnizează un mesaj aleator și să se afișeze acest mesaj în cadrul câmpului text, împreună cu data și ora la care a fost furnizat.

Indicații de Rezolvare

Indicații de Rezolvare

private class GetMessageFromServiceButtonListener implements View.OnClickListener {
  @Override
  public void onClick(View view) {
    if (boundedService != null && boundedServiceStatus == Constants.SERVICE_STATUS_BOUND) {
      messageFromServiceTextView.setText("[" + new Timestamp(System.currentTimeMillis()) + "] " + boundedService.getMessage() + "\n" + messageFromServiceTextView.getText().toString());
    }
  }
}

f) Să se afișeze mesaje sugestive pe metodele de callback ale activității și ale serviciului și să se observe care este momentul în care acesta este creat, respectiv este distrus. Ce se întâmplă în momentul în care se apasă tasta Home?

11. Să se încarce modificările realizate în cadrul depozitului 'Laborator05' de pe contul Github personal, folosind un mesaj sugestiv.

student@eim2016:~/Laborator05$ git add labtasks/*
student@eim2016:~/Laborator05$ git commit -m "implemented taks for laboratory 05"
student@eim2016:~/Laborator05$ git push Laborator05_perfectstudent master

Resurse Utile

Services
Bound Services
Android Service and Broadcast Receiver Example
Android Services - Tutorial
Dave MacLEAN, Satya KOMATINENI, Grant ALLEN, Pro Android 5 - capitolele 16 (BroadcastReceivers and Long-Running Services) și 17 (Exploring the Alarm Manager)
Marko GARGENTA, Masumi NAKAMURA, Learning Android, 2nd Edition - capitolele 10 (Services) și 13 (Broadcast Receivers)
Reto MEIER, Android 4 Application Development, capitolul 5 (Intents and Broadcast Receivers)
Joseph ANNUZZI, Jr, Lauren DARCEY, Shane CONDER, Introduction to Android Application Development - Developer's Library, 4th Edition, Addison-Wesley, 2013 - capitolul 4, subcapitolele Working with Services, Receiving and Broadcasting Intents
Bill PHILLIPS, Brian HARDY, Android Programming. The Big Nerd Ranch Guide, Pearson Technology Group, 2013 - capitolele 29 (Background Services), 30 (Broadcast Intents)

eim/laboratoare/laborator05.1458597761.txt.gz · Last modified: 2016/03/22 00:02 by dragos.niculescu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0