V minulém článku se nám podařilo nechat uživatele vybrat si kontakt z adresáře a tím získat jeho Uri
. Dnes budeme s tímto kontaktem dále pracovat.
Základní informace o kontaktu
Android sice používá interně klasickou SQLite databázi, ve které jsou kontakty uloženy v několika tabulkách, ale do databáze není bohužel možný přímý přístup. Místo toho je k dispozici API, které je zřejmě navrženo se záměrem odradit všetečné programátory od svého používání. Práce s ním vyžaduje pevné nervy a velkou trpělivost.
Základem tohoto API je ContentResolver
, který slouží k vytváření a volání databázových dotazů. Typický dotaz do interní databáze Androidu bude vypadat asi takto:
public static String lookupContact(Context ctx, Uri contactUri) { String[] projection = new String[]{ ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME, }; ContactDTO dto = null; Cursor c = ctx.getContentResolver().query(contactUri, projection, null, null, null); if (c != null && c.moveToFirst()) { Long id = c.getLong(0); String name = c.getString(1); } if (c != null) { c.close(); } return name; }
Nejdůležitějším kusem kódu je metoda query()
, tedy spíše její parametry. Takže popořadě: první je Uri
, v tomto případě ukazující na konkrétní kontakt, což nám protentokrát ušetřilo práci s tvořením WHERE části dotazu.
Další v řadě je projection
, což je vlastně seznam sloupečků tabulky, které má dotaz vrátit. V tomto případě tedy id záznamu a jméno kontaktu. Pokud je zadáno null
, pak dotaz vrátí všechny sloupečky dané tabulky. Zjištění, jaké sloupečky vlastně daný dotaz může vrátit, bývá velmi zábavnou částí vývoje Android aplikací a budu se mu věnovat níže.
Následují parametry selection
, což je vlastně WHERE část dotazu, selectionArgs
– její parametry a nakonec sortOrder
– jak se mají výsledky třídit. Všechny tři jsou prázdné.
Praktický tip
Pro vážnější práci je potřeba si udělat obrázek o skutečné struktuře kontaktů v databázi Androidu. To není až tak složité – stačí si zkopírovat (z emulátoru nebo rootnutého telefonu) kompletní databázi kontaktů pomocí nástroje adb
a prohlédnout si ji v oblíbeném databázovém IDE, případně si připravit SQL dotazy předem:
adb pull /data/data/com.android.providers.contacts/databases/contacts2.db .
Po odladění SQL dotazů „nasucho“ potom nastává zábavná fáze, kdy se nebohý vývojář snaží naroubovat dotaz do ContentResolver
u a najít, které konstanty by mohly odpovídat jménu sloupečků v databázi.
Další informace o kontaktu
Pro rozšíření základní sady kontaktů už je potřeba sáhnout do dalších tabulek. Jak vlastně Android s kontakty pracuje?
Každému kontaktu odpovídá jeden záznam v tabulce contacts
a jeden nebo více záznamů v tabulce raw_contacts
. Raw kontact je vždy navázaný na uživatelův účet (Google, Exchange, …) a obsahuje konkrétní informace o kontaktu. Záznam v tabulce contacts
potom slouží sdružuje všechny záznamy z raw_contacts
, které mají stejné jméno nebo telefonní číslo, popřípadě jinak přirozeně patří k sobě. To znamená, že pokud má telefon dva zdroje kontaktů (například osobní GMail a firemní Exchange), v telefonním seznamu bude konkrétní člověk uveden pouze jednou, přestože je přítomen v obou zdrojích.
Na Raw Contact jsou potom navázány rozšiřující údaje (poznámky, e-maily, …) uložené v tabulce data
. Takže pro telefonní číslo už je třeba sáhnout do této tabulky:
private static String getPhoneNumber(Context ctx, long contactId) { Cursor cursor = ctx.getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, new String[]{ ContactsContract.CommonDataKinds.Phone.NUMBER}, ContactsContract.CommonDataKinds.Phone.CONTACT_ID + "= ?" + " AND " + ContactsContract.CommonDataKinds.Phone.TYPE + "=" + ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE, new String[]{String.valueOf(contactId)}, null); try { if (cursor.moveToFirst()) { return cursor.getString(0); } else { return null; } } finally { cursor.close(); } }
Zde už je vidět, že Uri
ukazuje obecně na všechna telefonní čísla, takže je potřeba omezit výběr pomocí selection
. Jde opravdu o klasickou WHERE klauzuli, bohužel díky použití konstant poněkud nepřehlednou.
Poznámka: Tento kód rozhodně není vhodný pro reálné využití, neboť vybírá pouze první telefonní číslo ze seznamu. V reálné aplikaci by musel vracet všechna telefonní čísla a nechat uživatele vybrat.
Ukládání změn
Dejme tomu, že se nám podařilo načíst z databáze všechny podstatné údaje a zobrazit je uživateli. Například takto:
K tomu, dát uživateli možnost uložit změnu poznámky stačí na událost onClick()
příslušného tlačítka zavolat takovouto metodu:
public static void saveNote(Context ctx, long contactId, String note, NoteDTO prevNote) { ArrayList batch = new ArrayList(1); if (prevNote == null) { //INSERT Long rawContactId = getRawContactIds(ctx, contactId)[0]; //tady by si mel uzivatel spravne vybrat batch.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI). withValue(ContactsContract.CommonDataKinds.Note.RAW_CONTACT_ID, String.valueOf(rawContactId)). withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE). withValue(ContactsContract.CommonDataKinds.Note.NOTE, note). build() ); } else { //UPDATE batch.add(ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI). withSelection(ContactsContract.CommonDataKinds.Note._ID + "= ?", new String[]{String.valueOf(prevNote.id)}). withValue(ContactsContract.CommonDataKinds.Note.NOTE, note). build() ); } ContentProviderResult[] result = ctx.getContentResolver().applyBatch(ContactsContract.AUTHORITY, batch); }
V kódu je záměrně použit objekt ContentProviderOperation
, který sice má sloužit k dávkovému zpracování změn v databázi, ale práce s ním je (na poměry Androida) relativně jednoduchá. Jediná zrada je, že musíme rozlišit, zda se jedná o INSERT nebo UPDATE. Tedy zda jde o vytvoření nové poznámky nebo úpravu stávající. V aplikaci připravené pro tento článek je to vyřešeno předáním původního objektu s poznámkou. Pokud je prevNote
null
, pak se jedná o INSERT.
A ještě jedna drobněnka: Poznámka se nedá přidat ke kontaktu, ale k raw kontaktu. Takže místo contactId
musíme získat rawContactId
. Pravděpodobně nějak takhle:
private static Long[] getRawContactIds(Context ctx, Long contactId) { Cursor cursor = ctx.getContentResolver().query( ContactsContract.RawContacts.CONTENT_URI, new String[] {ContactsContract.RawContacts._ID, ContactsContract.RawContacts.CONTACT_ID + " = ?", new String[]{String.valueOf(contactId)}, null); List result = new ArrayList(); while (cursor.moveToNext()) { result.add(cursor.getLong(0)); } return result.toArray(new Long[result.size()]); }
Bonus: Získání fotky
Ve světle předešlých informací je docela překvapivé, že získání fotky kontaktu je operace na jeden řádek:
ContactsContract.Contacts.openContactPhotoInputStream(ctx.getContentResolver(), contactUri);
Ale takhle to s Androidem prostě je. Některé složité věci jdou překvapivě jednoduše, některé jednoduché věci překvapivě složitě.
Závěr
V článku určitě nebylo popsáno všechno, nicméně zvídavému čtenáři je k dispozici kompletní zdrojový kód aplikace.