Contactos en Android. Añadir información personalizada

Google nos ofrece una gestión de contactos que podemos tener sincronizada con nuestro smartphone. Esta información está estructurada y nos permite añadir direcciones, números de teléfono, direcciones de correo electrónico, fecha de aniversario, sitio web y un número limitado de información personal.

Pero para algunas aplicaciones, necesitamos poder añadir información extra que no aparece entre todos los campos.

En mi caso me surgió la necesidad de incluir un campo prioridad a mis contactos de tal forma que me encontré con la necesidad de incluir campos personalizados en mis contactos.

Basándome en el ejemplo que ofrece el blog de desarrollo de Android en su web para la gestión de contactos, he querido hacer una pequeña ampliación para poder añadir información personalizada. Pero antes de ponernos en materia, deberemos profundizar en algunos conceptos.

Para acceder a cualquier información estructurada de un dispositivo Android, se deberá utilizar un Content Provider,  que no es más que un mecanismo que sirve para intercambiar datos estructurados entre aplicaciones de forma aislada. El SDK de Android ofrece varios Contents Providers, como el Calendar Provider que es el repositorio de la información de los eventos de calendario del usuario, o el Contact Provider que sirve para controlar el repositorio de datos de personas de un dispositivo. En el caso que nos ocupa trataremos la información del Contact Provider.

La información del Contact Provider se divide principalmente en tres tablas ContactsRaw Contact y Data.

La tabla contacts identifica a una persona. Un contacto.

La tabla Raw contact identifica las distintas formas en que se guarda un contacto, normalmente serán distintas aplicaciones donde tenemos la información del contacto, como podría ser el Outlook, otro teléfono, etc.

La tabla Data, guarda cada uno de los datos que hay en cada Raw contact. Estos datos pueden ser números de teléfono, dirección postal, fotos, etc.

Para acceder a la información del Content Provider se deberá implementar un Content Resolver, que permitirá consultar, insertar, modificar y eliminar los datos.

La aplicación de ejemplo, es bastante sencilla, al acceder a la misma se hace una consulta de todos los contactos, mostrando la imagen asociada al contacto si la tuviera junto al nombre del contacto. Al pulsar sobre el contacto se accede a la información detallada, en la que se mostrará la foto del contacto en grande junto a las direcciones postales y sus teléfonos.

Por cada dirección postal, se podrá pulsar sobre un botón de geolocalización que al pulsarlo abrirá google maps e intentará ubicar la dirección.

En la parte superior hay dos iconos, uno a la izquierda para volver al listado de contactos y otro a la derecha que permite acceder a la edición de contacto del dispositivo.

Lo que haremos será incluir una barra justo debajo del teléfono que servirá para indicar la prioridad del contacto junto a un icono de guardar que permitirá almacenar esa información.

El código lo podéis descargar de mi repositorio de github https://github.com/dcrespim/contacts.

 

En el AndroidManifest.xml se deberá incluir el permiso para escribir en los contactos, android.permission.WRITE_CONTACTS

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="es.example.contacts"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="14"
        android:targetSdkVersion="19" />

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

    <application
        android:description="@string/app_description"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme"
        android:allowBackup="true">

        <!-- When the soft keyboard is showing the views of this activity should be resized in the
             remaining space so that inline searching can take place without having to dismiss the
             keyboard to see all the content. Therefore windowSoftInputMode is set to
             adjustResize. -->
        <activity
                android:name=".ui.ContactsListActivity"
                android:label="@string/activity_contacts_list"
                android:windowSoftInputMode="adjustResize">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <!-- Add intent-filter for search intent action and specify searchable configuration
                 via meta-data tag. This allows this activity to receive search intents via the
                 system hooks. In this sample this is only used on older OS versions (pre-Honeycomb)
                 via the activity search dialog. See the Search API guide for more information:
                 http://developer.android.com/guide/topics/search/search-dialog.html -->
            <intent-filter>
                <action android:name="android.intent.action.SEARCH" />
            </intent-filter>
            <meta-data android:name="android.app.searchable"
                       android:resource="@xml/searchable_contacts" />
        </activity>
        <activity
            android:name=".ui.ContactDetailActivity"
            android:label="@string/activity_contact_detail"
            android:parentActivityName=".ui.ContactsListActivity">
            <!-- Define hierarchical parent of this activity, both via the system
                 parentActivityName attribute (added in API Level 16) and via meta-data annotation.
                 This allows use of the support library NavUtils class in a way that works over
                 all Android versions. See the "Tasks and Back Stack" guide for more information:
                 http://developer.android.com/guide/components/tasks-and-back-stack.html
            -->
            <meta-data android:name="android.support.PARENT_ACTIVITY"
                       android:value=".ui.ContactsListActivity" />
        </activity>
    </application>
</manifest>

Modificaremos el fichero de layout contact_detail_item.xml para incluir el selector de prioridad y el botón de guardar en una línea horizontal.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:paddingTop="@dimen/padding"
              android:paddingLeft="@dimen/padding"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

    <TextView
            android:id="@+id/contact_detail_header"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            style="@style/addressHeader"/>

    <LinearLayout android:orientation="horizontal"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content"
                  android:showDividers="middle"
                  android:dividerPadding="12dp"
                  android:minHeight="48dp"
                  android:divider="?android:attr/listDivider">

        <TextView
                android:id="@+id/contact_detail_item"
                android:layout_height="wrap_content"
                android:layout_width="0dp"
                android:layout_weight="1"
                android:paddingRight="@dimen/padding"
                android:layout_gravity="center"
                style="@style/addressDetail"/>

        <ImageButton
            android:id="@+id/button_view_address"
            android:src="@drawable/ic_action_map"
            android:layout_height="match_parent"
            android:layout_width="wrap_content"
            android:paddingTop="8dp"
            android:paddingBottom="8dp"
            android:layout_gravity="center"
            android:contentDescription="@string/address_button_description"
            style="@style/addressButton"/>

    </LinearLayout>

    <TextView
        android:id="@+id/contact_phone"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        style="@style/addressHeader"/>

    <LinearLayout android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:showDividers="middle"
        android:dividerPadding="12dp"
        android:minHeight="48dp"
        android:divider="?android:attr/listDivider">

        <SeekBar
            android:id="@+id/contact_level"
            android:layout_height="wrap_content"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:paddingRight="@dimen/padding"
            android:layout_gravity="center"
            android:max="5"/>

        <ImageButton
            android:id="@+id/button_save"
            android:src="@android:drawable/ic_menu_save"
            android:layout_height="match_parent"
            android:layout_width="wrap_content"
            android:paddingTop="8dp"
            android:paddingBottom="8dp"
            android:layout_gravity="center"
            android:contentDescription="@string/button_save"
            style="@style/addressButton"/>
    </LinearLayout>
</LinearLayout>

En el fichero strings.xml se deberá añadir el name “button_save”.

 <string name="button_save">Save</string> 

Para finalizar hay que modificar la clase ContactDetailFragment añadiendo las siguientes partes.

Se crea una nueva propiedad LEVEL_MIME_TYPE con el valor ‘vnd.android.cursor.item/level‘. Esta propiedad nos servirá para identificar el registro de nivel de prioridad en la tabla DATA.

Se añade el interfaz ContactData1Query que servirá para realizar la consulta de la información del nivel de prioridad.

Se modifica el método onLoadFinished para incluir la consulta del nivel de prioridad y se modificará la llamada al método buildAddressLayout para incluir los nuevos parámetros.

Se modificará el método buildAddressLayout para gestionar los nuevos campos e incluir el código para el nuevo botón saveButton.

...
public class ContactDetailFragment extends Fragment implements
        LoaderManager.LoaderCallbacks<Cursor> {

    public static final String LEVEL_MIME_TYPE = "vnd.android.cursor.item/level";

    public static final String EXTRA_CONTACT_URI =
            "es.example.contacts.ui.EXTRA_CONTACT_URI";
...

    String displayName;
    String id;
    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {

        // If this fragment was cleared while the query was running
        // eg. from from a call like setContact(uri) then don't do
        // anything.
        if (mContactUri == null) {
            return;
        }

        switch (loader.getId()) {
            case ContactDetailQuery.QUERY_ID:
                // Moves to the first row in the Cursor
                if (data.moveToFirst()) {
                    // For the contact details query, fetches the contact display name.
                    // ContactDetailQuery.DISPLAY_NAME maps to the appropriate display
                    // name field based on OS version.
                    final String contactName = data.getString(ContactDetailQuery.DISPLAY_NAME);
                    displayName=contactName;
                    id=data.getString(ContactDetailQuery.ID);
                    if (mIsTwoPaneLayout && mContactName != null) {
                        // In the two pane layout, there is a dedicated TextView
                        // that holds the contact name.
                        mContactName.setText(contactName);
                    } else {
                        // In the single pane layout, sets the activity title
                        // to the contact name. On HC+ this will be set as
                        // the ActionBar title text.
                        getActivity().setTitle(contactName);
                    }
                }
                break;
            case ContactAddressQuery.QUERY_ID:
                // This query loads the contact address details. More than
                // one contact address is possible, so move each one to a
                // LinearLayout in a Scrollview so multiple addresses can
                // be scrolled by the user.

                // Each LinearLayout has the same LayoutParams so this can
                // be created once and used for each address.
                final LinearLayout.LayoutParams layoutParams =
                        new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                                ViewGroup.LayoutParams.WRAP_CONTENT);

                // Clears out the details layout first in case the details
                // layout has addresses from a previous data load still
                // added as children.
                mDetailsLayout.removeAllViews();

                // Loops through all the rows in the Cursor
                if (data.moveToFirst()) {
                    do {
                        final Uri uri = Uri.withAppendedPath(mContactUri, ContactsContract.RawContacts.Data.CONTENT_DIRECTORY);
                        ContentResolver cr = getActivity().getContentResolver();

                        Cursor cur = cr.query(uri,
                                ContactPhoneQuery.PROJECTION,
                                ContactPhoneQuery.SELECTION,
                                null,
                                null);
                        try {
                            if (cur.moveToFirst()) {
                                // Builds the address layout
                                final Uri uriData = Uri.parse("content://com.android.contacts/data");
                                Cursor curData = cr.query(Data.CONTENT_URI,
                                        ContactData1Query.PROJECTION,
                                        ContactData1Query.SELECTION,
                                        null,
                                        null);
                                try {
                                    if (curData.getCount() == 0) {
                                        final LinearLayout layout = buildAddressLayout(
                                                data.getInt(ContactAddressQuery.TYPE),
                                                data.getString(ContactAddressQuery.LABEL),
                                                data.getString(ContactAddressQuery.ADDRESS),
                                                cur.getString(ContactPhoneQuery.PHONE),
                                                null,
                                                data.getString(ContactAddressQuery.CONTACT_ID),
                                                displayName);
                                        // Adds the new address layout to the details layout
                                        mDetailsLayout.addView(layout, layoutParams);

                                    } else {
                                        if (curData.moveToFirst()) {

                                            final LinearLayout layout = buildAddressLayout(
                                                    curData.getInt(ContactAddressQuery.ID),
                                                    data.getString(ContactAddressQuery.LABEL),
                                                    data.getString(ContactAddressQuery.ADDRESS),
                                                    cur.getString(ContactPhoneQuery.PHONE),
                                                    curData.getString(ContactData1Query.DATA1),
                                                    data.getString(ContactAddressQuery.CONTACT_ID),
                                                    displayName);
                                            // Adds the new address layout to the details layout
                                            mDetailsLayout.addView(layout, layoutParams);
                                        }
                                    }
                                } finally {
                                    if (curData != null) curData.close();
                                }
                            }
                        } finally {
                            if (cur != null) cur.close();
                        }
                    } while (data.moveToNext());
                    data.close();
                } else {
                    // If nothing found, adds an empty address layout
                    mDetailsLayout.addView(buildEmptyAddressLayout(), layoutParams);
                }

                break;
        }
    }
...
    /**
     * Builds an empty address layout that just shows that no addresses
     * were found for this contact.
     *
     * @return A LinearLayout to add to the contact details layout
     */
    private LinearLayout buildEmptyAddressLayout() {
        return buildAddressLayout(0, null, null, null, null, null, null);
    }

    /**
     * Builds an address LinearLayout based on address information from the Contacts Provider.
     * Each address for the contact gets its own LinearLayout object; for example, if the contact
     * has three postal addresses, then 3 LinearLayouts are generated.
     *
     * @param addressType From
     * {@link android.provider.ContactsContract.CommonDataKinds.StructuredPostal#TYPE}
     * @param addressTypeLabel From
     * {@link android.provider.ContactsContract.CommonDataKinds.StructuredPostal#LABEL}
     * @param address From
     * {@link android.provider.ContactsContract.CommonDataKinds.StructuredPostal#FORMATTED_ADDRESS}
     * @param phone From
     * {@link android.provider.ContactsContract.CommonDataKinds.Phone#NUMBER}
     * @param data1 From
     * {@link android.provider.ContactsContract.Data#DATA1}
     * @param _id From
     * {@link android.provider.ContactsContract#}
     * @return A LinearLayout to add to the contact details layout,
     *         populated with the provided address details.
     */
    private LinearLayout buildAddressLayout(int addressType, String addressTypeLabel,
            final String address, final String phone, final String data1, final String _id,
            final String _name) {

        // Inflates the address layout
        final LinearLayout addressLayout =
                (LinearLayout) LayoutInflater.from(getActivity()).inflate(
                        R.layout.contact_detail_item, mDetailsLayout, false);

        // Gets handles to the view objects in the layout
        final TextView headerTextView =
                (TextView) addressLayout.findViewById(R.id.contact_detail_header);
        final TextView addressTextView =
                (TextView) addressLayout.findViewById(R.id.contact_detail_item);
        final ImageButton viewAddressButton =
                (ImageButton) addressLayout.findViewById(R.id.button_view_address);
        final TextView phoneTextView =
                (TextView) addressLayout.findViewById(R.id.contact_phone);
        final SeekBar levelSeekBar =
                (SeekBar) addressLayout.findViewById(R.id.contact_level);
        final ImageButton saveButton =
                (ImageButton) addressLayout.findViewById(R.id.button_save);

        Integer prioridad =  Integer.parseInt(data1 == null ? "4" : data1);
        // If there's no addresses for the contact, shows the empty view and message, and hides the
        // header and button.
        if (addressTypeLabel == null && addressType == 0 && phone == null) {
            headerTextView.setVisibility(View.GONE);
            viewAddressButton.setVisibility(View.GONE);
            addressTextView.setText(R.string.no_address);
        } else {
            // Gets postal address label type
            CharSequence label =
                    StructuredPostal.getTypeLabel(getResources(), addressType, addressTypeLabel);

            // Sets TextView objects in the layout
            headerTextView.setText(label);
            addressTextView.setText(address);
            phoneTextView.setText(phone);
            levelSeekBar.setProgress(prioridad);

            // Defines an onClickListener object for the address button
            viewAddressButton.setOnClickListener(new View.OnClickListener() {
                // Defines what to do when users click the address button
                @Override
                public void onClick(View view) {

                    final Intent viewIntent =
                            new Intent(Intent.ACTION_VIEW, constructGeoUri(address));

                    // A PackageManager instance is needed to verify that there's a default app
                    // that handles ACTION_VIEW and a geo Uri.
                    final PackageManager packageManager = getActivity().getPackageManager();

                    // Checks for an activity that can handle this intent. Preferred in this
                    // case over Intent.createChooser() as it will still let the user choose
                    // a default (or use a previously set default) for geo Uris.
                    if (packageManager.resolveActivity(
                            viewIntent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
                        startActivity(viewIntent);
                    } else {
                        // If no default is found, displays a message that no activity can handle
                        // the view button.
                        Toast.makeText(getActivity(),
                                R.string.no_intent_found, Toast.LENGTH_SHORT).show();
                    }
                }
            });

            // Defines an onClickListener object for the save button
            saveButton.setOnClickListener(new View.OnClickListener() {
                // Defines what to do when users click the address button
                @Override
                public void onClick(View view) {
                    // Creates a new intent for sending to the device's contacts application
                    // Creates a new array of ContentProviderOperation objects.
                    ArrayList<ContentProviderOperation> ops =
                            new ArrayList<ContentProviderOperation>();

                    Integer prioridad = levelSeekBar.getProgress();
                    Log.d("ContactDetalFragment", _id);

                    ContentProviderOperation.Builder op;
                    if (data1 == null) {
                        Uri uri = addCallerIsSyncAdapterParameter(Data.CONTENT_URI, true);
                        ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
                                .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
                                .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null)
                                .build());

                        ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
                                .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
                                .withValue(ContactsContract.Data.MIMETYPE,
                                        ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
                                .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, _name)
                                .build());

                        op = ContentProviderOperation.newInsert(Data.CONTENT_URI)
                                .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
                                .withValue(Data.DATA1, prioridad)
                                .withValue(Data.MIMETYPE, LEVEL_MIME_TYPE);

                    } else {
                        String where = Data.MIMETYPE + " = ? ";
                        String[] params = new String[]{LEVEL_MIME_TYPE,};

                        op = ContentProviderOperation.newUpdate(Data.CONTENT_URI)
                                .withSelection(where, params)
                                .withValue(Data.DATA1, prioridad);

                    }
                    ops.add(op.build());
                    try {
                        ContentProviderResult[] results = view.getContext().getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
                    } catch (Exception e) {
                         CharSequence txt = getString(R.string.contactUpdateFailure);
                         int duration = Toast.LENGTH_SHORT;
                         Toast toast = Toast.makeText(view.getContext(), txt, duration);
                         toast.show();
                         // Log exception
                         Log.e(TAG, "Exception encountered while inserting contact: " + e);
                    }
                }
            });

        }
        return addressLayout;
    }
...
    /**
     * This interface defines constants used by address retrieval queries.
     */
    public interface ContactData1Query {
        // A unique query ID to distinguish queries being run by the
        // LoaderManager.
        final static int QUERY_ID = 4;

        // The query projection (columns to fetch from the provider)
        final static String[] PROJECTION = {
                Data._ID,
                Data.DATA1
        };

        // The query selection criteria. In this case matching against the
        // content mime type.
        final static String SELECTION =
                Data.MIMETYPE + "='" + LEVEL_MIME_TYPE + "' ";


        // The query column numbers which map to each value in the projection
        final static int ID = 0;
        final static int DATA1 = 1;
    }
}

Doy gracias a los bloggers siguientes, en cuyos posts he podido profundizar en el tema.

Android development
La materia
Hermosa programación.

img_ccTodo el material publicado en este Blog, salvo las obras que no pertenecen a su autor, se difunden bajo licencia CC by-SA de Creative Commons, por lo que eres libre de copiar, distribuir y comunicar este contenido de forma publica, hacer un uso comercial del mismo, etc., siempre que lo hagas bajo las condiciones de la licencia indicada, y que reconozcas a su autor e indiques un enlace al contenido original o en su defecto a la pagina principal de este blog.

Anuncios
  1. No trackbacks yet.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: