Hlavní navigace

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

Lukáš Marek

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.

Doba čtení: 5 minut

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:

NMI18_Materna

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.

Našli jste v článku chybu?