lunes, 24 de septiembre de 2012

Manipulando El API de Contactos en Android

En mi País (Ecuador) han decidido que a partir del mes de octubre del 2012, todos los números celulares tendrán un "9" adicional después del 0 y pasarán a ser de 10 dígitos, por ejemplo, el 083456715 quedará 0(9)83456715. Esto con el objetivo de ampliar la capacidad a 100 millones de líneas para un país de 13 millones de habitantes, ¡ pero bueno eso es harina de otro costal. !

Ante este cambio es necesario que todos actualicemos nuestra lista de contactos. Los usuarios de Android podemos descargar una de las tantas aplicaciones que existen para esto (que es lo que yo recomiendo) o podemos entretenernos reinventando la rueda y escribiendo una pequeña aplicación que lo haga.

Yo aproveche esta oportunidad para escribir un poco sobre el API de contactos de android y crear un nuevo post porque tenía ya tiempo que no escribía nada.

Antes debo Aclarar que el código a explicar funciona a partir de Android 2.0 es decir la versión 5 del SDK.
El Api que nos permitirá acceder a la Agenda se encuentra en android.provider.ContactsContractY para gestionar los contactos debemos utilizar un content provider. Un Content provider es el mecanismo que proporciona Android para manejar el acceso de datos que no son propios de nuestra aplicación. Es decir, para poder compartir la información que una aplicación distinta a la nuestra almacena. Un content provider debe mostrar los métodos query, update, delete, insert, etc.

Debemos entender como Android almacena los contactos. Estos se almacenan en 3 Clases:
  • Contacts(android.provider.ContactsContract.Contacts)
  • RawContacts (android.provider.ContactsContract.RawContacts)
  • Data (android.provider.ContactsContract.Data)

Contacts se puede decir que es el primer nivel y contiene uno o mas contactos de la misma persona.

RawContacts representa un conjunto de datos de una persona, por ejemplo una persona tiene datos de facebook, gmail, etc. Cada uno de estos se almacena en un RawContacs.

Data almacena los datos asociados con el usuario como son el teléfono, correo, etc. Cada Data solo almacena un tipo de dato y se los diferencia por el MIME. por ejemplo si un DATA es de tipo Phone.CONTENT_ITEM_TYPE ese corresponde aun número telefónico. En la clase  ContactsContract.CommonDataKinds se encuentran definidos todos los tipos que se almacenan en un Data.

En Resúmen un mismo contacto (Contacts) puede tener un conjunto de RawContacs (cuenta de fb, twitter, gmail, etc ) y este tiene un conjunto de Data (número telefónico, correo, etc ).
En el presente ejemplo solo utilizaremos los DATA y filtraremos los que son de tipo Phone.CONTENT_ITEM_TYPE.

Si revisamos la documentación de cada una de estas clases en http://developer.android.com veremos cuales son los mecanismos de consulta, inserción, actualización, etc. que provee cada una de ellas.

Creo que esa es la teoría inicial ahora pasemos al codigo.

Implementé un método que lo llame modificarcontactosandroid(String iddelcontacto,String numerotelefono)
Este recibe el id del contacto que voy a modificar y el nuevo número de teléfono que voy actualizar.

private void modificarcontactosandroid(String iddelcontacto,String numerotelefono)throws RemoteException, OperationApplicationException{
  ArrayList<contentprovideroperation> ops =new ArrayList<contentprovideroperation>();
  ops.add(ContentProviderOperation.newUpdate(Data.CONTENT_URI)
           .withSelection(Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + 
           "' AND "+ Phone.NUMBER + " IS NOT NULL" + 
           "  AND "+ Data._ID + "='" + iddelcontacto +"' " ,null)
           .withValue(Phone.NUMBER, numerotelefono )
           .build() 
          ); 
  ContentProviderResult[] resultado = getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);      
}

Veamos detalladamente que hace el código anterior.
Necesitaremos llenar un array de tipo ContentProviderOperation que contiene en este caso la operación de actualización que vamos a realizar.
Para actualizar utilizamos el método newUpdate de la clase ContentProviderOperation, al que le indicaremos la URI que vamos a modificar. Una URI no es más que una cadena de texto que va a definir un recurso, como pueden ser las direcciones web. Podemos encontrar en la clase Data su CONTENT_URI. Su contenido es "content://com.android.contacts/data". Cada tabla contiene una constante con su URI.
Con - Data.CONTENT_URI estamos diciendo que vamos a trabajar sobre la información que se encuentra en DATA como habiamos mencionado anteriormente.
Debemos especificar la selección de datos a actualizar y el nombre del campo a modificar con el nuevo valor, esto lo hacemos con los métodos withSelection y withValue respectivamente.

En  withSelection  filtramos a manera de query los datos que vamos a actualizar, hacemos los siguientes filtros:
EL Data.MIMETYPE debe ser de tipo Phone.CONTENT_ITEM_TYPE, con esto me traerá solamente los números de teléfonos.
Phone.NUMBER IS NOT NULL, para que solo devuelva los teléfonos que tienen números.
Data._ID = iddelcontacto para que actualice el teléfono de un solo contacto y no de todos.

En withValue  indicamos el campo a modificar que es Phone.NUMBER y le asignamos el nuevo valor que se recibe como parámetro.

Todos estos datos Phone.NUMBER, Data.ID, Data.MIMETYPE, etc. es información que se encuentra almacenada dentro de un ContactsContract.Data.

El segundo método que implemente es : modificarTodoslosContactos().
Aquí básicamente lo que hacemos es consultar todos los números telefónicos de la agenda e ir actualizando cada uno de ellos.

public int modificarTodoslosContactos(){
// consulto todos los contactos del telefono siempre que tengan un número de teléfono
  Cursor mCursor = getContentResolver().query(
     Data.CONTENT_URI,
     new String[] { Data._ID, Data.DISPLAY_NAME, Phone.NUMBER, Phone.TYPE },
     Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "' AND "
     + Phone.NUMBER + " IS NOT NULL", null,
     Data.DISPLAY_NAME + " ASC");
  String id, fono;
  int contador=0;  
  //Recorro el cursor y modifico el número telefónico de cada contacto
  if (mCursor.moveToFirst()){
     while (mCursor.moveToNext()){
        id = mCursor.getString(0);
        fono = mCursor.getString(2);
           try {
              modificarcontactosandroid(id, fono.charAt(0)+"9"+fono.substring(1));
           } catch (RemoteException e) {
              contador ++;
              e.printStackTrace();
           } catch (OperationApplicationException e) {  
              contador ++;
              e.printStackTrace();
           }
     }
  }return contador;
}

Para consultar todos los teléfonos de la agenda, lo hacemos mediante el método getContentResolver () de clase android.content.Context, para llamarlo se necesita una instancia de contexto (actividad o servicio, por ejemplo). En nuestro caso lo vamos a llamar desde el Activity principal de la aplicación.

El  getContentResolver  me permite llamar al método query que recibe los siguientes parámetros:
  • LA URI, utilizaremos la misma del método anterior Data.CONTENT_URI.
  • El segundo parámetro es un array de String con los datos a recuperar new String[] { Data._ID, Data.DISPLAY_NAME, Phone.NUMBER, Phone.TYPE }. Aquí decimos que queremos obtener el id, nombre, telefono y tipo de teléfono del contacto.
  • El tercer parámetro es la selección de datos o where por así decirlo y le enviamos la siguiente cadena: Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "' AND " + Phone.NUMBER + " IS NOT NULL". Con esto estamos especificando que solo queremos obtener los datos que corresponden a números telefónicos y que no estén nulos.
  • El cuarto parámetro es un arreglo de argumentos que tuviésemos que pasar al Where, en este caso le envio null porque en el where concatene todos los parámetros.
  • El quinto parámetro son los campos por los que deseamos que se ordenen los resultados.
El resultado que me devuelve getContentResolver lo puedo almacenar en un objeto de tipo android.database.Cursor y recorrerlo como cualquier cursor con el método moveToNext, con cada elemento del cursor recupero el id del contacto y el número de teléfono.

Luego llamo al método creado anteriormente modificarcontactosandroid enviándole por número de teléfono el número modificado con el "9" como segundo dígito.
De esa manera actualizo todos los contactos de la Agenda de Android.

Algo a tener en cuenta es que para que la aplicación funciones debe tener los permisos:

<uses -permission="-permission" android:name="android.permission.READ_CONTACTS"></uses>
<uses -permission="-permission" android:name="android.permission.WRITE_CONTACTS"></uses>
Agregados en el archivo de configuración AndroidManifiest.xml

El activity principal completo quedaría de la siguiente manera, Los métodos desarrollados anteriormente son llamados desde botones

El proyecto completo de Eclipse lo puede descargar AQUÍ

package com.appcontactos;
package com.appcontactos;

import java.util.ArrayList;

import android.app.Activity;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.Context;
import android.content.Intent;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.os.Bundle;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.Data;
import android.view.Menu;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends Activity{
private static final int RESPUESTASELECCIONCONTACTO = 1;
private TextView vPhone;
private String idseleccionado="";
private String fonoseleccionado="";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

vPhone = (TextView) findViewById(R.id.Txt1);

findViewById(R.id.boton1).setOnClickListener(
 new View.OnClickListener() {
  
  public void onClick(View arg0)
  {
   Intent intent = new Intent("com.appcontactos.SELECCIONARCONTACTO");
   startActivityForResult(intent, RESPUESTASELECCIONCONTACTO);
  }
 }
);


findViewById(R.id.boton2).setOnClickListener(
  new View.OnClickListener() {
   
   public void onClick(View arg0)
   {
    CharSequence msj;
    try {
     if(idseleccionado.equals("")){
      msj = "Debe Seleccionar un Teléfono para modificarlo";
      Toast.makeText(getApplicationContext(), msj, Toast.LENGTH_SHORT).show();
     }else{
      modificarcontactosandroid(idseleccionado,
        fonoseleccionado.charAt(0)+"9"+fonoseleccionado.substring(1));
      
      idseleccionado="";
      fonoseleccionado ="";
      msj = "Se ha modificado el telefono,  Volver a Revisar";
      Toast.makeText(getApplicationContext(), msj, Toast.LENGTH_SHORT).show();
     }
    } catch (RemoteException e) {
     e.printStackTrace();
    } catch (OperationApplicationException e) {
     e.printStackTrace();
    }
         
   }
  }
 );

findViewById(R.id.boton3).setOnClickListener(
  new View.OnClickListener() {                 
   public void onClick(View arg0)
   {
    int c = modificarTodoslosContactos(); 
    CharSequence msj;
    if(c==0)
     msj = "Se han modificado Todos los Teléfonos,  Volver a Revisar";
    else
     msj = "Ocurrió un error modificando "+ c +" teléfonos,  Volver a Revisar";
    
    Toast.makeText(getApplicationContext(), msj, Toast.LENGTH_SHORT).show();
   }
  }
 );


}

//método que modifica un contacto de android, recibe el id del contacto y el nuevo número
private void modificarcontactosandroid(String iddelcontacto,String numerotelefono) throws RemoteException, OperationApplicationException{
ArrayList<ContentProviderOperation> ops =new ArrayList<ContentProviderOperation>();

ops.add(ContentProviderOperation.newUpdate(Data.CONTENT_URI)
  .withSelection(Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + 
    "' AND "+ Phone.NUMBER + " IS NOT NULL" + 
    "  AND "+ Data._ID + "='" + iddelcontacto +"' " ,null)
  .withValue(Phone.NUMBER, numerotelefono )
  .build() 
  );

 ContentProviderResult[] resultado = 
   getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);      
}

//Método que modifica todos los contactos, le añade un 9 después del primer digito
public int modificarTodoslosContactos(){
// consulto todos los contactos del telefono siempre que tengan un número de telefono
Cursor mCursor = getContentResolver().query(
  Data.CONTENT_URI,
  new String[] { Data._ID, Data.DISPLAY_NAME, Phone.NUMBER, Phone.TYPE },
  Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "' AND "
    + Phone.NUMBER + " IS NOT NULL", null,
  Data.DISPLAY_NAME + " ASC");

String id, fono;
int contador=0;

//Recorro el cursor y modifico el número telefónico de cada contacto
if (mCursor.moveToFirst()){
 while (mCursor.moveToNext()){
  id = mCursor.getString(0);
  fono = mCursor.getString(2);
  try {
   modificarcontactosandroid(id, fono.charAt(0)+"9"+fono.substring(1));
  } catch (RemoteException e) {
   contador ++;
   e.printStackTrace();
  } catch (OperationApplicationException e) {  
   contador ++;
   e.printStackTrace();
  }
 }
}
return contador;
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
 if ((requestCode == RESPUESTASELECCIONCONTACTO)
   && (resultCode == Activity.RESULT_OK)) {
  try {
   String phone = data.getStringExtra("phone");
   String idsel = data.getStringExtra("id");
   vPhone.setText(phone);
   this.idseleccionado = idsel;
   this.fonoseleccionado = phone;
  } catch (Exception e) {
   e.printStackTrace();
  }
 }
}

}

A continuación muestro algunas Pantallas de la Aplicación Funcionando (No fijarse en el Diseño). Se Seleccionó el número del contacto Call Center y luego se lo modificó obteniendo el mismo número con el "9" como segundo dígito.



Se que falta documentar un poco el código, pero en general si esta entendible. Cualquier comentario o sugerencia lo pueden hacer a ancantos99@gmail.com

DESCARGAS:
PROYECTO EN ECLIPSE 
APK DE LA APLICACIÓN

7 comentarios:

Andres118 dijo...

Cambia todos los contactos asi ya esten actualizados, internacionales, fijos y si presionas el boton mas de una ves coloca varios nueves ademas al presionar el boton se cuelga la aplicacion no c puede hacer nada....... no es mala la idea pero falta muchisimas cosas que validar y mejorar

Unknown dijo...

Gracias por el comentario Andrés, bueno antes que nada aclarar que este código lo escribí en un par de hora... y si tienes razón faltan muchas validaciones y cosas por mejorar.!! el objetivo del post era hacer una introducción del funcionamiento del api de contactos. y utilicé el escenario del numero 9 para esto... Por eso si el objetivo era actualizar la agenda al inicio del post recomendaba descargar una de las tantas aplicaciones que existen para esto...!! o en su debido caso terminar el código.. =) saludos.. !!!

Unknown dijo...

Ahh y no se cae.. simplemente toma su tiempo en terminar..!! faltaría un feedback que muestre el porcentaje de progreso..!!! Saludos..!!!

JATT dijo...

NO PUEDO DESCARGAR PORQUE

Unknown dijo...

Lo siento parece que se han roto los enlaces del servidor donde comparti los archivos..!!! y lamento deciros que hace poco sufrí un percance y perdí toda la información de mi laptop..!! junto con este pequeño código.... =(
He estado medio ocupado.. pero tengo unas ideas interesantes XD y pronto subiré mas ejemplos..!!
Para la próxima tendré mas cuidado con el servidor que utilizo para compartir los ejemplos...

Unknown dijo...

Hola:
Para filtrar los contactos que no salgan repetidos como se haría?
Mi código es este:


Cursor mCursor = getContentResolver().query(
Data.CONTENT_URI,
new String[]{ Data._ID, Data.DISPLAY_NAME, Phone.NUMBER,Phone.TYPE },
Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "' AND "+ Phone.NUMBER + " IS NOT NULL",
null,
Data.DISPLAY_NAME + " ASC");
startManagingCursor(mCursor);


@SuppressWarnings("deprecation")
ListAdapter adapter = new SimpleCursorAdapter(this, // context
R.layout.list,mCursor, new String[] { Data.DISPLAY_NAME, Phone.NUMBER }, // fields
new int[] { android.R.id.text1, android.R.id.text2 } // view
// fields
);
int numerocontactos = adapter.getCount();
setListAdapter(adapter);

Que debo modificar en la query para que no me salgan contactos repetidos¿?

Unknown dijo...

Saludos,
bueno no estoy seguro si funcione.. pero se me ocurre usar un distinct en el arreglo que se le envia al getcontentresolver...
quedaría así:
getContentResolver().query(Data.CONTENT_URI, new String[] { Data._ID, Data.DISPLAY_NAME, DISTINCT Phone.NUMBER, Phone.TYPE },... etc..

o tmb pruebalo con "DISTINCT "+Phone.NUMBER; si es que da error..!

también se puede usar group by en la claúsula del where.
Pruébalo ojala funcione.!

Sobre el proyecto temo que se extravió.. voy a ver si vuelvo armar el proyecto para compartirlo nuevamente esta vez en una repositorio mas seguro.!!

Suerte.!!

Publicar un comentario