Hlavní navigace

Android v příkladech: práce s kontakty

17. 3. 2011
Doba čtení: 5 minut

Sdílet

Každý programátor aplikací pro Android narazí dříve nebo později na nutnost pracovat s kontakty uloženými v telefonu. Není to ale příliš složité? K pochopení Android 2.0 Contacts API sice není třeba mít mozek Sheldona Coopera, ale malá pomoc do začátku se určitě bude hodit. Ukážeme si, jak na to.

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:

ict ve školství 24

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.

Autor článku

Lukáš Marek pracuje jako Software Architect a věnuje se vývoji pro Android.