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

Lukáš Marek 17. 3. 2011

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:

widgety

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?
Vitalia.cz: Muž, který miluje příliš. Ženám neimponuje

Muž, který miluje příliš. Ženám neimponuje

Vitalia.cz: Opuncie je plod kaktusu. Pozor na trny

Opuncie je plod kaktusu. Pozor na trny

Lupa.cz: Jak se prodává firma za miliardu?

Jak se prodává firma za miliardu?

Vitalia.cz: Tahák, jak vyzrát nad zápachem z úst

Tahák, jak vyzrát nad zápachem z úst

DigiZone.cz: Parlamentní listy: kde končí PR...

Parlamentní listy: kde končí PR...

Vitalia.cz: Kterou dýni můžete jíst za syrova?

Kterou dýni můžete jíst za syrova?

Podnikatel.cz: EET pro e-shopy? Postavené na hlavu

EET pro e-shopy? Postavené na hlavu

Podnikatel.cz: Chystá se smršť legislativních novinek

Chystá se smršť legislativních novinek

DigiZone.cz: DVB-T2 ověřeno: seznam TV zveřejněn

DVB-T2 ověřeno: seznam TV zveřejněn

120na80.cz: Hrbatá prsa aneb mýty o implantátech

Hrbatá prsa aneb mýty o implantátech

Vitalia.cz: 5 pravidel proti infekci močových cest

5 pravidel proti infekci močových cest

DigiZone.cz: Koncesionářské poplatky pro RTVS

Koncesionářské poplatky pro RTVS

DigiZone.cz: Funbox 4K v DVB-T2 má ostrý provoz

Funbox 4K v DVB-T2 má ostrý provoz

DigiZone.cz: Mordparta: trochu podchlazený 87. revír

Mordparta: trochu podchlazený 87. revír

Podnikatel.cz: Instalatér, malíř a elektrikář. "Vymřou"?

Instalatér, malíř a elektrikář. "Vymřou"?

Podnikatel.cz: Babišovi se nedá věřit, stěžovali si hospodští

Babišovi se nedá věřit, stěžovali si hospodští

DigiZone.cz: Ginx TV: pořad o počítačových hráčích

Ginx TV: pořad o počítačových hráčích

DigiZone.cz: Numan Two: rozhlasový přijímač s CD

Numan Two: rozhlasový přijímač s CD

Lupa.cz: Aukro.cz mění majitele. Vrací se do českých rukou

Aukro.cz mění majitele. Vrací se do českých rukou

Vitalia.cz: Když všichni seli řepku, on vsadil na dýně

Když všichni seli řepku, on vsadil na dýně