6. stretnutie: Online prezenčka

Cieľové elementy

Jedným klikom na tlačidlo zverejnite svoju prítomnosť v miestnosti a nájdite svojich priateľov. Ukážeme si sieťovú komunikáciu cez HTTP a prejdeme možnosti pre spúšťanie činností na pozadí.

Koncepty, ktoré zvládneme

  • HTTP klient
  • AsyncTask pre vykonávanie činností na pozadí, aby aplikácia ostala svižná
  • služby pre dlhotrvajúce operácie
  • AsyncLoader pre načítavanie dát do adaptérov na pozadí
  • notifikácie pre podstatné informácie, ktoré nepotrebujú okamžitý zásah používateľa
  • vysielanie správ v systéme cez broadcasty
  • periodické plánovanie činností

Výsledná aplikácia

Príprava projektu

Vízia

Aplikácia ponúkne možnosť oznámiť svetu prítomnosť používateľa. Netreba prepadať paranoji: jednoducho po stlačení tlačidla sa odošle na vzdialený server reťazec s menom prítomného.

Popri tom bude zobrazovať zoznam prítomných v miestnosti, ktorý sa opäť načíta zo servera. Používateľ bude môcť tento zoznam aktualizovať buď ručne, alebo automaticky. Ako bonus periodickej aktualizácie bude používateľ vidieť notifikácie o počte ľudí v miestnosti.

Nový projekt

Založme si nový projekt s názvom Presentr a balíčkom sk.upjs.ics.android.presentr na platforme Android 4.0.3. a vytvorme prázdnu aktivitu MainActivity.

Ohlásenie prítomnosti

Najprv poďme implementovať ohlasovanie prítomnosti, ktoré v používateľskom rozhraní sprístupnime pomocou tlačidla na action bare. Následne tlačidlo sfunkčníme a v obsluhe kliknutia naň odošleme serveru cez HTTP protokol informáciu o prihlásení. Samotné odosielanie však musíme vykonať na pozadí, aby nebrzdilo používateľské rozhranie.

Tlačidlo na action bare

Do menu_main.xml dodajme definíciu tlačidla lišty akcií:

<item android:id="@+id/iAmHereAction"
    android:title="Som tu!"
    app:showAsAction="always" />

Do kódu aktivity dodáme kostru obsluhy kliknutia na tlačidlo:

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    int id = item.getItemId();

    if (id == R.id.iAmHereAction) {
        sendPresence();
        return true;
    }

    return super.onOptionsItemSelected(item);
}

private void sendPresence() {

}

Metóda sendPresence() je zatiaľ prázdna: čaká na kód odoslania prítomnosti!

Úloha na pozadí: AsyncTask

Zrejme sa zhodneme, že aplikácie by mali byť čo najsvižnejšie: veďže si porovnajte aplikáciu na staršom a pomalšom Androide so svižnými iPhonami!

Vždy, keď máme v úmysle programovať dlhotrvajúcu činnosť, nesmieme to spraviť v hlavnom vlákne, teda priamo v metóde, pretože potom by sme zdržovali prekresľovanie používateľského rozhrania. Namiesto toho musíme spustiť túto činnosť na pozadí, v samostatnom vlákne.

Ak to nedodržíme, uvidíme neslávne známe ANR (Application Not Responding*), ktorý sa prejaví upozornením:

V Logcate navyše uvidíme hlášku

I/Choreographer: Skipped 301 frames! The application may be doing too much work on its main thread.

Štandardne je táto hláška zobrazená po piatich sekundách zdržania používateľského rozhrania.

Aká dlhá je dlhotrvajúca činnosť?

„Americkí vedci zistili, že“ ľudia nerozpoznajú činnosť s oneskorením menším než desatina sekundy. Všetko dlhšie by sme mohli chápať ako dlhotrvajúcu činnosť. V praxi sa však osvedčí iná úvaha: vždy keď plánujeme pracovať s databázou alebo sieťou, alebo mienime spustiť siahodlhý výpočet. Na druhej strane, ak výpočet potrvá dlhšie, než je plánovaná životnosť aktivity (typicky dlhšie než jednotky sekúnd), budeme musieť použiť iné techniky.

AsyncTask

AsyncTask predstavuje triedu pre asynchrónnu úlohu, teda činnosť spúšťanú na pozadí, v samostatnom vlákne.

Použitie triedy spočíva predovšetkým vo vytvorení jej podtriedy a prekrytí viacerých metód, spomedzi ktorých nás zaujímajú dve:

  • doInBackground() obsahuje kód pre činnosť bežiacu na pozadí. Metóda sa spustí v samostatnom vlákne.
  • onPostExecute() sa vykoná, keď činnosť na pozadí dobehne. Metóda sa spustí v hlavnom vlákne.

Vytvorme teda podtriedu SendPresenceAsyncTask, ktorá bude dediť od AsyncTasku. Musíme sa však ešte porozprávať o troch generických parametroch, ktoré reprezentujú postupne:

  • dátový typ pre vstupný parameter činnosti na pozadí,
  • dátový typ pre sledovanie progresu činnosti,
  • dátový typ pre výsledok činnosti na pozadí

Naša úloha bude mať na vstupe meno používateľa, ktorého prihlásime, reprezentované reťazcom. Progres nebudeme sledovať, na čo využijeme špeciálny dátový typ Void (s veľkým V). Výsledok činnosti budeme monitorovať len v rovine "podarilo sa"/"nepodarilo sa prihlásiť", teda Booleanom.

Triedu deklarujeme ako privátnu vnútornú triedu hlavnej aktivity. Prečo? Kvôli pohodlnosti, čo hneď uvidíme.

private class SendPresenceAsyncTask extends AsyncTask<String, Void, Boolean> {

    @Override
    protected Boolean doInBackground(String... params) {
        String username = params[0];
        return true;
    }

    @Override
    protected void onPostExecute(Boolean success) {
        String message;
        if(success) {
            message = "You are here!";
        } else {
            message = "Try again!";
        }
        Toast.makeText(MainActivity.this, message, Toast.LENGTH_SHORT)
                .show();
    }
}

Naša trieda bude dediť od AsyncTask<String, Void, Boolean>. Keďže vstupný parameter je reťazcový, v metóde doInBackground() dostaneme pole reťazcových parametrov (áno, v Jave je String... chápaný ako premenlivý počet parametrov, ku ktorému pristupujeme ako k poľu). Z nich nás však zaujíma len prvý prvok poľa, ktorý vytiahneme do premennej. Metóda musí vracať Boolean výsledok (tak sme si to totiž nastavili v treťom generickom parametri).

Ak dobehne doInBackground(), výsledok (u nás Boolean˙) sa zjaví ako parameter metódy onPostExecute(), kde s ním môžeme pracovať. Ak sa podarilo prihlásiť, upozorníme používateľa toastom, a ak nie, tak ... upozorníme používateľa toastom. Všimnite si výhodu vnútornej triedy: keďže vytváranie toastu potrebuje kontext, vieme pristúpiť k inštancii vonkajšej triedy cez MainActivity.this.

Vďaka vnútornej triede vidíme všetky inštančné premenné vonkajšej triedy, teda napríklad aj prípadné premenné s widgetmi. Ale pozor!

V metóde doInBackground() nesmieme pristupovať ku widgetom, ani žiadnym iným zložkám používateľského rozhrania. Táto metóda beží na pozadí a ak by sme túto zásadu nedodržali, môže dôjsť k Naozaj Strašidelným Vláknovým Chybám (NSVCh).

A naopak, metóda onPostExecute beží v hlavnom vlákne, a teda pokojne môžeme upravovať widgety. Ale opäť pozor, v onPostExecute() nesmú bežať dlhotrvajúce operácie, pretože inak poprieme hlavný účet AsyncTasku.

Spustenie AsyncTasku

AsyncTask vytvoríme a spustíme v našej predpripravenej metóde:

private void sendPresence() {
    SendPresenceAsyncTask sendPresenceAsyncTask = new SendPresenceAsyncTask();
    sendPresenceAsyncTask.execute("robert1");
}

Jednoducho vytvoríme inštanciu, a následne ju spustíme, pričom do ne pošleme do nej reťazcový parameter s menom používateľa, ktorého máme prihlásiť.

Inter-net, inter-da

V metóde doInBackground() odošleme cez HTTP príkaz na server. Na klasickú webovú komunikáciu platí klasická classa: je ňou HttpUrlConnection z balíčka java.net. Postup práce bude nasledovný:

  1. vytvoríme objekt URL reprezentujúci adresu, na ktorú sa budeme pripájať
  2. zavoláme na ňom openConnection(), čím získame pripojenie na server. Výsledný všeobecný objekt pretypujeme na HttpURLConnection.
  3. na pripojení nastavíme parametre pripojenia, najmä HTTP metódu (cez setRequestMethod()).
  4. pomocou getInputStream() získame prúd bajtov s odpoveďou zo servera
  5. cez getResponseCode() môžeme testovať stavové kódy odpovede
  6. získané pripojenie po uzavretí komunikácie odpojíme cez disconnect().

V našej aplikácii potrebujeme zavolať HTTP POST na zverejnenej adrese služby, pričom nepožadujú sa od nás žiadne ďalšie dáta v tele správy. K tejto adrese akurát musíme prilepiť meno používateľa z parametra metódy doInBackground(). Z URL získame HttpUrlConnection, na ktorom nastavíme HTTP metódu POST a odpoveď od servera, v podobe stavového kódu získame cez getResponseCode().

@Override
protected Boolean doInBackground(String... params) {
    HttpURLConnection connection = null;
    try {
        String username = params[0];
        String urlString = "https://ics.upjs.sk/~novotnyr/android/demo/presentr/index.php/available-users/" + username;
        URL url = new URL(urlString);
        connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("POST");
        if(connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
            return false;
        }
        return true;
    } catch (IOException e) {
        Log.e(getClass().getName(), "Unable to send presence", e);
        return false;
    } finally {
        if(connection != null) {
            connection.disconnect();
        }
    }
}

Všimnime si len, že booleovský výsledok metódy doInBackground() nastavíme podľa toho, či sa komunikácia so serverom podarila, a či je stavový kód HTTP v poriadku (HTTP OK, 200).

Nezabudnime po skončení práce zatvoriť pripojenie v bloku finally, ktorý sa vykoná bez ohľadu na to, či nastala výnimka alebo nie! Android totiž optimalizuje prácu s nízkoúrovňovým pripojením cez recykláciu socketov, a bez zatvorenia pripojenia nemôže dôjsť k recyklácii ani uvoľneniu zdrojov.

Právo internetu

Ak sa chceme pripojiť k internetu, potrebujeme právo internetu, čiže do manifestu dodáme:

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

Beží to!

V tejto chvíli môžeme aplikáciu spustiť a skúšať posielať svoju prítomnosť na vzdialený server.

AsyncTasky a sieťová komunikácia

Keďže sieťová komunikácia je z princípu pomalá (2G pripojenie so slabým signálom môže viesť k dlhým latenciám), od Androidu 3.0 je zakázané pristupovať k sieti z hlavného vlákna. Ak sa o to pokúsite (napr. pripojením k internetu z obsluhy kliku na tlačidlo), uvidíte výnimku android.os.NetworkOnMainThreadException.

Záludnosti pri práci s AsyncTaskmi

Pauzovanie aktivity nepozastaví AsyncTask

Ak je aktivita pozastavené (napr. príde na popredie iná aktivita), spustený AsyncTask sa nepozastaví. Treba si dať na to pozor.

Dlhšia životnosť AsyncTasku bráni uvoľnovaniu pamäte

AsyncTask slúži na vykonávanie dlhotrvajúcich úloh, ale pozor! Ak má takáto úloha dlhšiu životnosť než je životnosť aktivity, z ktorej bola spustená, môžu nastať podivné problémy.

Asynchrónna úloha implementovaná ako vnútorná trieda v sebe nesie referenciu na vonkajšiu triedu: koniec-koncov využívame ju pri prístupe ku kontextu cez MainActivity.this v našom príklade.

Predstavme si situáciu, že sa počas behu asynchrónnej úlohy príde na popredie iná aktivita, a Android sa rozhodne našu MainActivity zničiť, aby uvoľniť pamäť. Alebo si predstavte situáciu, keď používateľ otočí zariadenie, čo takisto povedie k ničeniu aktivity a jej zneplatneniu. Lenže pozor, stále beží úloha na pozadí, ktorá má silný vzťah s aktivitou! A keďže garbage collector nikdy neuprace objekty, ktoré majú silný vzťah s inými objektami, pamäť sa uvoľní až po dobehnutí úlohy. Na obrázku je tento silný vzťah zobrazený modrastou farbou „mraku“.

Dlhšia životnosť AsyncTasku mení vlastnosti neplatnej aktivity

Ďalší, očividnejší problém, nastáva, keď má AsyncTask dlhšiu životnosť než aktivita.

Všimnime si opäť obrázok, ktorý zhora nadol sleduje vývin v čase. Aktivita MainActivity spustí asynchrónnu AsyncTask, lenže skôr než úloha dobehne, je zničená a zneplatnená. Android následne vyrobí novú inštanciu aktivity, ktorá sa zobrazí používateľovi. O niečo neskôr dobehne úloha na pozadí, a má sa v onPostExec() aktualizovať používateľské rozhranie. Lenže... AsyncTask je úzko spätý s aktivitou, ktorá sa už zneplatnila a ak sa pokúsi meniť jej widgety, používateľ to nikdy neuvidí! Prečo? Lebo používateľ už vidí novšiu, znovuvytvorenú inštanciu. (A ak sa pýtate, či možno vrátiť na popredie staršiu inštanciu, tak to nie je možné, ani to nedáva zmysel.)

Riešenie problémov so životnosťou

Problémy so životnosťou sa riešia špinavými trikmi, ktoré však vyžadujú hlbšie znalosti platformy. Najlepšie riešenie tohto problému je v správnom rozhodnutí, či má AsyncTask vôbec zmysel. Ak úloha trvá niekoľko málo sekúnd, a nepotrebujeme bezpodmienečne obdivovať jej výsledok v používateľskom rozhraní, sme v suchu. Toto je presne náš prípad: pošleme na server údaje o prihlásení a výsledok zobrazený v Toaste nám úplne postačuje.

Ak úloha trvá dlhšie alebo chceme bezpodmienečne vidieť výsledok či neúspech, musíme použiť iné nástroje.

Zoznam prihlásených: ručná aktualizácia

Keďže už vieme ohlasovať svoju prítomnosť, je čas implementovať aj opačný smer: zisťovať zoznam prihlásených. V prvej verzii vytvoríme ručne aktualizovateľný zoznam, ktorý neskôr vylepšíme o automatické obnovovanie.

Plán práce bude nasledovný:

  1. oboznámime sa s REST API služby
  2. vytvoríme si pomocnú triedu pre sťahovanie dát
  3. oboznámime sa s JSONom a jeho parsovaním
  4. vytvoríme vlastný loader pre sťahovanie dát
  5. loader prepojíme s aktivitou a jeho dáta budeme dodávať do ArrayAdaptera zoznamu.
  6. pripravíme používateľské rozhranie v podobe zoznamu ListView

REST API služby a JSON

Vzdialená služba poskytuje na adrese https://ics.upjs.sk/~novotnyr/android/demo/presentr/index.php/available-users zoznam aktuálne prihlásených používateľov vo formáte JSON.

JSON

JSON je jednoduchý formát na reprezentáciu dát inšpirovaný JavaScriptom, o ktorom sa môžete dočítať viac napr. na stránke s jeho špecifikáciou. Tento formát je podporovaný Androidom, ktorý poskytuje API na jeho čítanie i zápis.

Výstup z našej prezenčnej služby vyzerá nasledovne:

[
    {
        "login": "novotnyr"
    },
    {
        "login": "mr.been"
    },
    {
        "login": "qwerty0123"
    },
]

Služba poskytuje pole používateľov, kde každým prvkom je objekt s jediným atribútom login, obsahujúcim reťazec.

Pomocná trieda pre sťahovanie dát

Vytvoríme si pomocnú triedu PresenceDao, ktorá poskytne zoznam používateľských mien stiahnutý zo systému. Na pripojenie k serveru využijeme opäť protokol HTTP, v podobe triedy URL.

Jej tvorba pôjde v nasledovných krokoch:

  1. získame prúd dát s odpoveďou zo servera
  2. prevedieme ho na reťazec
  3. reťazec spracujeme pomocou parsera JSON a vytiahneme z neho loginy

Získanie prúdu dát s odpoveďou zo servera

Na rozdiel od odosielania dát, ktoré sme dokončili, tuto sa pripojíme na server cez HTTP metódu GET, a nebudeme potrebovať posielať žiadne špeciálne dáta. V takom prípade stačí vytvoriť objekt URL a zavolať na ňom metódu openStream(), ktorý vráti prúd dát InputStream s odpoveďou zo servera. Po načítaní dát prúd zatvoríme.

URL url = new URL(...);
InputStream in = url.openStream();
...
in.close();

Prevod na reťazec

Parser JSONu, ktorý použijeme v ďalšom kroku, požaduje vstup v tvare reťazca. Urobíme si teda pomocnú metódu, ktorá prevedie InputStream na String:

private String toString(InputStream in) {
    Scanner scanner = new Scanner(in, "utf-8");
    StringBuilder sb = new StringBuilder();
    while(scanner.hasNextLine()) {
        sb.append(scanner.nextLine());
    }
    return sb.toString();
}

Spracovanie JSONu

Špecifikácia JSONu umožňuje držať štruktúrované dáta v objektoch a poliach, ku ktorým existuje v Androide protipól v podobe triedy JSONObject, JSONArray. Každá z týchto tried má konštruktor, vďaka ktorému sa dokáže naparsovať z reťazca.

V našom prípade máme dáta zo servera reprezentované reťazcom JSONu obsahujúcim pole. Vytvoríme teda JSONArray, kde čakáme, že v premennej json je reťazec s obsahom zo servera.

JSONArray people = new JSONArray(json);

Cez JSONArray vieme iterovať tradičným cyklom for (žiaľ, trieda nepodporuje vylepšenú for-each syntax. Každý prvok poľa vytiahneme ako JSONObject, vytiahneme z neho atribút login a výsledok budeme kumulovať v celkovom zozname reťazcov s menami:

List<String> peopleNames = new ArrayList<String>();
for(int i = 0; i < people.length(); i++) {
    JSONObject person = (JSONObject) people.get(i);
    String login = (String) person.get("login");
    peopleNames.add(login);
}

Výnimky a upratovanie

Metódy JSONovských tried vracajú výnimku JSONException a to obvykle v prípade, že sa pri parsovaní narazí na syntaktickú chybu. V prípade našej aplikácie ľubovoľnú výnimku odchytíme, vypíšeme do logu a vrátime prázdny zoznam.

Po skončení práce nezabudneme uzavrieť InputStream z výsledku pomocou metódy close().

Celá trieda

Celá trieda bude vyzerať nasledovne:

package sk.upjs.ics.android.presentr;

import android.util.*;
import org.json.*;
import java.io.*;
import java.net.*;
import java.util.*;

public class PresenceDao {
    public static final String DEFAULT_SERVICE_URL = "https://ics.upjs.sk/~novotnyr/android/demo/presentr/index.php/available-users";

    private URL serviceUrl;

    public PresenceDao() {
        try {
            this.serviceUrl = new URL(DEFAULT_SERVICE_URL);
        } catch (MalformedURLException e) {
            // URL is hardwired and well-formed
        }
    }

    public List<String> loadUsers() {
        InputStream in = null;
        try {
            in = this.serviceUrl.openStream();
            String json = toString(in);

            JSONArray people = new JSONArray(json);
            List<String> peopleNames = new ArrayList<String>();
            for(int i = 0; i < people.length(); i++) {
                JSONObject person = (JSONObject) people.get(i);
                String login = (String) person.get("login");
                peopleNames.add(login);
            }

            return peopleNames;
        } catch (IOException e) {
            Log.e(getClass().getName(), "I/O Exception while loading users", e);
            return Collections.EMPTY_LIST;
        } catch (JSONException e) {
            Log.e(getClass().getName(), "JSON parsing error while loading users", e);
            return Collections.EMPTY_LIST;
        } finally {
            if(in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    // nothing can be done at this point
                }
            }
        }
    }

    private String toString(InputStream in) {
        Scanner scanner = new Scanner(in, "utf-8");
        StringBuilder sb = new StringBuilder();
        while(scanner.hasNextLine()) {
            sb.append(scanner.nextLine());
        }
        return sb.toString();
    }
}

Vlastný loader pre spracovanie dát

Ak chceme asynchrónne načítať dáta z ľubovoľného dátového zdroja a výsledok spracovať v aktivite, pričom chceme rešpektovať životný cyklus aktivity a jeho záludnosti, môžeme použiť známy mechanizmus loaderov. Doposiaľ sme loadovali len dáta z databázy pomocou CursorLoadera.

Nemusíme sa však obmedzovať len na loadery z content providerov: vieme si totiž pripraviť aj vlastnú implementáciu loadera, ktorá bude vyťahovať dáta zo vzdialeného servera.

Pri implementácii loadera využijeme odporúčanie Dianne Hackborne: „prosím, prosím, prosím, prosím, pozrite sa do zdrojákov CursorLoadera a prepíšte ich”.

AbstractObjectLoader

Stiahnime si teda kostru v podobe triedy AbstractObjectLoader, pridajme ju do projektu a následne vytvoríme jej podtriedu, kde budeme ťahať zoznam používateľov z webu.

Vlastný AbstractObjectLoader

Náš loader teda bude dediť od AbstractObjectLoader<List<String>>, pričom generický typ zodpovedá zoznamu reťazcov (s menami), ktoré budeme načítavať.

Implementácia kódu bude jednoduchá: využijeme náš PresenceDao, a v metóde doInBackground(), ktorá sa vykoná na pozadí, v samostatnom vlákne, vrátime zoznam používateľkých mien.

package sk.upjs.ics.android.presentr;

import android.content.Context;
import java.util.List;

public class PresenceLoader extends AbstractObjectLoader<List<String>> {
    private final PresenceDao presenceDao;

    public PresenceLoader(Context context) {
        super(context);
        this.presenceDao = new PresenceDao();
    }

    @Override
    public List<String> loadInBackground() {
        return this.presenceDao.loadUsers();
    }
}

Prepojenie loadera s aktivitou

Ak máme vlastný loader, s aktivitou ho prepojíme tradičným spôsobom cez LoaderCallbacks, a metódu initLoader() a spol.

Aktivita nech implementuje LoaderManager.LoaderCallbacks<List<String>> (dáta sú reprezentované zoznamom), v metóde onCreate() inicializujeme loader:

getLoaderManager().initLoader(USER_LIST_LOADER, Bundle.EMPTY, this);

a dodáme implementáciu pre tri metódy callbackov loadera. V prvej vyrobíme novú inštanciu nášho PresenceLoadera, a ďalšie dve necháme prázdne do momentu, kým si nepridáme používateľské rozhranie (budeme v nich pracovať s adaptérom zoznamu).

@Override
public Loader<List<String>> onCreateLoader(int id, Bundle args) {
    return new PresenceLoader(this);
}

@Override
public void onLoadFinished(Loader<List<String>> loader, List<String> data) {

}

@Override
public void onLoaderReset(Loader<List<String>> loader) {

}

Používateľské rozhranie pre zoznam používateľov

Jadro aktivity bude tvoriť jediný zoznam ListView. Do layoutového súboru teda dodáme:

<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:tools="https://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">

    <ListView
        android:id="@+id/peopleListView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        />

</RelativeLayout>

Do kódu aktivity dodáme inštančnú premennú pre zoznam a ihneď dodáme aj adaptér.

Adaptér

Kým v prípade dát z databázy sme používali CursorAdapter, tu sa môžeme vrátiť k jednoduchému adaptéru pracujúcemu nad poľom či zoznamom, teda k ArrayAdapteru. Dáta z loadera sú totiž pevné a menia sa len vtedy, ak prídu zo servera nanovo.

Zoznam a adaptér: inicializácia

Dodajme teda do aktivity získavanie ListView, a vytváranie adaptéra, ktorý si poznačíme do inštančnej premennej

public static final int USER_LIST_LOADER = 0;
private ListView peopleListView;
private ArrayAdapter<String> adapter;

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

    peopleListView = (ListView) findViewById(R.id.peopleListView);
    adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
    peopleListView.setAdapter(adapter);

    getLoaderManager().initLoader(USER_LIST_LOADER, Bundle.EMPTY, this);
}

Teraz máme k dispozícii takmer všetko: i loader, i adaptér a poďme ich prepojiť:

Prepojenie loadera s adaptérom

Doplňme teda do metód onLoadFinished() a onLoaderReset() príslušný kód.

Prvá metóda bude jednoduchá: získame zoznam používateľských mien z loadera a keďže poľový adaptér má podobné metódy ako zoznam List, jednoducho do adaptéra vložíme nové dáta a ešte predtým pre istotu zoznam premažeme.

@Override
public void onLoadFinished(Loader<List<String>> loader, List<String> presentUsers) {
    this.adapter.clear();
    this.adapter.addAll(presentUsers);
}

Druhá metóda, volaná pri zneplatnení dát, jednoducho odstráni dáta z adaptéra.

@Override
public void onLoaderReset(Loader<List<String>> loader) {
    this.adapter.clear();
}

Aktualizácia zoznamu?

Aktualizácia zoznamu ListView sa bude diať priamo. Adaptér ListView vie, že po pridaní prvku má upozorniť widget, ktorý z neho číta dáta, že je potrebné sa prekresliť.

Tlačidlo pre obnovu dát

Dáta sa teraz načítajú hneď po štarte aktivity. Dodajme si však tlačidlo, ktorým vieme vyvolať aktualizáciu dát.

Predovšetkým, dodajme novú položku do definičného súboru action baru, teda do menu_main.xml. Položku schováme do dodatočných akcií, čo povedie k tomu, že na lište bude stále len jedno tlačidlo.

<item android:id="@+id/refreshPeopleAction"
    android:title="Obnoviť"
    app:showAsAction="never" />

Potom upravme obsluhu kliknutia na tlačidlo na lište akcií:

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    int id = item.getItemId();

    switch(id) {
        case R.id.iAmHereAction:
            sendPresence();
            return true;
        case R.id.refreshPeopleAction:
            refreshPeople();
            return true;
    }
    return super.onOptionsItemSelected(item);
}

Metóda refreshPeople bude zatiaľ prázdna:

private void refreshPeople() {
}

Ako obnoviť zoznam dát?

Reštart loaderov

Doposiaľ sme loader vždy len štartovali cez loader managera a jeho metódu initLoader(). V aplikácii však možno loadery aj reštartnúť, čo znamená, že sa predošlé dáta zahodia a namiesto nich sa objavia čerstvé nové dáta z loadera.

Ak by bol svet krásny, tento kód by fungoval:

private void refreshPeople() {
    getLoaderManager().restartLoader(0, Bundle.EMPTY, this);
}

Žiaľ, realita ukazuje, že v životnom cykle Loaderov je chyba. Namiesto očakávaného cyklu onLoaderReset() -> onLoadFinished() sa však dejú čudesné veci, ktoré nie sú veľmi dobre zdokumentované.

Nie je vôbec jasné, kedy sa volá metóda onLoaderReset(). Nedá sa spoľahnúť na to, že reštart loadera povedie k volaniu onLoaderReset().

Spoľahlivé riešenie vedie k preventívnemu ručnému uprataniu dát. Buď zavoláme metódu onLoaderReset() ručne, alebo, ako v tomto prípade, vyčistíme adaptér.

private void refreshPeople() {
    this.adapter.clear();
    getLoaderManager().restartLoader(0, Bundle.EMPTY, this);
}

Aplikácia beží

Aplikácia beží! Nielenže pri štarte načítava nové dáta, ale po kliknutí na tlačidlo v lište akcií vie dáta ručne aktualizovať!

Automatická aktualizácia dát pomocou služieb

V Androide nemáte len aplikácie pozostávajúce z aktivít, do ktorých môžete ďubať prstom. A nejde len o prípady, keď je aktivita pauznutá (lebo je prekrytá inou aktivitou) alebo zničená (lebo sa vyčerpali systémové zdroje). Popri nich si radostne na pozadí cvrliká množstvo služieb, ktoré nepotrebujú žiadne používateľské rozhranie: budíky, ktoré čakajú na moment, keď vás o 6:30 vyženú z postele; kontrola (G)mailu, a mnoho iných, ktoré nevnímate dovtedy, kým o sebe nedajú vedieť notifikáciou.

Služby (services) sú presne tým komponentom, ktorý môže bežať na pozadí, uskutočňovať dlhotrvajúce operácie a to bez toho, aby potrebovali kamaráta v podobe aktivity, teda bez toho, aby vyžadovali používateľské rozhranie.

Naša služba bude robiť to isté, čo robí náš loader: bude na pozadí sťahovať zoznam prihlásených ľudí. Lenže nebude to úplný duplikát, a ani to nespraví z loadera zbytočnosť. Služby, na rozdiel, od loaderov, budeme môcť periodicky spúšťať, čo si ukážeme neskôr. A ako bonus si predvedieme, ako môže služba upozorňovať používateľa na dôležité udalosti pomocou systémovej notifikácie.

Služba IntentService

Najjednoduchším typom služby je IntentService, ktorá sa presne hodí na prípad, keď:

  • úloha na pozadí má zbehnúť len raz, čiže nejde o dlhotrvajúcu operáciu v duchu chatu, kde sa dokola kontroluje prítomnosť nových správ na serveri
  • úloha má zbehnúť v samostatnom vlákne, aby nezdržiavala hlavné vlákno aplikácie
  • nepotrebuje priebežne aktualizovať GUI, samozrejme okrem jednorazovej notifikácie

Tutoriálovým príkladom je odosielanie fotky na server: je to činnosť, ktorá je príliš dlhá na vykonanie v AsyncTasku, a zakázaná v hlavnom vlákne.

Vytvorenie služby

  1. Vytvoríme triedu PresenceService, ktorá oddedí od IntentService.
  2. Vytvoríme implicitný konštruktor.
  3. Prekryjeme onHandleIntent()
  4. Zavedieme ju do manifestu.

Kostra kódu

Vytvorme v projekte prázdnu triedu a naplňme ju kódom:

package sk.upjs.ics.android.presentr;

import android.app.IntentService;
import android.content.Intent;
import android.util.Log;

public class PresenceService extends IntentService {
    public static final String WORKER_THREAD_NAME = "PresenceService";

    public PresenceService() {
        super(WORKER_THREAD_NAME);
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        Log.i(getClass().getName(), "Downloading users...");
    }
}

Implicitný konštruktor

Implicitný konštruktor musí byť prítomný: bez neho sa nebude dať vytvoriť inštancia služby. V tomto prípade sme povinní zavolať rodičovský konštruktor s reťazcom popisujúcim názov vlákna, v ktorom služba pobeží. (Je to kvôli ladiacim dôvodom.)

Metóda onHandleIntent()

Jadrom je metóda onHandleIntent(), v ktorej sa nachádza samotný kód služby, ktorý má bežať na pozadí. Zatiaľ sa toho veľa neudeje: jeden výpis do logu, ktorý časom vylepšíme.

Zavedenie do manifestu

Služby sú popri aktivitách základnými komponentami aplikácie a je nutné ich uviesť do manifestu. Do elementu <application> v manifeste zaveďme nový element <service> s názvom triedy našej služby:

<application
    android:allowBackup="true"
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme" >

    ...

    <service android:name=".PresenceService" />

</application>

Spustenie služby

Službu odpáľme z aktivity, z metódy onCreate(). Spustenie dosiahneme zaslaním správy, teda intentu, podobne ako keby sme chceli spustiť novú aktivitu. Vytvoríme teda nový intent s názvom triedy služby:

Intent startPresenceService = new Intent(this, PresenceService.class);

a pomocou startService() ju naštartujeme.

V príklade máme metódu runServiceButtonClick(), ktorá sa zavolá po kliknutí na tlačidlo (prepojenie je vďaka atribútu android:onClick="runServiceButtonClick" v XML layoute aktivity):

public class MainActivity extends Activity {
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Intent startPresenceService = new Intent(this, PresenceService.class);
        startService(startPresenceService);
    }
...

Služba sa spustí na pozadí, vypíše do logu jeden riadok a ukončí sa.

sk.upjs.ics.android.presentr I/sk.upjs.ics.android.presentr.PresenceService﹕ Downloading users...

Ako funguje IntentService?

Po naštartovaní nášho IntentService sa v systéme vytvorí jediná inštancia, ktorá spustí samostatné vlákno (tzv. worker thread), ktorý bude čakať na prichádzajúce intenty. Každý prichádzajúci intent (teda každý intent, ktorý aktivita poslala cez startService()) sa zaradí do frontu a postupne sa spracováva v metóde onHandleIntent(). Ak by sme službu spúšťali klikaním na tlačidlo, na ktoré by sme zbesilo poklikali trikrát v tesnom závese, spôsobili by sme odoslanie troch intentov, ktoré sa postupne spracujú.

Ak náš intent service spracuje všetky intenty čakajúce vo fronte, automaticky sa ukončí.

Chcete to vidieť v akcii?

Prekrývanie metód životného cyklu

Ak prekryjeme onCreate(), vieme reagovať na situáciu, keď sa služba vytvorí:

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

    Log.i(LOG_TAG, "Presence service created");
}

Takisto vieme reagovať na moment, keď sa služba plánuje zničiť, či už preto, že dobehla, alebo preto, že sa v systéme minuli zdroje a treba ju odstreliť:

@Override
public void onDestroy() {
    Log.i(LOG_TAG, "Presence service destroyed");

    super.onDestroy();
}

V oboch prípadoch nezabúdame volať rodičovské metódy!

Pri štarte aktivity uvidíme, že sa spustí služba, vykoná sa výpis do logu a ihneď sa služba ukončí.

Kontaktovanie servera

Ak má služba kontaktovať server a sťahovať dáta, musíme dodať príslušný kód. Opäť využijeme PresenceDao, ktorý pridáme do nášho intent servicu ako inštančnú premennú:

private PresenceDao presenceDao = new PresenceDao();

Použitie v metóde bude jednoduché:

@Override
protected void onHandleIntent(Intent intent) {
    List<String> users = presenceDao.loadUsers();
    Log.i(getClass().getName(), "Downloaded user list: " + users.size() + " are present");
}

Notifikácie: indikácia, že služba beží!

Niektoré služby naozaj nepotrebujú používateľské rozhranie v podobe ťažkotonážnej aktivity. (Napr. sťahovanie súboru nepotrebuje aktivitu, ktorá zaberie celý displej progressbarom...) Napriek tomu je často vhodné indikovať, že sa niečo na pozadí naozaj deje: nie je nič horšie než naspúšťať stopäťdesiat skrytých služieb, ktoré zožerú všetku baterku a nechať používateľa sa veľmi diviť, čo sa to všetko s jeho mobilom deje.

Notifikácie sú výbornou možnosťou, ako nemať GUI a zároveň indikovať progres.

Budeme potrebovať:

  1. Získať inštanciu NotificationManagera, čo je správca notifikácií zodpovedný za ich zobrazovanie, skrývanie a obsluhu udalostí.
  2. Vytvorenie inštancie Notification.Builder, ktorý vybuduje notifikáciu Notification.
  3. Zobrazenie vybudovanej notifikácie pomocou NotificationManagera.

Celý kód

Do služby hoďme pomocnú metódu, ktorá zobrazí notifikáciu:

private void triggerNotification(List<String> users) {
    Notification notification = new Notification.Builder(this)
            .setContentTitle("Presentr")
            .setContentText("Počet ľudí v miestnosti: " + users.size())
            .setContentIntent(getEmptyNotificationContentIntent())
            .setTicker("Presentr")
            .setAutoCancel(true)
            .setSmallIcon(R.mipmap.ic_launcher)
            .getNotification();

    NotificationManager notificationManager
            = (NotificationManager) getSystemService(
            Context.NOTIFICATION_SERVICE);
    notificationManager.notify("Presentr", 0, notification);
}

Rozoberme si postupne túto metódu.

Vybudovanie notifikácie

Na vybudovanie notifikácie sa nepoužíva konštruktor, ale builder, teda pomocná trieda Notification.Builder, ktorá umožňuje nastaviť všakovaké vlastnosti notifikácie a následne získať hotový objekt. (Kiežby sa builder používal aj pri príprave SQL dopytov...)

Postupne nastavujeme:

  • veľký nadpis v notifikácii (content title)
  • podnadpis v notifikácii (content text)
  • intent, ktorý sa má vykonať po kliknutí na notifikáciu (content intent). O tomto porozprávame ešte nižšie.
  • text, ktorý sa zjaví, ak sa notifikácia zjaví po prvý krát (ticker)
  • auto cancel, teda zrušenie notifikácie po kliknutí na ňu
  • a malú ikonku.

Nezabudnime po vybudovaní získať objekt notifikácie pomocou metódy getNotification().

Objekt notifikácie musí obsahovať prinajmenšom:

  • malú ikonu (cez setSmallIcon())
  • veľký nadpis (cez setContentTitle())
  • detail notifikácie (set setContentText())

Ak niektorú z týchto požadovaných vlastností vynecháte, notifikácia sa jednoducho nezobrazí, čo môže byť dosť frustrujúce.

Content Intent -- co zrobic po kliknutí?

Notifikácie nemusia slúžiť len na zobrazovanie informácií. Neraz sa čaká, že po kliknutí na ne sa spustí ďalšia aktivita: ak príde notifikácia o SMSke, asi si tú správu chcete prečítať; ak sa zjaví notifikácia o neprijatom hovore, chcete možno zavolať danému človeku.

Na to slúži content intent: jednoducho deklarujete intent, ktorý sa odošle po kliknutí na správu.

To však nie je náš prípad: u nás sa na content intent vykašleme. Ak používateľ klikne na notifikáciu, nestane sa nič špeciálne: notifikácia sa zruší vďaka nastavenému auto cancel.

Content intent však nemôžeme vynechať: Android sa začne sťažovať výnimkou. Na vytvorenie prázdneho intentu poslúži pomocná metóda:

public PendingIntent getEmptyNotificationContentIntent() {
    int REQUEST_CODE = 0;
    int NO_FLAGS = 0;

    PendingIntent contentIntent = PendingIntent.getActivity(this, REQUEST_CODE, new Intent(), NO_FLAGS);
    return contentIntent;
}

Pending intent predstavuje akéhosi prostredníka, ktorý umožní odoslať intent s takými právami a za rovnakých okolností, ako keby to robila samotná aplikácia, a to i vtedy, ak príslušná aktivita už/ešte nebeží. (Nezabudnite, pokojne sa môže stať, že download beží, i keď GUI aktivity neexistuje.).

Inými slovami, pending intent je bežný intent, ku ktorému priradíme kľúč (token) umožňujúce „otvoriť pomyselné dvere aktivity tak, akoby to robila samotná aplikácia“.

Vytvoríme teda inštanciu pending intentu nad prázdnym intentom (new Intent()), s implicitnými nulovými hodnotami v ostatných parametroch a so štandardným kontextom (keďže služba je zároveň kontextom).

Získanie NotificationManagera

Získanie manažéra notifikácií je jednoduché: ide o systémovú službu:

NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

Zobrazenie notifikácie

Toto je jednoduché:

notificationManager.notify(USER_LIST_NOTIFICATION_ID, notification);

Metóda notify() potrebuje dva parametre: číslo identifikujúce konkrétnu notifikáciu a samotný objekt notifikácie. ID notifikácie je unikátne pre celú aplikáciu a dá sa využiť na aktualizáciu existujúcej notifikácie: stačí druhýkrát notifykovať manažéra s identickým identifikátorom a aktualizovaným objektom Notification.

Použitie v kóde

Použitie v kóde jednoducho vytiahne zoznam používateľských mien a pošle ho do našej pomocnej metódy.

@Override
protected void onHandleIntent(Intent intent) {
    List<String> users = presenceDao.loadUsers();
    Log.i(getClass().getName(), "Downloaded user list: " + users.size() + " are present");
    triggerNotification(users);
}

Komunikácia od IntentService k aktivite

Napriek tomu, že služby nemajú používateľské rozhranie, a typicky sa s používateľom „rozprávajú“ cez notifikácie, existujú situácie, keď chceme, aby komunikovali s používateľmi.

Priamočiary spôsob využíva vzor broadcast manager-broadcast receiver, kde sa do „éteru“ vysielajú správy reprezentované intentami tak, ako to robí bežný rozhlas. Správy môžu prijímať poslucháči, broadcast receivers, ktorí ich môžu spracovať. V našom prípade bude vysielačom služba a prijímačom aktivita.

Vysielanie intentov

Služba môže vysielať správy reprezentované intentami kamsi von, do éteru, a nemusí sa vôbec starať, či ich niekto prijme. Ako máme chápať éter?

Jedna z možností je poslať správu do celého systému, a to pomocou metódy sendBroadcast(). To je však v tomto prípade prílišné mrhanie prostriedkami.

Ak chceme posielať správu len v rámci komponentov aplikácie, vieme využiť sprostredkovateľa v podobe triedy LocalBroadcastManager. Do tohto objektu pošleme intent, ktorý sa rozdistribuujeme všetkým zaregistrovaným poslucháčom.

V službe si teda vytvorme pomocnú metódu broadcastPresence()

private void broadcastPresence(List<String> users) {
    LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(this);
    Intent intent = new Intent(PRESENCE_INTENT_ACTION);
    intent.putExtra(PRESENCE_INTENT_EXTRAS, (Serializable) users);

    broadcastManager.sendBroadcast(intent);
}

Potrebujeme dohodnúť dve záležitosti:

  • názov akcie reprezentovaný unikátnym reťazcom, čo dosiahneme cez názov balíčka.
  • dáta (extras) v intente so zoznamom používateľov.

Využijeme pri tom dve konštanty:

public static final String PRESENCE_INTENT_ACTION = PresenceService.class.getPackage().getName() + ".Presence";
public static final String PRESENCE_INTENT_EXTRAS = "userList";

Ak máme inštanciu lokálneho vysielača, cez metódu sendBroadcast() odošleme intent do éteru.

Metódu broadcastPresence() zavolajme na konci onHandleIntent(), čím zaručíme vysielanie správy.

Prijímač BroadcastReceiver

Prijímačom broadcastovaných intentov je podtrieda triedy BroadcastReceiver, ktorá v metóde onReceive() spracováva prijatý intent. V našom prípade bude prijímačom vnorená trieda, ktorá môže aktualizovať zoznam používateľov. (Samozrejme, bude duplikovať loader, ale opäť: to všetko je príprava na periodické spúšťanie úlohy.)

Prijímač vytvoríme nasledovne:

  1. vytvoríme vnútornú triedu implementujúcu poslucháča.
  2. implementujeme metódu onReceive()
  3. obslúžime registrovanie a odregistrovanie tohto poslucháča na lokálnom broadcast manažérovi.

Vytvorenie triedy poslucháča

Triedu poslucháča vytvoríme vo vnútri MainActivity. Ako vidno, jednoducho vytiahneme z intentu príslušný zoznam používateľov spod príslušného kľúča v extras a aktualizujeme adaptér.

private class PresenceBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        List<String> users = (List<String>) intent.getSerializableExtra(PresenceService.PRESENCE_INTENT_EXTRAS);
        adapter.clear();
        adapter.addAll(users);
    }
}

Prijímač, ktorý zaregistrujeme ručne v aktivite, pobeží v hlavnom vlákne. Metóda onReceive() preto musí zbehnúť rýchlo (inak brzdí používateľské rozhranie) a platia pre ňu rovnaké obmedzenia, ako sme spomínali pri AsyncTasku.

Dôkaz tohto tvrdenia sa nachádza v dokumentácii metódy registerReceiver() triedy Context.

Registrácia prijímača

Poslucháča môžeme registrovať dvoma spôsobmi:

  1. Deklaratívne v manifeste, čo je užitočné pre prijímače, ktoré nepotrebujú pracovať s GUI. (To nie je prípad našej aplikácie. Podrobnosti pre túto možnosť udáva dokumentácia Androidu.)
  2. Manuálne v rámci aktivity.

Ručná registrácia prijímača pozostáva z dvoch fáz:

  • v metóde onResume() poslucháča zaregistrujeme
  • v metóde onPause() ho deregistrujeme

Ak je aktivita pozastavená, jej používateľské rozhranie je skryté, a preto nie je dôvod, aby prijímala intenty: koniec koncov, výsledok aj tak nebude viditeľný.

Filtre intentov

Správa odoslaná do éteru môže mať množstvo poslucháčov, ale i naopak: éterom môže putovať obrovské množstvo správ. Aby sa dosiahla rozumná výkonnosť pri spracovávaní správ, musíme povedať, ktorým správam náš prijímač porozumie. Dosiahneme to cez filter intentov.

Intent Filter (filter intentov) predstavuje akýsi firewall na intenty postavený pred aplikačný komponent. Predstavuje pravidlo, ktoré špecifikuje intenty, na ktoré dokáže komponent reagovať, a ktoré je potrebné odfiltrovať.

Filter intentov vie intent prijať či zamietnuť podľa troch vlastností: podľa akcie (action), dát (data) a kategórie (category). My si ukážeme rozhodovanie podľa akcie.

Ak chceme prijímať len intenty odosielané našou službou, stačí prijať tie, ktoré majú nastavenú akciu na konštantu sk.upjs.ics.android.presentr.PresenceService#PRESENCE_INTENT_ACTION.

Dosiahneme to cez intent filter:

IntentFilter filter = new IntentFilter(PresenceService.PRESENCE_INTENT_ACTION);

Implementácia registrácie

Ako sme spomínali vyššie, registráciu poslucháča vykonáme v metóde onResume() aktivity:

@Override
protected void onResume() {
    super.onResume();

    IntentFilter filter = new IntentFilter(PresenceService.PRESENCE_INTENT_ACTION);
    this.presenceBroadcastReceiver = new PresenceBroadcastReceiver();
    LocalBroadcastManager.getInstance(this).registerReceiver(this.presenceBroadcastReceiver, filter);
}

Vytvoríme teda intent filter s príslušným pravidlom, následne vytvoríme nášho poslucháča a poznačíme si ho do inštančnej premennej a nakoniec ho zaregistrujeme v lokálnom broadcast manažérovi.

Inštančná premenná je dôležitá, budeme ju totiž potrebovať pri odregistrovávaní poslucháča:

private PresenceBroadcastReceiver presenceBroadcastReceiver;

Implementácia deregistrácie

V Androide platí, že vždy po sebe musíme upratať: inak máme memory leak, teda zaberáme pamäť, ktorá sa nedá uvoľniť. Odregistrovanie urobíme v metóde onPause() aktivity a bude veľmi jednoduché:

@Override
protected void onPause() {
    LocalBroadcastManager.getInstance(this).unregisterReceiver(this.presenceBroadcastReceiver);
    super.onPause();
}

Jednoducho odregistrujeme inštanciu poslucháča, ktorú máme v inštančnej premennej.

Periodické spúšťanie služieb

Poďme teraz k veľkému finále: máme službu, ktorá broadcastuje dáta do aktivity a poďme si ju naplánovať na pravidelné spúšťanie.

Ak chceme spúšťať periodické operácie i v čase, keď aplikácia nebeží, použijeme systémovú službu android.app.AlarmManager. Operačný systém optimalizuje spúšťanie naplánovaných úloh, pričom berie do úvahu ich prioritu, stav uspatia telefónu a šetrenie baterky.

Implementácia v projekte

  1. vytvoríme si pomocnú statickú metódu na naplánovanie
  2. získajme inštanciu AlarmManagera.
  3. vytvorme intent, ktorým naštartujeme službu
  4. vytvoríme pending intent, ktorý sa spustí pri „tiku“, teda uplynutí intervalu
  5. naplánujme spúšťanie.

Vytvorme si samostatnú triedu PresenceScheduler s metódou schedule(), ktorá zoberie kontext užitočný vo viacerých metódach, a naplánuje úlohu.

Následne získajme inštanciu AlarmManagera podobne, ako sme to robili v prípade správcu notifikácií:

AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);

Spustenie služby vykonáme v dvoch fázach. Najprv vytvoríme intent, ktorým spustíme náš intent service:

Intent intent = new Intent(context, PresenceService.class);

Následne ho potrebujeme obaliť do pending intentu, pretože potrebujeme dať alarm manažérovi oprávnenie posielať intenty do služby s rovnakým oprávnením, ako by to robila naša aplikácia.

PendingIntent pendingIntent = PendingIntent.getService(context, SERVICE_REQUEST_CODE, intent, NO_FLAGS);

V tomto PendingIntente uvedieme:

  • kontext
  • kód požiadavky (request code) -- ak ho nepoužívame, pošleme napr. konštantu s nulovou hodnotou
  • intent, ktorým spustíme službu
  • príznaky (flags), používané pri odstraňovaní či modifikácii pending intentu, môžeme použiť vlastnú konštantu NO_FLAGS rovnú nule.

Pending intent následne naplánujeme na alarm manažérovi cez niektorú z metód setXXX(). Vo všeobecnosti mám e k dispozícii dve metódy:

  • setRepeating() plánuje presne, s čo najmenšími odchýlkami, ale je náročnejší na batériu.
  • setInexactRepeating() plánuje spúšťanie s istou toleranciou.

Nám nezáleží na presnosti, preto si zvolíme druhú možnosť a uvedieme parametre:

  • prvý parameter uvádza časovú škálu. Máme k dispozícii dve skupiny po dvoch možnostiach, spomedzi ktorých si zvolíme civilizovanú možnosť ELAPSED_REALTIME bez dodatku, aby sme šetrili baterku.
    • ELAPSED_REALTIME udáva počet milisekúnd od bootu, vrátane času, keď zariadenie spalo.
    • RTC udáva počet milisekúnd od začiatku unixovej epochy
    • normálna možnosť: ak zariadenie spí, alarm sa odignoruje.
    • WAKEUP: ak zariadenie spí a nastane alarm, zariadenie sa zobudí.
  • druhý parameter udáva čas prvého spustenia
  • tretí parameter udáva periodicitu v milisekundách
  • štvrtý parameter udáva pending intent, ktorý sa má spustiť pri tiku

Výsledný kód vyzerá nasledovne:

package sk.upjs.ics.android.presentr;

import android.app.*;
import android.content.*;
import android.os.SystemClock;

public class PresenceScheduler {
    private static final int SERVICE_REQUEST_CODE = 0;
    private static final int NO_FLAGS = 0;

    public static void schedule(Context context) {
        AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);

        Intent intent = new Intent(context, PresenceService.class);
        PendingIntent pendingIntent = PendingIntent.getService(context, SERVICE_REQUEST_CODE, intent, NO_FLAGS);

        alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime(), 7 * 1000, pendingIntent);
    }
}

Spustenie plánovania

Plánovanie odpálime jednoducho: v hlavnej aktivite v metóde onCreate() zavoláme:

PresenceScheduler.schedule(this);

Správca alarmov následne začne periodicky spúšťať našu službu, ktorá každých 7 sekúnd stiahne údaje zo servera, broadcastne ich, a následne aktivitový poslucháč prijme zoznam prihlasovacích mien, aktualizuje adaptér, čo povedie k prekresleniu zoznamu.

Prehľad naplánovaných úloh

Prehľad naplánovaných úloh môžeme získať pomocou nástroja adp, ktorý sa nachádza v inštalačnom adresári, v podadresári sdk/platform-tools.

Volaním adb shell dumpsys alarm uvidíme výpis aktívnych alarmov:

Uvidíme tiahly výpis, z ktorého nás bude zaujímať len:

  ELAPSED #0: Alarm{5368fe78 type 3 sk.upjs.ics.android.presentr}
    type=3 when=+6s28ms repeatInterval=7000 count=1
    operation=PendingIntent{53615cb4: PendingIntentRecord{537263e0 sk.upjs.ics.android.presentr startService}}

a

  Alarm Stats:

  sk.upjs.ics.android.presentr
    15ms running, 0 wakeups
    28 alarms: flg=0x4 cmp=sk.upjs.ics.android.presentr/.PresenceService

Vidíme, že máme naplánovaný alarm so sedemsekundovou periódou, ktorý zatiaľ prebehol 28 krát.

Porovnanie jednotlivých asynchrónnych tried

AsyncTask

Účel:

  • pre činnosti na pozadí, ktoré nesmú brzdiť UI,
  • ...ale ktorých životnosť je kratšia než životnosť aktivity (trvanie v jednotkách sekúnd).
  • pre činnosti, ktoré nepotrebujú garantovanú spätnú väzbu od používateľa po dobehnutí

Technický popis:

  • analógia SwingWorkera

Problémy:

  • nesmie držať referenciu na vonkajšiu aktivitu (inak nastáva memory leak)
  • ak je aktivita odstrelená, AsyncTask môže nechtiac meniť aktivitu, ktorá už neexistuje alebo je neplatná, a používateľ ju už neuvidí
  • pauznutie aktivity nepauzuje AsyncTask

Hack:

  • využitie headless fragmentov

AsyncTaskLoadery

Účel:

  • pre asynchrónne načítavanie dát v rámci jednej aktivity
  • zvláda zmenu konfigurácie a reštart aktivity

Technický popis:

  • rovnaké API ako načítavanie z databázy
  • zvláda bez problémov zmeny konfigurácie
  • možno dopracovať podporu pre cacheovanie dát

Problémy:

  • v onLoadFinished() sa nedajú meniť fragmenty

IntentService

Účel:

  • pre dlhotrvajúce úlohy, ktoré nepotrebujú používateľské rozhranie
  • pre činnosti s dlhou životnosťou, ktoré bežia nezávisle od aktivít

Technický popis:

  • má front správ (intentov), ktoré spracováva v samostatnom vlákne
  • ak sa front vyprázdni, služba skončí
  • vysoká priorita behu (služby sa killujú až po aktivitách)

Problémy:

  • s používateľom komunikuje notifikáciami
  • s ostatnými komponentami komunikuje cez broadcasty alebo handlery

Výsledný projekt

Výsledný projekt má zdrojové kódy na GitHub.com.

Zdroje