4. stretnutie: História volaní Callgrid

Cieľové elementy

Vytvorme aplikáciu, ktorá zobrazí históriu volaní v utešenej mriežke a la Windows Phone.

Koncepty, ktoré zvládneme

  • existujúce content providery
  • korektná práca s kurzormi na pozadí cez loadery
  • mriežka položiek GridView
  • vlastný layout položiek cez ViewBinder
  • farby ako resources

Výsledok

Príprava projektu

V Android Studiu vytvorme nový projekt CallGrid s jedinou aktivitou. Kompilovať budeme oproti platforme 4.0.3.

Nainštalujte si pôvodný emulátor pre Android 4.0.3

V projekte budeme využívať prichádzajúce, odchádzajúce a zmeškané hovory. Žiaľ, emulátor Genymotion podporuje tieto vlastnosti len v platených verziách, čo nás núti využiť len pomalší pôvodný emulátor.

Míľnik 1: Natvrdo zadané údaje v mriežke

Zobrazovanie údajov v mriežke

Jestvuje mnoho pekných situácií pre použitie mriežky namiesto zoznamu. Telefónne čísla, vlajky štátov, galéria obrázkov a tak ďalej. Ak je každá položka pomerne krátka, dokážeme tak na displeji zobraziť omnoho viac údajov a zároveň prehľadnejšie, než v prípade zoznamu. (Nehovoriac o tom, že položky zoznamu v ListView sa na širokom displeji zobrazujú do priveľkej šírky.)

Na mriežkové dáta máme v Androide dva spôsoby: buď využijeme špeciálny layout manager alebo si poradíme s mriežkovým komponentom.

Mriežkový layout manager je reprezentovaný triedou GridLayout, do ktorého naukladáme komponenty. Jeho nevýhodou je nedostupnosť v starších Androidoch (do API Level 13).

Pre nás bude oveľa jednoduchšie použiť dedikovaný komponent GridView, ktorý má API podobné klasickému zoznamu ListView, ale namiesto ukladania položiek pod seba podporuje presne mriežkovú reprezentáciu. Veľká výhoda spočíva vo využití adaptérov, teda modelov s dátami, ktoré sme si ukázali v minulom dieli.

Prispôsobenie layoutu aktivity

Do XML súboru pre layout aktivity dodajme komponent GridView:

<GridView
    android:id="@+id/callLogGridView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:numColumns="auto_fit"
    android:columnWidth="90dp"
    android:listSelector="@null"
    />

Layoutu pridáme nasledovné atribúty:

  • identifikátor, na ktorý sa odkážeme v kóde aktivity
  • konfiguráciu veľkosti: i výšku i šírku nastavíme na match_parent, pretože komponent zaberie celú aktivitu.
  • dynamický počet stĺpcov nastavíme v atribúte numColumns s hodnotou auto_fit. Ak nastavíme šírku stĺpca columnWidth na pevnú hodnotu, pri otáčaní na šírku budú pribúdať nové stĺpce.
  • ak sa chceme zbaviť svetlého okraja (paddingu) okolo GridViewu, vymažme všetky atribúty android:padding na LinearLayoute a vypnime vizuálny indikátor kliknutia na položu reprezentovaný tzv. list selectorom listSelector (nastavme ho na @null).
  • zároveň nastavme horizontálny a vertikálny rozostup medzi jednotlivými bunkami na 90 dp.

Naplnenie mriežky dátami

V tejto fáze naplníme mriežku údajmi z adaptéra, ktorý sme videli už minule. Definujeme si ArrayAdaptér obsahujúci reťazce, ktorý naplníme vzorovými telefónnymi číslami.

String[] adapterData = { "0905223223", "0905123456", "112", "055123456" };            
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, android.R.id.text1, adapterData);

Podľa zásad z minula bude každá položka mriežky využívať zabudovaný layout s jedinou textovou položkou (android.R.layout.simple_list_item_1) a reťazec s telefónnym číslom namapuje na túto textovú položku, ktorej systémový identifikátor je android.R.id.text1.

Adaptér potom asociujeme s GridViewom cez metódu setAdapter():

Kompletný kód vyzerá nasledovne:

public class MainActivity extends Activity {

    private GridView callLogGridView;

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

        String[] adapterData = { "0905223223", "0905123456", "112", "055123456" };            
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, android.R.id.text1, adapterData);

        callLogGridView = (GridView) findViewById(R.id.callLogGridView);
        callLogGridView.setAdapter(adapter);
        callLogGridView.setOnItemClickListener(this);
    }
}

Po spustení uvidíme neveľmi vábnu aplikáciu, ale aspoň máme možnosť sledovať tvorbu mriežky i preskupovanie jej stĺpcov, ak začneme točiť zariadením:

Miľník 2: GridView nad dátami z content providera

Doposiaľ sme dáta ťahali z poľového adaptéra, kde boli zadané natvrdo v kóde. Je čas ich ťahať zo skutočných dát v telefóne!

Content Providery a kurzory

Androiďácke zariadenie nesie v sebe množstvo interných údajov: či už ide o kontakty, SMSky, históriu volaní, údaje v kalendári, fotky v galérii alebo MP3ky či videá. Každý typ dát sa pritom môže nachádzať na rozličných miestach, či už v relačnej databáze, alebo v súborovom systéme (to sa týka MP3jek a fotiek) alebo kdekoľvek inde.

Content Provider (poskytovateľ obsahu) je mechanizmus, ktorý dokáže sprístupniť ľubovoľné dáta jednotným spôsobom bez ohľadu na konkrétny spôsob ich uloženia.

Každý content provider v systéme poskytuje konkrétny typ dát -- máme teda content providera pre fotky, iného providera pre históriu volaní, či onakého providera pre kontakty.

A keďže už starí Rimania vedeli, že ukladať dáta relačným spôsobom je osvedčená metóda, content provider ju preberá a využíva. Dáta sú vždy chápané relačným, teda tabuľkovým spôsobom, kde údaje sú uložené v akýchsi riadkoch, ich vlastnosti (atribúty), sú uložené v pomenovaných stĺpcoch. Každý content provider tak hovorí, aké ,,tabuľky" chce sprístupniť a aké ,,stĺpce" ponúka.

Všetky zabudované providery sú v Androide dostupné v balíčku android.content a sú reprezentované bežnými triedami. Každý provider má v API niekoľko vnútorných tried, ktoré zodpovedajú jeho tabuľkám. A ak sa ponoríme ešte hlbšie, zistíme, že každá tabuľková trieda definuje jednak sadu konštát zodpovedajúcu názvom stĺpcom a jednak špeciálnu konštantu CONTENT_URI, ktorá reprezentuje jednoznačný systémový identifikátor (URI), pomocou ktorého vieme k danej "tabuľke" providera pristúpiť.

Content provider pre históriu volaní

Vezmeme si príklad histórie volaní. V dokumentácii sa dozvieme, že existuje trieda content providera android.provider.CallLog, ktorá obsahuje jedinú "tabuľkovú" podtriedu CallLog.Calls. Jednoznačný identifikátor tejto tabuľky je definovaný v konštante URI konštante CallLog.Calls.CONTENT_URI a jednotlivé stĺpce sú špecifikované jednak názvami (napr. NUMBER a jednak popisom dátového typu v dokumentácii.)

Kurzory

K tabuľkovým dátam pristupujeme pomocou kurzorov.

Kurzor je objekt, ktorý reprezentuje výsledok dopytu (teda tabuľkové dáta) na content providera. Plní dvojakú úlohu: jednak obsahuje celý výsledok a jednak predstavuje akýsi kurzor, či ukazovateľ, ktorý stále mieri na konkrétny riadok vo výsledku.

Získať dáta z content providera znamená získať kurzor, ktorý je na začiatku nastavený pred prvý riadok, a postupne sa bude posúvať po jednotlivých riadkoch výsledku až na koniec tabuľkových dát.

O tom, ako konkrétne získať kurzor z content providera, si povieme neskôr, pretože ešte musíme zvládnuť dve dôležité veci: porozprávať sa o loaderoch a zmeniť typ adaptéra pre GridView.

Kurzorové adaptéry

Doposiaľ sme používali poľový adaptér nad pevnými dátami, ale ten už nestačí. Vyhoďme teda jeho inicializáciu z kódu.

Našťastie, kurzory sú natoľko častá technika dát, že ním bola venovaná špeciálna trieda pre adaptéry (teda modely) zoznamu. Používajú sa totiž nielen pri práci s content providermi, ale tiež pri SQL databázach.

Nazýva sa SimpleCursorAdapter a pri vytváraní potrebuje šesť parametrov. Výsledok si poznačme do inštančnej premennej:

this.adapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_1, NO_CURSOR, from, to, NO_FLAGS);
  1. this predstavuje kontext, v tomto prípade vlastniacu aktivitu
  2. druhý parameter obsahuje odkaz na layout jednotlivých položiek v zozname. V tomto prípade použijeme zabudovaný layout android.R.layout.simple_list_item1, kde položku tvorí jeden reťazec. (Tento layout sme použili už pri zozname úloh.)
  3. kurzor obsahujúci dáta zatiaľ nemáme k dispozícii, čo vyriešime hodnotou null. Pre čitateľnosť si definujeme konštantu NO_CURSOR, ktorá sa nám hodí ešte na jednom mieste v budúcnosti:

        public static final Cursor NO_CURSOR = null;
    
  4. pole názvov stĺpcov z tabuľky získanej z content providera, ktoré sa zobrazia v podpoložkách položky zoznamu (pozri nižšie)

  5. pole identifikátorov widgetov, na ktoré sa namapujú hodnoty zo stĺpcov tabuľky.
  6. špeciálne príznaky, ktoré nevyužijeme a preto ich dodáme v podobe našej vlastnej konštanty NO_FLAGS s hodnotou 0.

Štvrtý a piaty parameter si ukážme na príklade:

Z tabuľky histórie volaní sa rozhodneme zobrazovať len posledné telefónne číslo, čomu zodpovedá stĺpec CallLog.Calls.NUMBER (zistili sme to z dokumentácie k tabuľke Calls v content provideri CallLog). Pole from teda bude obsahovať len jeden prvok s názvom stĺpca:

String[] from = { CallLog.Calls.NUMBER };

Layout pre celú položku definovaný v druhom parametri, ktorý je v tomto prípade zabudovaný androidovský layout android.R.layout.simple_list_item_1, obsahuje len jeden widget, a to s identifikátorom android.R.id.text1. Pole to bude teda vyzerať:

int[] to = { android.R.id.text1 };

Na prvý prvok z poľa from sa teda bude mapovať widget z prvého prvku poľa to. Ak by widgetov v to bolo viac, tak i-ty prvok z poľa from sa bude mapovať na i-ty widget z poľa to.

Ak máme hotový adaptér pre zoznamy, stačí ho asociovať s mriežkou cez:

callLogGridView.setAdapter(adapter);

Konštruktor SimpleCursorAdaptera

public class MainActivity extends ActionBarActivity {

    public static final Cursor NO_CURSOR = null;
    public static final int NO_FLAGS = 0;

    private SimpleCursorAdapter adapter;

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

        String[] from = { CallLog.Calls.NUMBER };
        int[] to = { R.id.grid_item_text };
        adapter = new SimpleCursorAdapter(this, R.layout.grid_item, NO_CURSOR, from, to, NO_FLAGS);

        /*...*/
 }

Loadery

Povedzme si to rovno: prístup k dátam content loadera je drahý a pomalý. Samozrejme, nie taký pomalý, aby sme ho museli úplne zavrhnúť, ale predsa dosť pomalý na to, aby jeho nesprávne používanie viedlo k pomalému a nepoužiteľnému používateľskému rozhraniu.

Celé používateľské rozhranie Androidu totiž beží v jednom vlákne. Ak pracujeme s UI, každá naša aktivita (ťapnutie prstom, posun, písanie po klávesnici) vyvoláva vždy novú udalosť, ktorá sa postupne zaraďuje do frontu udalostí. Vlákno používateľského rozhrania vyberá jednotlivé udalosti zo začiatku frontu, spracováva ich a postupne prekresľuje používateľské rozhranie. Ak sa však vo fronte zjaví udalosť, ktorá trvá veľmi dlho, prekresľovanie prestane byť plynulé a používateľ znervóznie a nadobudne pocit pomalosti, ktorý následne vyventiluje na fórach v príspevkoch s textom "Android je pomalý".

Keďže databázových aktivít je ako maku, v Androide 4.x sa objavilo API v podobe loaderov, ktoré tento problém riešia spoľahlivým spôsobom.

Loader je objekt, ktorý načítava ľubovoľné dáta, typicky kurzory, v separátnom vlákne na pozadí. Načítané dáta dokáže preklopiť do vlákna používateľského rozhrania, odkiaľ sa dajú zobraziť v jednotlivých komponentoch.

Správne implementovaný loader dodržiava životný cyklus aktivity, korektne spravuje kurzory a uvoľnuje ich, ak ich už netreba a zároveň spoľahlivým spôsobo upozorňuje adaptéry na zmenené či nové dáta, a tým garantuje správne prekresľovanie widgetov.

Kurzorové loadery

CursorLoader je loader, ktorá dokáže načítať dáta v podobe Cursora z content providera. Na načítavanie mu stačí Uri adresa content providera.

Kurzorový loader je najčastejšie používaný loader: nielen v prípade content providerov, ale i v prípade, že budeme v budúcnosti budovať databázové aplikácie.

Prepojenie loadera, content providera a aktivity

Už vieme, čo je content provider, vieme (aspoň teoreticky) využívať kurzory, všeobecne vieme o užitočnosti loaderov, a teraz je čas to prepojiť.

Plán práce bude nasledovný:

  • v aktivite inicializujeme loader pomocou správcu loaderov (loader manager)
  • loader manager bude notifikovať našu aktivitu o dôležitých javoch, ktoré nastanú v loaderi. Najdôležitejšie javy sú:
    • je potrebné vytvoriť nový loader
    • loader skončil načítavanie dát na pozadí a má ich pripravené.
  • aktivita bude reagovať na udalosti a v nich spravovať loader, využívať jeho dáta a preklápať ich do adaptérov, z ktorých budú čerpať dáta widgety

Loader Manager

Jedna aktivita môže načítavať dáta z viacerých loaderov: nie je vylúčené, že vyťahujete dáta z content providera pre kontakty, a zároveň potrebujete i dáta z histórie volaní.

Loader Manager je objekt, ktorý je prostredníkom medzi aktivitou a loadermi, ktoré sa v nej majú využívať.

Inicializácia loadera

Konkrétny loader inicializujeme v aktivite prostredníctvom loader managera, a to typicky v jej metóde onCreate():

getLoaderManager().initLoader(LOADER_ID_GRIDVIEW, NO_BUNDLE, this);

Inicializácia potrebuje tri parametre:

  • loaderId: číselný identifikátor loadera, ktorý si zvolíme podľa potreby. Ak máme len jeden loader, môžeme si si definovať vlastnú konštantu public static final int LOADER_ID_GRIDVIEW = 0. Použitie konštánt nám sprehľadní kód.
  • bundle: kýblik pre prenos dát medzi aktivitou a loaderom. V tomto prípade bundle nepoužijeme a preto môžeme použiť null. (Je užitočné si definovať konštantu NO_BUNDLE, ktorá sprehľadní čítanie.)
  • loader callback: trieda, ktorá bude reagovať na udalosti loadera a spravovať dáta. Touto triedou bude samotná aktivita, ale prečo? Povedzme si o nej viac.

Loader Callbacks

Každý loader vie odosielať správy (udalosti) o svojom stave, na ktoré môže reagovať ľubovoľný poslucháč. Tieto udalosti sú tri:

  • je potrebné vytvoriť nový loader
  • načítavanie dát z loadera skončilo, sú dostupné a možno ich využiť v používateľskom rozhraní
  • dáta z loadera už nie sú aktuálne, aplikácia by ich mala zneplatniť, uvoľniť, či zrušiť odkazy na ne

V praxi sa poslucháčom stane aktivita, ak implementuje interfejs LoaderCallbacks, a dodá kód do jeho troch metód, ktoré zodpovedajú trom udalostiam.

  • vytváranie loaderov: onCreateLoader()
  • dáta sú načítané: onLoaderFinished()
  • doterajšie dáta sú neplatné: onLoaderReset().

Ak to naplánujeme ešte podrobnejšie, tak:

  • necháme aktivitu implementovať LoaderCallbacks
  • inicializujeme loader
  • implementujeme tri metódy:
    • v onCreateLoader() vytvoríme kurzorový loader CursorLoader nasmerovaný na Uri content providera histórie volaní
    • v onLoaderFinished() dostaneme kurzor Cursor s novými dátami, ktorý priradíme do adaptéra mriežky
    • v onLoaderReset() odpojíme kurzor od adaptéra mriežky.

Ukážka prepojenia

Nechajme našu aktivitu implementovať interfejs LoaderCallbacks:

public class MainActivity extends ActionBarActivity implements LoaderManager.LoaderCallbacks<Cursor>

Generický typ Cursor znamená, že chceme pracovať s kurzorovými loadermi.

V metóde onCreate() inicializujeme loader prostredníctvom loader managera:

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    getLoaderManager().initLoader(LOADER_ID_GRIDVIEW, NO_BUNDLE, this);

}

Doimplementujme kód do troch metód (spravíme to hneď) a prepojme získané dáta s mriežkou cez adaptér.

Ukážka callback metód

Vytvorenie loadera cez onCreateLoader()

Metóda onCreateLoader() je zavolaná loader managerom vo chvíli, keď má vzniknúť nový loader. Keďže v našom prípade využívame content providery, veľmi sa hodí CursorLoader, ktorý využijeme nasledovne:

@Override
public Loader onCreateLoader(int id, Bundle args) {
    if(id == LOADER_ID_GRIDVIEW) {
        CursorLoader loader = new CursorLoader(this);
        loader.setUri(CallLog.Calls.CONTENT_URI);
        return loader;
    }
    return null;
}

Metóda má dva parametre: id reprezentujúci identifikátor loadera, ktorý sa má vytvoriť (ide o rovnaký identifikátor, aký bol uvedený v metóde initLoader()) a voliteľný bundle. Pomocou if-u zistíme, ktorý loader je potrebné vytvoriť.

V CursorLoaderi definujeme URI adresu content providera a príslušnej tabuľky (teda jeho content URI). Ak nenastavíme na loaderi žiadne iné vlastnosti, znamená to, že chceme načítať celú históriu volaní.

Reakcia na načítanie dát

Ak sa kurzorovému loaderu podarí na pozadí načítať dáta, zavolá sa metóda onLoadFinished(), a dáta sa ocitnú v druhom argumente. U nás načítavame kurzory, čo znamená, že dostaneme k dispozícii Cursor.

Čo s máme spraviť s dátami? Potrebujeme ich zobraziť v mriežke (ešte sme nezabudli na mriežku?). A ako ich dopravíme do mriežky? Cez adaptér!

Adaptér SimpleCursorAdapter máme definovaný v inštančnej premennej adapter, kurzor s dátami dostaneme v parametri metódy, a prepojíme ich volaním metódy swapCursor().

@Override
public void onLoadFinished(Loader loader, Cursor cursor) {
    adapter.swapCursor(cursor);
}

Reakcia na zneplatnenie dát

Ak náhodou prestanú byť dáta v kurzore aktuálne (napr. sa zmenili dáta v content provideri), alebo je loader odsúdený na zánik, potrebujeme zrušiť prepojenie medzi nimi a adaptérom. Obvyklé riešenie je prosté: zavoláme metódu swapCursor() do ktorej dodáme null. Samozrejme, opäť kvôli prehľadnosti zrecyklujeme konštantu pre "žiadny kurzor".

@Override
public void onLoaderReset(Loader loader) {
    adapter.swapCursor(NO_CURSOR);
}

Prístupové práva k citlivým dátam

Ak by sme spustili aplikáciu už teraz, uvideli by sme Force Close, teda pád ihneď po štarte, a v logcate.

Naša aplikácia totiž pristupuje k citlivým údajom. (Chceli by ste mať svoju históriu údajov odosielanú na pochybný server?).

Aplikácie v Androide majú veľký potenciál, ale to v sebe nesie aj bezpečnostné riziká. Telefonovanie na thajské čísla na pozadí, odosielanie SMSiek bez súhlasu používateľa, využívanie internetu alebo fotoaparátu.

Riešenie tohto problému spočíva v definícii systému oprávnení (permissions). Android ako platforma definuje zoznam potenciálnych oprávnení, a každá z aplikácii určí, ktoré oprávnenia potrebuje využívať.

Naša aplikácia potrebuje čítať z histórie volaní, čomu zodpovedá oprávnenie android.permission.READ_CONTACTS (resp. v novších verziách Androidu android.permission.CALL_PHONE). V budúcich fázach aplikácie budeme tiež potrebovať telefonovať, a teda žiadať o oprávnenie android.permission.CALL_PHONE.

Oprávnenia v manifeste

Oprávnenia sa definujú v manifeste. Uvedené požiadavky uvedieme do elementu <manifest> v elementoch <uses-permissions>

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

Oprávnenia v Androide sú pokazené

Požadované oprávnenia sa zobrazia používateľovi pri inštalovaní aplikácie z trhu aplikácií Google Play (v emulátore je táto možnosť potlačená).

Žiaľ, zodpovednosť za bezpečnosť je ponechaná na používateľa. Ak používateľ nesúhlasí s ktorýmkoľvek z oprávnení, aplikácia sa vôbec nenainštaluje. Nie je možné odoprieť konkrétne oprávnenia -- teda ak používateľ chce hrať pochybný Tetris vyžadujúci telefonovanie, nemôže mu odoprieť právo telefonovať a ponechať si ostatné oprávnenia.

Typický používateľ tak často "odklikne" oprávnenia len preto, aby sa aplikácia nainštalovala.

Aplikácia beží!

Teraz, keď sa tešíme z aplikácie, ktorá zobrazuje dáta z content providera, dajme si pauzu a po nej doimplementujme obsluhu kliku na položku.

Zmena poslucháča na kliknutie na položku

Ak chceme reagovať na kliknutia na položky, stačí na objekte GridView zaregistrovať poslucháča typu AdapterView.OnItemClickListener a to pomocou metódy setOnItemClickListener().

Keďže naša aktivita bude obsahovať len jediný komponent, môžeme za poslucháča vyhlásiť práve ju, čím sa zbavíme nutnosti používať anonymnú vnútornú triedu. Nechajme aktivitu implementovať zmienený interfejs:

public class MainActivity extends ActionBarActivity 
    implements LoaderManager.LoaderCallbacks<Cursor>, 
               AdapterView.OnItemClickListener {
    ...

a dodajme kód pre jedinú metódu z tohto interfejsu:

@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {

}

V rámci tejto metódy potrebujeme zistiť, na ktorú položku sme klikli, vytiahnuť z nej telefónne číslo a niečo pekné s ním spraviť.

Pri práci s poľovým adaptérom sme využívali argument position, ktorý nám dokázal vrátiť príslušnú položku na základe jej poradia v zdrojovom zozname. V tomto prípade však berieme dáta z content providera, ktoré nemajú konkrétny definovaný typ (dáta zodpovedajú "riadkom" z "tabuliek" content providera).

Našťastie máme k dispozícii parameter long id, ktorý obsahuje primárny kľúč, čiže jednoznačný identifikátor vybraného riadka v content providerovi. Cez tento primárny kľúč sa vieme content providera spýtať na aktuálnu položku, z ktorej zistíme všetko, čo potrebujeme (primárne vybrané telefónne číslo).

Content Resolvery

Na získanie konkrétneho riadka využijeme priamy prístup ku content provideru. Dosiahneme ho však cez prostredníka, a to triedu content resolver.

Content Resolver je trieda, ktorá umožňuje pristupovať k údajom v content providerom. Poskytuje metódy pre dopytovanie, ktoré vracajú Cursor, ale i vkladanie, či mazanie dát. Z implementačného hľadiska je to továreň pre získavanie content providerov.

Ku content resolveru sa v aktivite dostaneme cez metódu getContentResolver(). Keďže chceme získavať dáta z content providera, využijeme metódu query(), kde uvedieme parametre zodpovedajúce SQL dopytu.

Metóda query() má mnoho parametrov:

  • identifikátor URI pre content providera, kde využívame konštantu CONTENT_URI. Zodpovedá klauzule FROM [názov tabuľky].
  • projekciu, kde uvedieme názvy stĺpcov z content providera, ktoré chceme získať. Zodpovedajú klauzule SELECT ___*. Ak uvedieme null, získajú sa všetky stĺpce.
  • selekciu zodpovedajúcu podmienke vo WHERE,
  • parametre selekcie zodpovedajúce parametrizovaným hodnotám pre WHERE podmienku,
  • poradie triedenia, ktoré korešponduje s klauzulou ORDER BY.

Výsledkom metódy query() je kurzor.

Ak chceme pristúpiť ku konkrétnemu riadku z histórie volaní, využime:

Cursor cursor = getContentResolver().query(
                    CallLog.Calls.CONTENT_URI,
                    null, /* všetky stĺpce */
                    CallLog.Calls._ID = " + id,
                    null /* žiadne parametre selekcie */,
                    null) /* žiadne triedenie */;

Podmienka sa odkazuje na primárny kľúč definovaný konštantou _ID.

Ručné práce s kurzormi

Kurzor získaný z metódy query() by mal vo výsledku obsahovať jediný riadok, prislúchajúci telefónnemu číslu vybranému v mriežke. Vyššie sme vraveli, že kurzor reprezentuje nielen výsledok, ale aj odkaz smerujúci na konkrétny riadok, ktorý môžeme posúvať smerom ku koncu tabuľkových dát.

Metóda moveToNext() dokáže posúvať kurzor na ďalší riadok, a vráti true, ak taký ďalší riadok existuje. Ak sa už žiadny záznam vo výsledku nenachádza, vráti prirodzene false.

Na začiatku je kurzor nasmerovaný pred ktorýkoľvek riadok, a prvé volanie tejto metódy nám dokáže zistiť, či vôbec nejaké dáta existujú vo výsledku.

Dáta z riadku, na ktorý ukazuje kurzor, získame pomocou sady metód getXXX() metódy, kde XXX je dátový typ. Príkladom je getString(), ktorá vráti konkrétnu hodnotu zo zadaného stĺpca ako reťazec. Pozor však! Parameter týchto metód nie je názov stĺpca, ale jeho index!

Aby sme nemuseli ručne zisťovať mapovanie medzi stĺpcami a indexami, využijeme pomocnú metódu cursor.getColumnIndex(*meno*), ktorá nám to povie.

Posledná dôležitá zásada súvisí s uvoľnovaním prostriedkov: po skončení práce s kurzorom ho nezabudneme zatvoriť! Inak nám vyfiltrované dáta budú zbytočne zaberať miesto v pamäti.

Kód

Ukážme si situáciu, keď vybranú položku zobrazíme v toaste:

@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    Cursor cursor = getContentResolver().query(CallLog.Calls.CONTENT_URI, null, CallLog.Calls._ID + "=" + id, null, null);
    if(cursor.moveToNext()) {
        String phoneNumber = cursor.getString(cursor.getColumnIndex(CallLog.Calls.NUMBER));
        Toast.makeText(this, phoneNumber, Toast.LENGTH_SHORT).show();
    }
    cursor.close();     
}

Vytáčanie hovorov

Ak chceme po kliknutí na telefónne číslo začať rovno vytáčať hovor, využijeme na to intent, ktorým oznámime úmysel telefonovať. V tomto prípade však nemôžeme uviesť názov triedy aktivity, ktorá uskutoční hovor, keďže ho nepoznáme. Namiesto toho môžeme uviesť v konštruktore intentu názov akcie, v tomto prípade ACTION_CALL, ktorá hovorí o telefonovaní, a Android sa sám postará o vytočenie hovoru.

Samozrejme, musíme tiež vedieť, aké číslo mienime vytočiť. Realizujeme to cez dáta intentu, a v súlade s dokumentáciou číslo zareprezentujeme ako URI v tvare tel:______.

Intent callIntent = new Intent(Intent.ACTION_CALL);
callIntent.setData(Uri.parse(WebView.SCHEME_TEL + number));
startActivity(callIntent);

Aktualizácia dát

Všimnime si aktualizáciu dát, ak nasimulujeme telefonát, ktorý zavesíme, po návrate našej aktivity na popredie sa mriežka okamžite aktualizuje. Content provider totiž umožňuje registráciu poslucháčov na zmeny a SimpleCursorAdapter dokáže byť takým poslucháčom a pri zmenách aktualizovať mriežku.

Míľnik 3: vlastný layout položiek

Doposiaľ sme používali pre jednotlivé "bunky" mriežky zabudovaný systémový layout android.R.layout.simple_list_item_1, kde text bol namapovaný na TextView s identifikátorom android.R.id_text1.

Ak chceme používať farebné a väčšie bunky, definujme si vlastný layout, v adresári layout a nazvime ho grid_item.xml.

Kliknime pravým myšidlom na adresár layout v projekte, z podmenu New zvoľme Layout Resource File a nazvime ho grid_item.xml.

Súbor res/layout/grid_item.xml

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

    <TextView
        android:id="@+id/grid_item_text"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:height="90dp"
        android:background="@android:color/black"
        android:textColor="@android:color/white"
        android:gravity="center"
        />
</LinearLayout>

Zvolíme lineárny layout (aj keď to nie je až také podstatné, lebo využijeme len jeden komponent) a vložme doň jeden TextView, ktorému zároveň pridelíme identifikátor, aby sme sa naň vedeli odkázať v SimpleCursorAdapteri.

Výšku a šírku v layoute nastavíme podľa rodiča, ale výšku nastavíme napevno na 90 dp, čím zachováme štvorcovitosť bunky (šírka 90 dp je nastavená v layoute aktivity, v šírke stĺpca columnWidth). Aby sme dali textu v bunke trochu vzdušnosti, nastavíme vhodný padding (vzdialenosť medzi obsahom a okrajom komponentu) a celý text v bunke vycentrujeme cez gravity (pozor, nemýliť si s layout_gravity!).

Okrem toho ešte nastavíme biely text a čierne pozadie, a to cez odkazy na zabudované farby Androidu.

Inicializácia adaptéra

Upravme potom inicializáciu kurzorového adaptéra. V konštruktore už budeme využívať layout definovaný v našom novom súbore a pole to bude obsahovať identifikátor textového políčka z nášho layoutu.

int[] to = { R.id.grid_item_text } ;
adapter = new SimpleCursorAdapter(this, R.layout.grid_item, NO_CURSOR, from, to);

Ak teraz spustíme aplikáciu, je možné, že uvidíme podivnú mriežku s čiernymi bunkami na bielom pozadí. Súvisí to so systémovou farebnou schémou, ktorá nastaví pozadie aktivity na biele. Tento estetický nedostatok odstránime na konci tutoriálu.

Farby ako resource

Poďme teraz definovať rozličné farby pre bunky: prichádzajúce hovory nech sú zelené, odchádzajúce červené a zmeškané modré.

Definícia farieb sa môže diať buď v kóde, alebo v resources. Vytvorme si nový resources súbor (File | New | XML | Values XML File) a nazvime ho color.xml.

V adresári values vznikne nový súbor color.xml.

Uvedieme doňho 6 farieb, kde v dvojici nastavíme pozadie i text:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="incomingCallBackground">#99CC00</color>
    <color name="incomingCallForeground">#FFFFFF</color>

    <color name="outgoingCallBackground">#33B5E5</color>
    <color name="outgoingCallForeground">#FFFFFF</color>

    <color name="missedCallBackground">#FF4444</color>
    <color name="missedCallForeground">#FFFFFF</color>

</resources>

Mapovanie medzi dátami a výzorom widgetov: SimpleCursorAdapter.ViewBinder

Ako prepojíme farby s mriežkou GridView? Cez view binder.

Prispôsobenie vykresľovania dát, či úprava dát z adaptéra pred ich zobrazením v komponentoch je v prípade SimpleCursorAdaptéra možná cez tzv. view binder

Interfejs SimpleCursorAdapter.ViewBinder má jednu metódu:

public boolean setViewValue(View view, Cursor cursor, int columnIndex)

Tá sa volá toľkokrát, koľko je záznamov v kurzore, čo zodpovedá počtu buniek v mriežke. Pri každom volaní dostaneme v parametri view, teda widget, na ktorý sa má namapovať príslušná hodnota zo stĺpca tabuľky. Druhý parameter je kurzor bude nastavený na príslušný záznam a columnIndex obsahuje index stĺpca v tabuľke, z ktorého máme namapovať dáta na widget.

Metóda má vrátiť true, ak úspešne vykonala mapovanie medzi dátami z tabuľkami. Ak vráti false, indikuje tým kurzorovému adaptéru, že má použiť vlastné štandardné správanie, ktoré sme videli v predošlom dieli.

Celý kód pre náš príklad vyzerá:

public class CallLogViewBinder implements SimpleCursorAdapter.ViewBinder {

    @Override
    public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
        if(view instanceof TextView) {
            TextView textView = (TextView) view;
            int type = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.TYPE));
            switch(type) {
                case CallLog.Calls.INCOMING_TYPE:
                    textView.setBackgroundColor(
                            textView.getResources()
                                    .getColor(R.color.incomingCallBackground)
                            );
                    break;
                case CallLog.Calls.OUTGOING_TYPE:
                    textView.setBackgroundColor(
                            textView.getResources()
                                .getColor(R.color.outgoingCallBackground)

                    );
                    break;
                case CallLog.Calls.MISSED_TYPE:
                    textView.setBackgroundColor(
                            textView.getResources()
                                 .getColor(R.color.missedCallBackground)

                    );
                    break;
            }
        }
        return false;
    }
}

Predovšetkým, musíme zistiť, či je komponent vo view správneho typu. Keďže kurzorový adaptér pracuje nad layoutom z grid_item.xml, kde máme jediný komponent TextView, sme povinní sa spýtať, či ideme naozaj prispôsobovať správny komponent.

Následne zistíme typ hovoru, zo stĺpca CallLog.Calls.TYPE.

int type = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.TYPE));

Veľký switch odlíši konkrétny typ hovoru, a ako nastavíme farbu? Najprv musíme získať odkaz na definičný súbor s farbami. Z komponentu view získame tento objekt:

Resources resources = view.getResources();

Následne získame z tohto objektu farbu:

int incomingCallBackgroundColor = resources.getColor(R.color.incomingCallBackground)

A túto farbu nastavíme na komponente (napríklad pre farbu prichádzajúceho hovoru):

textView.setBackgroundColor(resources.getColor(R.color.incomingCallBackground));

Pozor! V tomto prípade sa používa int v dvoch významoch: jednak ako identifikátor konkrétneho resourcu ale jednak ako reprezentácia namiešanej farby. Prevody z resources nás zbavia nečakaných farebných prekvapení.

Metóda je povinná vrátiť true, ak sme prispôsobovali či upravovali dáta mapované na komponent (môže sa to stať napríklad v prípade dátumu hovoru, ktorý musíme predspracovať). V tomto prípade sa žiadna úprava dát nediala a preto vrátime false, čo znamená, že sa spoľahneme na štandardné správanie, ktoré využíva toString() na hodnotách z kurzora.

Asociovanie viewbindera s komponentom

View binder prepojíme s adaptérom v metóde onCreate() v aktivite:

adapter.setViewBinder(new CallLogViewBinder());

Finálne dolaďovačky

Schovanie action baru a tmavé pozadie

V manifeste nastavíme tému aplikácie v elemente <application> a v atribúte theme. Ak nastavíme tému Theme.AppCompat.NoActionBar, využijeme tmavé pozadie a zároveň schováme lištu akcií, ktoré je v tejto aplikácii zbytočná.

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/Theme.AppCompat.NoActionBar" >

Hotová aplikácia

Zdrojové kódy výslednej aplikácie sú dostupné na Githube v repe novotnyr/android-callgrid-2015.