Índice
1Librerías de compatibilidad y servicios...3
1.1Compatibilidad de la aplicación...3
1.2Fragmentos...4
1.3Loaders...11
1.4Librerías de compatibilidad... 14
1.5Librerías de servicios... 16
2Ejercicios de fragmentos y compatibilidad... 19
2.1Lector de noticias (1,5 puntos)...19
2.2Carga de noticias (1 punto)... 20
2.3Servicios de Google (0,5 puntos)...20
3Agenda y calendario... 22
3.1Agenda de contactos... 22
3.2Calendario... 27
4Ejercicios de agenda y calendario... 33
4.1Acceso a la agenda de contactos (1 punto)... 33
4.2Agregar contactos a la agenda (1,5 puntos)... 33
4.3Creación de eventos del calendario (0,5 puntos)... 33
5Servicios... 35
5.1Servicios propios...35
5.2Broadcast receiver...42
5.3PendingIntents y servicios del sistema... 44
5.4Comunicación entre procesos... 45
6Ejercicios - Servicios...50
6.1Contador: Servicio con proceso en background (0.6 puntos)... 50
6.2Broadcast Receiver: Captura de llamadas (0.6 puntos)... 50
6.3Broadcast Receiver: Reenvío de datos (0.6 puntos)... 51
6.4Arranque: Iniciar servicio al arrancar el móvil (0.6 puntos)...52
6.5Calculadora: Comunicación con el servicio (0.6 puntos)... 52
7AppWidgets...54
7.1AppWidgets... 54
7.2Crear un Widget... 55
7.3Actualización del Widget...59
7.4Eventos de actualización...60
7.5Servicio de actualización... 62
7.6Actividad de configuración...63
8Ejercicios - AppWidgets... 65
8.1IP AppWidget (1.5 puntos)... 65
8.2StackWidget (1.5 puntos)...67
9Notificaciones...69
9.1Notificaciones Toast... 69
9.2Notificaciones de la Barra de Estado... 73
9.3Cuadros de Diálogo...77
10Ejercicios - Notificaciones...82
10.1Notificaciones con Toast (1 punto)... 82
10.2Servicio con notificaciones: Números primos (1 punto)...82
10.3Notificaciones mediante diálogos (1 punto)...83
11Depuración y pruebas... 86
11.1Depuración con Eclipse...86
11.2Pruebas unitarias con JUnit para Android...88
11.3Pruebas de regresión con Robotium...90
11.4Pruebas de estrés con Monkey... 93
12Ejercicios - Depuración y pruebas... 94
12.1Caso de prueba con JUnit para Android (3 puntos)... 94
1. Librerías de compatibilidad y servicios
Vamos a ver algunas librerías adicionales que podemos añadir a nuestras aplicaciones, para conseguir compatibilidad con dispositivos antiguos o para acceder a servicios externos. Por ejemplo, una característica importante aparecida en versiones recientes de Android son los fragmentos. Es recomendable construir nuestras aplicaciones utilizando estos elementos para la interfaz, pero si lo hacemos nuestra aplicación ya no sería compatible con dispositivos Android 2.2 y 2.3, que son una parte importante del mercado actual. Vamos a ver cómo podemos resolver esto incluyendo librerías adicionales de compatibilidad. También veremos cómo incluir librerías adicionales que nos den acceso a los servicios de Google Play.
1.1. Compatibilidad de la aplicación
A la hora de desarrollar una aplicación es importante decidir a qué versiones de la plataforma Android está destinada. Las últimas versiones nos dan muchas más facilidades para crear las aplicaciones, pero es importante dar soporte a versiones antiguas para abarcar a un mayor número de usuarios. Se recomienda que nuestras aplicaciones soporten al menos el 90% de los dispositivos que hay actualmente en uso. En el momento de la escritura de este texto, esto implicaría dar soporte al menos a partir de Android 2.2.
Para especificar el rango de versiones a las que destinamos nuestra aplicación se utilizan los atributos minSdkVersion y targetSdkVersion de la etiqueta uses-sdk del
AndroidManifest.xml:
<uses-sdk android:minSdkVersion="4" android:targetSdkVersion="17" />
El atributo minSdkVersion indica la versión mínima de Android necesaria para poder utilizar nuestra aplicación. Por debajo de dicha versión nuestra aplicación no funcionará.
Sin embargo, este atributo no nos condiciona a utilizar sólo las características compatibles con la versión mínima. Podemos utilizar características de versiones mayores. La versión para la cual hemos diseñado la aplicación se indica contargetSdkVersion. En el código podremos utilizar cualquier característica que soporte dicha versión.
Pero, ¿qué ocurre si utilizamos una característica detargetSdkVersionno soportada en
minSdkVersion? En ese caso, cuando probemos la aplicación en un dispositivo con la versión mínima fallará. Por lo tanto, es importante que antes de utilizar dichas características comprobemos la versión de Android en la que se está ejecutando:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { // Utilizar características de Android 3.0 (Honeycomb) }
Nota
En el caso de las propiedades de los ficheros XML, si utilizamos atributos que no estaban definidos en la versión mínima, cuando ejecutemos la aplicación en dicha versión simplemente
serán ignorados.
Deberemos llevar cuidado de hacer siempre esta comprobación cuando estemos utilizando características no presentes en la versión mínima. Para asegurar el correcto funcionamiento deberemos probar la aplicación de forma exhaustiva con dispositivos que tengan tanto la versión mínima como la versión para la cual estamos desarrollando.
Sin embargo, existen algunas características, como por ejemplo los fragmentos, que son fundamentales en la construcción de la aplicación, y no se pueden ignorar mediante la comprobación anterior. Para estas características veremos a continuación cómo utilizar una librería de compatibilidad que añada soporte para versiones previas de Android.
1.2. Fragmentos
A partir de Android 3.0 aparece una característica importante a la hora de construir la interfaz: los fragmentos de actividades. Estos fragmentos nos permiten construir la interfaz de forma modular. Un fragmento se define como un panel, y una actividad puede estar compuesta por varios fragmentos. De esta forma podremos reutilizar los fragmentos en diferentes actividades, y podremos construir actividades complejas de forma sencilla.
Una ventaja importante es que nos permiten adaptar de forma sencilla la interfaz a distintos tipos de pantallas, como tabletas, ya que distintos fragmentos que en un móvil están en distintas pantallas, en una tableta podrían incluirse en una única pantalla, reutilizando su código.
Fragmentos
1.2.1. Creación de fragmentos
Los fragmentos se crean como una subclase de Fragment, casi de la misma forma en la que se define una actividad. Se utilizan los mismos métodos para controlar el ciclo de vida, y debemos definir un método adicionalonCreateViewque especifica la forma en la que se debe generar la interfaz del fragmento. La interfaz se generará normalmente a partir de un layout XML:
public class DetalleFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragmento, container, false);
} }
Además de este método anterior, tenemos disponibles todos los métodos que teníamos en el ciclo de vida de las actividades (onCreate, onStart, onResume, etc). Pero debemos tener en cuenta que un fragmento no es una actividad, por lo que por ejemplo no heredamos métodos como findViewById, y tampoco podemos utilizarlo en los métodos que nos piden pasar como parámetro el contexto (Context), donde normalmente siempre indicabamos nuestra actividad. Para poder hacer todo esto deberemos acceder a la actividad en la que está contenido nuestro fragmento. Esto lo haremos con el método
getActivity():
Button boton = (Button)getActivity().findViewById(R.id.boton);
Al igual que ocurría con las actividades, tenemos diferentes subclases deFragment para definir tipos específicos de fragmentos, como ListFragment, DialogFragment, o
PreferencesFragment. Por ejemplo, si queremos definir un fragmento de tipo lista, podemos heredar directamente deListFragment:
public class PrincipalFragment extends ListFragment { ...
}
Por otro lado, DialogFragment nos permitirá crear diálogos. En este caso el contenido del fragmento se mostrará dentro de la ventana del diálogo.
1.2.2. Ciclo de vida de los fragmentos
Los fragmentos siempre estarán contenidos dentro de una actividad, por lo que su ciclo de vida quedará vinculado al estado de la actividad contenedora. En la siguiente figura mostramos los distintos eventos del ciclo de vida de los fragmentos, y la vinculación con el estado de la actividad a la que pertenecen:
Ciclo de vida
En la anterior figura podemos observar que durante el estado de creación de la actividad, en el fragmento se producen diferentes eventos. En primer lugar el fragmento se vincula a la actividad (onAttach). Tras esto se crea el fragmento (onCreate), pero debemos tener en cuenta que en este punto todavía no contamos con su interfaz, por lo que no podemos inicializarla. Este método será útil por ejemplo para inicializar el adapter que pueble de datos la lista de un ListFragment, en el que no es necesario crear la interfaz nosotros.
Tras este evento, tenemosonCreateView, que es donde deberemos crear la interfaz como hemos visto anteriormente (excepto si estamos en un ListFragment, caso en el que la interfaz ya viene predefinida). Por último, se ejecutaráonActivityCreateduna vez haya terminado de ejecutarse el métodoonCreatede la actividad contenedora.
Los eventos onStart, onResume, onPause y onStop están directamente vinculados con sus equivalentes en la actividad contenedora.
La destrucción será similar a la inicialización pero en orden inverso. En primer lugar se elimina la vista (onDestroyView). Esto puede hacerse para liberar recursos, pero el
fragmento podría volver a utilizarse posteriormente. Si esto ocurriese, se volvería a llamar a onCreateView para volver a construir la vista. Si el fragmento ya no se va a utilizar más, se llamará a onDestroy, y tras esto a onDetach en el momento en el que se desvincula de la actividad.
1.2.3. Añadir el fragmento a una actividad
Una vez definido el fragmento, podemos añadirlo a una o varias actividades. Existen dos formas de añadir los fragmentos:
• Estática: Se añaden directamente en el layout XML de la actividad. Si se hace de esta forma no podremos modificar el conjunto de fragmentos que se muestran en la
actividad en tiempo de ejecución (la actividad siempre mostrará los mismos fragmentos dispuestos de la misma forma).
• Dinámica: Los fragmentos se añaden desde código. De esta forma podremos cambiar en cualquier momento el fragmento que se está mostrando en la actividad.
Utilizaremos esta forma cuando queramos poder hacer transiciones entre fragmentos.
La definición de fragmentos mediante el método estático se realizaría de la siguiente forma:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<fragment android:name="es.ua.jtech.fragments.PrincipalFragment"
android:id="@+id/principal_fragment"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent" />
<fragment android:name="es.ua.jtech.fragments.DetalleFragment"
android:id="@+id/detalle_fragment"
android:layout_weight="2"
android:layout_width="0dp"
android:layout_height="match_parent" />
</LinearLayout>
Si queremos dar soporte a varios tamaños de dispositivos, simplemente deberíamos definir diferentes layouts alternativos con distintos clasificadores (large,xlarge, etc).
Pero, ¿qué ocurre si queremos dar soporte a dispositivos más pequeños en los que los dos fragmentos no caben en la misma pantalla? En este caso podemos utilizar la método dinámico, para mostrar un único fragmento en pantalla y poder cambiar a otro de forma dinámica. Para hacer esto deberemos definir un layout alternativo para la actividad (por ejemplo con clasificador small o normal) en el que definamos un marco vacío donde podamos poner el fragmento que queramos directamente (FrameLayout):
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
En nuestra actividad, en primer lugar deberemos comprobar si el layout cargado es el estático o el dinámico:
public class MainActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
setContentView(R.layout.news_articles);
// Comprueba si estamos usando el layout dinámico if (findViewById(R.id.fragment_container) != null) {
// Si se está restaurando, no hace falta cargar el fragmento if (savedInstanceState != null) {
return;
}
// Creamos el fragmento
PrincipalFragment ppalFragment = new PrincipalFragment();
// Pasamos los extras del intent al fragmento ppalFragment.setArguments(getIntent().getExtras());
// Añadimos el fragmento al contenedor getFragmentManager().beginTransaction()
.add(R.id.fragment_container, ppalFragment).commit();
} } }
Como podemos observar, para modificar los fragmentos en pantalla debemos obtener un objetoFragmentManager y a partir de él abrir una transacción (FragmentTransaction).
Una vez se haya hecho la operación oportuna (en este caso add), confirmaremos la transacción (commit).
1.2.4. Transiciones entre fragmentos
En el caso en el que hayamos añadido el fragmento de forma dinámica, podremos hacer una transición a otro fragmento utilizando el objetoFragmentTransaction.
DetalleFragment detalleFragment = new DetalleFragment();
// Pasamos parametros al nuevo fragmento Bundle args = new Bundle();
args.putInt(PARAM_POSICION, posicionSeleccionada);
detalleFragment.setArguments(args);
FragmentTransaction transaction =
getFragmentManager().beginTransaction();
transaction.replace(R.id.fragment_container, detalleFragment);
transaction.addToBackStack(null);
transaction.commit();
En este caso vemos que tras reemplazar el fragmento, se añade la operación a la back stack, para así poder volver al fragmento anterior pulsando la tecla atrás.
1.2.5. Comunicación entre fragmentos
Cuando hacemos una transición entre fragmentos, si un fragmento tiene que pasar datos a otro lo puede hacer en el momento de la transición, como hemos visto en el caso anterior.
Pero si todos los fragmentos se muestran de forma estática, necesitaremos poder comunicarlos para que por ejemplo, cuando pulsemos un item de un fragmento lista, en otro fragmento veamos los detalles de dicho item.
La comunicación entre fragmentos siempre lo haremos a través de la actividad a la que pertenecen. En uno de los fragmentos definiremos una interfaz que deba implementar la actividad:
public class PrincipalFragment extends ListFragment { OnItemSelectedListener mCallback;
// La actividad debe implementar esta interfaz public interface OnItemSelectedListener {
public void onItemSelected(int position);
}
@Override
public void onAttach(Activity activity) { super.onAttach(activity);
// Comprueba que la actividad implemente la interfaz definida try {
mCallback = (OnItemSelectedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " debe implementar OnItemSelectedListener");
} } ...
}
El métodoonAttachse ejecutará cuando el fragmento se vincule a la actividad. Aquí nos guardamos una referencia a la actividad mediante un campo del tipo del listener recibido, con el que podremos notificar a la actividad cada vez que se haya seleccionado un item.
En el fragmento anterior podemos utilizar esta referencia a la actividad para hacer un callback en el momento en el que se pulse sobre un item de la lista:
public class PrincipalFragment extends ListFragment { ...
@Override
public void onListItemClick(ListView l, View v, int position, long id) { mCallback.onItemSelected(position);
} }
En la actividad deberemos implementar el listener definido, y en él deberemos pasar el mensaje al fragmento con los detalles. Esto se hará de forma distinta según si utilizamos el método estático o dinámico.
public static class MainActivity extends Activity
implements PrincipalFragment.OnItemSelectedListener { ...
public void onItemSelected(int position) {
DetalleFragment detalleFragment = (DetalleFragment)
getFragmentManager().findFragmentById(R.id.detalle_fragment);
if (detalleFragment != null) {
// Tipo estático: actualizamos directamente el fragmento detalleFragment.setDetalleItem(position);
} else {
// Tipo dinámico: hacemos transición al nuevo fragmento detalleFragment = new DetalleFragment();
Bundle args = new Bundle();
args.putInt(PARAM_POSICION, position);
detalleFragment.setArguments(args);
FragmentTransaction transaction =
getFragmentManager().beginTransaction();
transaction.replace(R.id.fragment_container, detalleFragment);
transaction.addToBackStack(null);
transaction.commit();
} } }
En el caso estático, basta con definir un método propio en el fragmento que nos permita actualizar su contenido. En el caso dinámico, tendremos que hacer una transición al nuevo fragmento pasándole los datos del item seleccionado.
1.2.6. Uso de diálogos
Como hemos visto anteriormente, podemos crear diálogos utilizando fragmentos (de hecho, esta es la forma recomendada de hacerlo). Simplemente deberemos crear una clase que herede de DialogFragment. El fragmento se definirá de la misma forma que cualquiera de los anteriores, cargando su contenido en onCreateView. En este caso, además de cargar la vista, también podremos especificar un título para la ventana del diálogo con el métodogetDialog().setTitle():
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.dialog, container);
getDialog().setTitle("Título ");
return view;
}
Donde encontramos mayores diferencias respecto a los fragmentos anteriores es la forma de mostrarlo. En este caso lo haremos de la siguiente forma:
FragmentManager manager = getFragmentManager();
MiDialogFragment dialog = new MiDialogFragment();
dialog.setArguments(bundle);
dialog.show(manager, "fragment_dialog");
Podemos observar que en este caso el propio fragmento tiene un método show que nos permite mostrarlo en pantalla (proporcionando como parámetro el fragment manager y una etiqueta identificativa opcional para el diálogo que nos permitirá localizarlo más adelante).
Al igual que en casos anteriores, al fragmento se le pueden pasar datos en el momento de su creación proporcionando un bundle.
1.3. Loaders
Otra características aparecida en Android 3.0 son los loaders. Esta característica está disponible en cualquier fragmento o actividad. Dichos fragmentos o actividades normalmente necesitan cargar datos al ejecutarse (por ejemplo de un servicio web, o de una base de datos SQlite). Los loaders están pensados para realizar esta tarea de forma asíncrona.
Una ventaja de los loaders es que una vez han cargado los datos los retienen aunque la actividad se detenga (onStop), de forma que no será necesario volverlos a cargar cuando la actividad se vuelva a poner en marcha (onStart). Además, el loader estará pendiente de los cambios que se produzcan en su fuente de datos. Si detecta un cambio, obtendrá los nuevos datos y se los volverá a proporcionar a nuestra actividad o fragmento de forma automática.
La clase principal que deberemos utilizar es LoaderManager, que nos permitirá manejar varios loaders. Normalmente pondremos en marcha los loaders en el método onCreate de nuestra actividad o en el métodoonActivityCreatedde un fragmento:
getLoaderManager().initLoader(0, null, this);
El primer parámetro es un código identificador del loader que queremos poner en marcha.
Si ya existe un loader activo con dicho código, se reutilizará dicho loader. Si no existe, se creará uno nuevo. Para crear un loader necesita que se le proporcione un objeto que implemente la interfaz LoaderManager.LoaderCallbacks (como tercer parámetro del método anterior debemos indicar dicho objeto). Normalmente haremos que sea la propia actividad o fragmento quien la implemente (this en el ejemplo anterior), lo cual nos obligará a definir los siguientes métodos:
public class MiListFragment extends ListFragment
implements LoaderManager.LoaderCallbacks<Tipo> { ...
public Loader<Tipo> onCreateLoader(int id, Bundle args) { ... } public void onLoadFinished(Loader<Tipo> loader, Cursor data) { ... } public void onLoaderReset(Loader<Tipo> loader) { ... }
}
Podemos observar que en el callback hay que especificar el tipo de datos que maneja el loader, es decir, el tipo de datos que queremos cargar con él, mediante el uso de genéricos.
1.3.1. Creación de un loader
Cuando solicitemos iniciar un loader que todavía no está en funcionamiento se llamará al método onCreateLoader en el que deberemos especificar la forma de crearlo. Este método recibe como parámetro el identificador del loader que debemos crear.
Podemos crear el loader mediante alguna clase derivada de Loader. Habitualmente utilizaremos CursorLoader para cargar datos de proveedores de contenidos, o
AsyncTaskLoaderpara cargar datos de forma personalizada mediante una async task.
Por ejemplo, para crear un loader de tipo cursor que lea los datos de un proveedor de contenidos, haremos lo siguiente:
public Loader<Cursor> onCreateLoader(int id, Bundle args) { return new CursorLoader(getActivity(), baseUri, proyeccion,
seleccion, args, orden);
}
1.3.2. Obtención de resultados
Cuando el loader haya terminado de cargar los datos (puede que esto sea instantáneo si ya estaban cargados de antemano), se llamará al método onLoaderFinished. En este método deberemos utilizar los datos obtenidos, por ejemplo para mostrarlos en pantalla.
Por ejemplo, si contamos con un CursorAdapter podemos proporcionarle el nuevo cursor:
public void onLoadFinished(Loader<Cursor> loader, Cursor data) { mAdapter.swapCursor(data);
}
Es importante no cerrar el cursor anterior, ya que de esto se encargará el loader.
1.3.3. Reinicio del loader
Si queremos que el loader vuelva a cargar los datos (por ejemplo por haber cambiado algún criterio de búsqueda), podemos utilizar el método restartLoader, de la misma forma en la que se utilizabainitLoader:
getLoaderManager().restartLoader(0, null, this);
En este caso se deberán eliminar los datos cargados anteriormente, y comenzar una nueva carga. En el método onLoadReset deberemos indicar la forma de eliminar estos datos
(por ejemplo, eliminando el cursor delCursorAdapter):
public void onLoaderReset(Loader<Cursor> loader) { mAdapter.swapCursor(null);
}
1.3.4. Loader personalizado
Si queremos realizar la carga de datos de forma personalizada, lo más sencillo será crear una subclase de AsyncTaskLoaderen la que programaremos cómo se realiza la descarga en segundo plano.
Se define de forma parecida a la clase AsyncTask. En este caso el método principal que deberemos definir esloadInBackground, que será el que se ejecutará en segundo plano para realizar la carga de datos, pero además deberemos sobrescribir otros métodos para controlar el proceso de descarga. A parte de loadInBackground, al menos deberemos definir onStartLoading que deberá comprobar si ya tenemos disponibles los datos actualizados, o si por el contrario debemos volverlos a cagar. Si ya dispusiésemos de ellos, los devolveremos llamando a deliverResult(datos), y en caso contrario llamaremos a forceLoad() para hacer que se vuelvan a descargar (esto provocará la llamada a loadInBackground desde un hilo en segundo plano). Es importante destacar que si no sobrescribimos onStartLoading, siempre considerará por defecto que ya dispone de los datos y nunca se realizará la descarga.
En el siguiente ejemplo almacenamos los datos descargados en una variable de instancia, y con ella controlaremos si es necesario descargarlos (si es null) o si ya disponemos de ellos y podemos devolverlos. Para ello sobrescribimos tambiéndeliverResult, para que tras obtener los datos antes de devolverlos se retengan en dicha variable de instancia.
También definimos el método onStopLoading (que detiene el proceso de carga) y
onReset (que detiene el proceso y además elimina los datos ya descargados para que la próxima vez se vuelvan a descargar).
static class MiLoader extends AsyncTaskLoader<Tipo> { Tipo mDatos = null;
public MiLoader(Context context) { super(context);
}
@Override
public Tipo loadInBackground() { return cargarDatos();
}
@Override
public void deliverResult(Tipo data) { mDatos = data;
super.deliverResult(data);
}
@Override
protected void onStartLoading() { super.onStartLoading();
if(mDatos != null) {
deliverResult(mDatos);
} else {
forceLoad();
} }
@Override
protected void onStopLoading() { super.onStopLoading();
cancelLoad();
}
@Override
protected void onReset() { super.onReset();
onStopLoading();
mDatos = null;
} }
Una vez finalizada la carga de datos, le proporcionará al callback los datos obtenidos.
Definiremos el callback de la siguiente forma:
public class MiFragmento implements LoaderManager.LoaderCallbacks { ...
public Loader<Tipo> onCreateLoader(int id, Bundle args) { return new MiLoader(getActivity());
}
public void onLoadFinished(Loader<Tipo> loader, Tipo data) { mAdapter.clear();
for(Item item: data) { mAdapter.add(item);
} }
public void onLoaderReset(Loader<Tipo> loader) { mAdapter.clear();
} }
Si detectásemos que ha habido algún cambio en nuestra fuente de datos, podemos llamar al método onContentChanged() del objeto AsyncTaskLoader para forzar que vuelva a descargar los datos y se los proporcione a nuestra actividad o fragmento.
1.4. Librerías de compatibilidad
Los fragmentos están disponibles sólo a partir de Android 3.0, y todavía existen en el mercado numerosos dispositivos con versiones anteriores. Sin embargo, el uso de fragmentos se ha convertido en una práctica muy recomendable en el diseño de aplicaciones, por lo que deberíamos utilizarlos. Para permitir que los desarrolladores utilicen esta nueva característica, pero no se pierda el soporte para dispositivos antiguos, se proporciona una librería de compatibilidad que incorpora las características más importantes de las nuevas versiones de Android a dispositivos con versiones anteriores
(da soporte a dispositivos desde Android 1.6). Vamos a ver cómo utilizar esta librería.
En primer lugar, deberemos descargar la librería con el Android SDK Manager (si no la tenemos ya). La encontraremos en la carpeta Extras > Android Support.
Librería de soporte Una vez descargada, podremos encontrarla en el directorio:
$ANDROID_SDK/extras/android/support/v4/android-support-v4.jar
Para incluirla en nuestro proyecto tendremos que crear en él un directoriolibs y copiar ahí dicha librería. Android reconocerá de forma automática todas las librerías introducidad enlibs.
Con esta librería ya podemos indicar en el ficheroAndroidManifest.xmlque la versión mínima a la que damos soporte es la 1.6 (nivel 4):
<uses-sdk android:minSdkVersion="4" android:targetSdkVersion="17" />
Ahora deberemos adaptar nuestra aplicación para que use la librería de compatibilidad para acceder a los fragmentos y loaders. Deberemos hacer una serie de modificaciones:
• Ahora todos losimportreferentes a fragmentos deberemos cogerlos del paquete
android.support.v4.appcorrespondiente a la librería de compatibilidad. De esta forma tendremos:
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.app.LoaderManager;
• La actividad contenedora de fragmentos ahora deberá heredar deFragmentActivity, en lugar de heredar simplemente deActivity:
import android.support.v4.app.FragmentActivity;
public class MainActivity extends FragmentActivity { ...
}
• Para obtener el objetoFragmentManagerahora utilizaremos el método
getSupportFragmentManager, y de la misma forma para obtenerLoaderManager
utilizaremosgetSupportLoaderManager:
FragmentManager manager = getSupportFragmentManager();
LoaderManager manager = getSupportLoaderManager();
Con los cambios anteriores conseguiremos que nuestra aplicación funcione correctamente en cualquier versión de Android desde la 1.6.
1.5. Librerías de servicios
Google proporciona una serie de servicios que pueden ser integrados en las aplicaciones Android, como Google Maps y Google+. En los dispositivos Android normalmente encontramos incluidas una serie de librerías de Google que nos permiten integrar Google Maps en nuestras aplicaciones. Incluir esta API en el propio sistema operativo del dispositivo hace que sea poco flexible a la hora de actualizar las aplicaciones de Google, ya que lo más común es que las actualizaciones del sistema operativo se proporcionen sólo durante un periodo limitado de tiempo.
Por este motivo Google ha pasado a incluir sus librerías de forma externa. Ahora sus librerías deben descargarse de Google Play, y se actualizan continuamente desde esa tienda. De esta forma la mayor parte de los dispositivos van a tener soporte para las últimas versiones de estos servicios.
Actualmente la librería de servicios de Google (que soporta Google Maps v2 y Google+) soporta todos los dispositivos Android a partir de la versión 2.2 (API de nivel 8). El único requerimiento es descargar las librerías de Google desde Google Play y mantenerlas actualizadas.
1.5.1. Configuración de las librerías de Google para el desarrollo
Para utilizar las librerías de Google en nuestros proyectos, deberemos descargarlas y añadirlas como proyecto a Eclipse. Seguiremos los siguientes pasos:
1. Descargamos las librerías desde Android SDK Manager. Las encontraremos en la carpeta Extras > Google Play services.
2. Una vez descargadas las podremos encontrar en la siguiente ruta, que contiene un proyecto Eclipse con dichas librerías:
$ANDROID_SDK/extras/google/google_play_services/
3. Importamos en Eclipse el proyecto que se encuentra en la ruta anterior, con File >
Import ... > Android > Existing Android Code into Workspace
Con esto tendremos las librerías de Android disponibles para ser utilizadas en nuestros proyectos. Para hacer que uno de nuestros proyectos las utilice deberemos añadir en él
una referencia al proyecto con las librerías. Para ello:
1. Pulsaremos con el botón derecho sobre el proyecto y seleccionaremos Properties.
2. Seleccionamos el apartado Android.
3. En el apartado Library pulsamos el botón Add ...
4. Seleccionamos el proyecto con las librerías de Google y pulsamos Ok.
5. Pulsamos sobre el botón Apply y tras esto Ok para cerrar el diálogo.
En nuestra aplicación deberemos tener en cuenta que es posible que los servicios de Google no estén disponibles en el dispositivo. Esto podemos comprobarlo con el siguiente método:
int status = GooglePlayServicesUtil.isGooglePlayServicesAvailable();
if(status == ConnectionResult.SUCCESS) { // Los servicios están disponibles }
1.5.2. Configuración de la aplicación
Una vez configurada la librería de servicios de Google, podemos utilizarla para integrar la versión 2 de los mapas. Para ello en primer lugar deberemos generar una clave de desarrollador asociada a nuestra aplicación y a nuestro certificado:
https://developers.google.com/maps/documentation/android/start
#the_google_maps_api_key
Una vez obtenida la clave, deberemos configurar el fichero AndroidManifest.xml. En primer lugar añadiremos una serie de permisos necesarios:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name=
"android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name=
"com.google.android.providers.gsf.permission.READ_GSERVICES"/>
<uses-permission android:name=
"android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name=
"android.permission.ACCESS_FINE_LOCATION"/>
Hay que añadir un permiso adicional propio (en lugar dees.ua.jtech deberemos poner el paquete de nuestra aplicación):
<permission
android:name="es.ua.jtech.permission.MAPS_RECEIVE"
android:protectionLevel="signature"/>
<uses-permission android:name="es.ua.jtech.permission.MAPS_RECEIVE"/>
La versión 2 de los mapas de Google necesita que los dispositivos soporten Open GL ES 2.0, por lo que esto también debe ser indicado en el manifest:
<uses-feature
android:glEsVersion="0x00020000"
android:required="true"/>
También deberemos especificar la clave de desarrollador, introduciendo la siguiente etiqueta justo antes de</application>:
<meta-data
android:name="com.google.android.maps.v2.API_KEY"
android:value="pon_aqui_tu_clave"/>
1.5.3. Integración de los mapas
Una vez tenemos configurada la aplicación, podremos integrar en ella los mapas de Google. Para ello podemos añadir un fragmento con el mapa a alguna de nuestras actividades:
<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"
class="com.google.android.gms.maps.MapFragment"/>
Con esto podremos ver el mapa integrado en nuestra actividad. En caso de que estuviésemos utilizando la librería de compatibilidad el código anterior no funcionará. En ese caso deberemos utilizar la claseSupportMapFragment:
<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"
class="com.google.android.gms.maps.SupportMapFragment"/>
2. Ejercicios de fragmentos y compatibilidad
Antes de empezar a crear los proyectos, debes descargarte las plantillas desde bitbucket.
Para ello:
1. Entraremos en nuestra cuenta debitbucket.org, seleccionaremos el repositorio git
expertomoviles/serv-android-expertomoviles(del que tendremos únicamente permisos de lectura), y haremos un Fork de dicho repositorio en nuestra cuenta, para así tener una copia propia del repositorio con permisos de administración.
2. Para evitar que bitbucket nos dé un error por sobrepasar el número de usuarios permitidos, debemos ir al apartado Access management de las preferencias del repositorio que acabamos de crear y eliminar los permisos de lectura para el grupo Estudiantes (tendremos estos permisos concedidos si al hacer el Fork hemos
especificado que se hereden los permisos del proyecto original). Los únicos permisos que debe tener nuestro repositorio deben ser para el propietario (owner) y para el usuario Experto Moviles.
3. Una vez tenemos nuestra copia del repositorio con las plantillas correctamente configuradas en bitbucket, haremos uncloneen nuestra máquina local:
git clone https://[usr]:bitbucket.org/[usr]/serv-android-expertomoviles
4. De esta forma se crea en nuestro ordenador el directorio
serv-android-expertomovilesy se descargan en él las plantillas para los ejercicios del módulo y un fichero.gitignore. Además, ya está completamente configurado y conectado con nuestro repositorio remoto, por lo que lo único que deberemos hacer será subir los cambios conforme realicemos los ejercicios, utilizando los siguientes comandos:
git add .
git commit -a -m "[Mensaje del commit]"
git push origin master
2.1. Lector de noticias (1,5 puntos)
Vamos a crear una aplicación que nos permita leer una serie de noticias obtenidas de un RSS. En primer lugar crearemos la interfaz utilizando fragmentos, para así adaptarla a teléfonos y tablets. En la aplicación encontramos dos fragmentos: MainFragment, que contiene una lista de noticias, y DetailFragment, que contiene los detalles de la noticia seleccionada. Deberemos:
a) En primer lugar vamos a definir el contenido del fragmento DetailFragment. En su método onCreateView deberemos cargar su contenido del layout
detail_fragment.xml.
b) Vamos a crear el layout de la actividad para tablets. En layout-large editaremos el contenido de activity_main.xml para introducir en él los dos fragmentos definidos en la aplicación. El fragmento principal se introducirá al comienzo del LinearLayout
principal, con peso (layout-weight)1. El fragmento con los detalles se introducirá en el
LinearLayout secundario, con altura wrap_content. Con esto podremos probar la aplicación en un simulador de tipo tablet para ver la composición que hemos creado.
c) Vamos a hacer ahora que la aplicación también funcione en móviles. Para ello, en el método onCreate de MainActivity comprobaremos si estamos utilizando el layout de móviles (miraremos si existe el contenedor R.id.fragment_container) y en tal caso crearemos el fragmento principal y lo añadiremos al contenedor.
d) Por último, vamos a comunicar los dos fragmentos. La comunicación será distinta según el tipo de dispositivo. En un móvil haremos una transición, mientras que en tablets comunicaremos los dos fragmentos que ya se muestran en pantalla a través de la actividad. En el método onNoticiaSelected de MainActivity comprobaremos en primer lugar si el fragmento con los detalles está accesible (en ese caso sabremos que estamos en un tablet). En caso de que exista, simplemente deberemos actualizar en él la noticia llamando a su método updateNoticia. En caso contrario, deberemos crear un nuevo fragmento de detalles, le pasaremos un parámetro "noticia" de tipo
Serializableen el bundle con la noticia seleccionada, y lo mostraremos en pantalla.
2.2. Carga de noticias (1 punto)
Vamos a actualizar el lector de noticias para que ahora lea las noticias a través de Internet utilizando para ello un loader. Deberemos:
a) En MainFragment definimos una clase interna de tipo AsyncTaskLoader que se encargue de cargar las noticias (puede utilizar para ello el método
DataSource.loadNoticias()). Este loader debe proporcionar objetos de tipo
List<Noticia>.
b) Hacer que dicha clase implemente el listener LoaderCallbacks, e implementar los métodos necesarios. Al crearse el loader se deberá proporcionar una instancia de la clase
AsyncTaskLoader definida en el punto anterior. Al obtenerse datos tendremos que introducirlos en el adapter (vaciándolo previamente), y al reiniciar el loader simplemente vaciaremos el adapter.
c) Por último, modificaremos el método onCreate de la clase anterior para que ya no introduzca ningún dato manualmente en el adapter, y que en su lugar lo que haga sea poner en marcha el loader. Con esto la aplicación deberá descargar las noticias del RSS proporcionado.
2.3. Servicios de Google (0,5 puntos)
En este ejercicio vamos a añadir al proyecto anterior un mapa de Google utilizando los servicios de Google Play. Para ello deberemos:
a) Descargar la librería de servicios de Google Play utilizando SDK Manager. Importar la
librería descargada en Eclipse, y añadir una dependencia de nuestro proyecto a ella.
b) Obtenemos la clave de la API para acceder a Google Maps v2 accediendo a la siguiente dirección:
https://developers.google.com/maps/documentation/android/start
c) Introducimos en el ficheroAndroidManifest.xmlla información necesaria:
• Indicar que es necesario contar con OpenGLES 2.0.
• Solicitar los permisos necesarios.
• Indicar la clave de la API obtenida.
d) Lo último que haremos será añadir el mapa a nuestra aplicación. Simplemente lo añadiremos como fragmento al fichero activity_main.xml de layout-large
(utilizando la librería de soporte). En caso de usar una tablet, veremos el mapa bajo los detalles de la noticia.
Nota
Los servicios de Google Play no funcionan en el emulador, sólo en un móvil real. Lo único que podremos ver en el emulador es un botón que nos invita a instalarnos Google Play, pero no podremos hacerlo.
3. Agenda y calendario
En esta sesión vamos a ver cómo utilizar proveedores de contenidos que nos den acceso a información personal del usuario manejada por el dispositivo, como es el caso de su agenda de contactos y sus calendarios. La forma de acceder a esta información ha ido variando a lo largo de las diferentes versiones de Android, hasta estandarizarse completamente en ICS (Ice Cream Sandwich, Android 4.0). Veremos cómo mantener la compatibilidad con versiones anteriores.
3.1. Agenda de contactos
El proveedor de la agenda de contactos se encuentra estructurado en tres tablas de datos:
• Contacts: Contiene la lista de contactos únicos. Un contacto puede tener varias cuentas (Google, Twitter, etc). Esta tabla unifica todas esas cuentas en una única entrada.
• RawContacts: En esta tabla tenemos entradas para cada cuenta concreta de un contacto. Una única entrada en Contacts puede estar relacionada con varias entradas en RawContacts, para cada cuenta diferente del usuario.
• Data: Esta es la tabla donde realmente están almacenados los datos de cada cuenta de usuario. Para cada cuenta (almacenada en RawContacts) tendremos un conjunto de datos almacenados en la tabla Data.
Tablas de contactos
El acceso al proveedor de la agenda de contactos se hace mediante una serie de constantes definidas en las siguientes subclases deContactsContract, cada una de ellas referida a una de las tablas anteriores:
• ContactsContract.Contacts
• ContactsContract.RawContacts
• ContactsContract.Data
En primer lugar, para que nuestra aplicación pueda acceder a los contactos (leerlos y/o modificarlos) deberemos solicitar el permiso correspondiente en el
AndroidManifest.xml:
<uses-permission android:name="android.permission.READ_CONTACTS">
<uses-permission android:name="android.permission.WRITE_CONTACTS">
3.1.1. Carga de contactos
Para leer los contactos almacenados en el dispositivos utilizaremos la clase
ContentsResolver, al igual que para cualquier otro tipo de contenidos. Por ejemplo, podríamos leer todos los contactos de la siguiente forma:
ContentResolver cr = getContentResolver();
Cursor cursor = cr.query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
En lugar de seleccionar todos los datos podemos indicar la proyección o selección que nos interese mediante constantes deContactsContract.Contacts.
mProjection = new String[] { ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME_PRIMARY };
mProfileCursor =
getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, mProjection, null, null, null);
En lugar de cargar el cursor directamente, también podríamos utilizar un loader con un
CursorAdapterpara cargar los datos del proveedor de contenidos.
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) { String[] projection = {
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME_PRIMARY };
String sortOrder = ContactsContract.Contacts.DISPLAY_NAME_PRIMARY +
" ASC";
return new CursorLoader(getApplicationContext(),
ContactsContract.Contacts.CONTENT_URI, projection, null, null, sortOrder);
}
El loaderCursorLoaderse encarga de cargar los datos de un proveedor de contenidos de forma asíncrona (internamente utilizaráContentResolver).
Nota
En versiones anteriores a Android 2.0 (API 5) el acceso al proveedor de contenidos de la agenda de contactos se hacía mediante constantes de la claseContacts.People. Si queremos hacer una aplicación compatible deberemos tener esto en cuenta y utilizar una u otra en función de la versión actual.
3.1.2. Acceso a datos de los contactos
Una vez tenemos el identificador de un contacto, podríamos obtener todas sus cuentas asociadas (raw contacts). Utilizaremos para ello las constantes definidas en
ContactsContract.RawContacts:
Cursor c = getContentResolver().query(
ContactsContract.RawContacts.CONTENT_URI,
new String[] { ContactsContract.RawContacts._ID,
ContactsContract.RawContacts.ACCOUNT_TYPE, ContactsContract.RawContacts.ACCOUNT_NAME }, ContactsContract.RawContacts.CONTACT_ID + "=?",
new String[] { String.valueOf(contactId) }, null);
Podemos obtener los datos un contacto utilizando constantes de
ContactsContract.Data para el acceso a la URI y a datos genéricos, y a constantes de clases internas de ContactsContract.CommonDataKinds para acceder a tipos de datos comunes. Por ejemplo, podemos leer los números de teléfono asociados a una cuenta dada:
Cursor c = getContentResolver().query(
ContactsContract.Data.CONTENT_URI,
new String[] { ContactsContract.Data._ID,
ContactsContract.CommonDataKinds.Phone.NUMBER, ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.LABEL}, ContactsContract.Data.RAW_CONTACT_ID + "=?" + " AND "
+ ContactsContract.Data.MIMETYPE + "='" +
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE + "'", new String[] { String.valueOf(rawContactId) },
null);
También podemos obtener los teléfonos de todas las cuentas de un contacto dado:
Cursor c = getContentResolver().query(
ContactsContract.Data.CONTENT_URI,
new String[] { ContactsContract.Data._ID,
ContactsContract.CommonDataKinds.Phone.NUMBER, ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.LABEL}, ContactsContract.Data.CONTACT_ID + "=?" + " AND "
+ ContactsContract.Data.MIMETYPE + "='" +
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE + "'", new String[] { String.valueOf(contactId) },
null);
3.1.3. Inserción de contactos
No podemos añadir un contacto directamente, sino que deberemos añadir una cuenta (raw contact). Si al añadir la cuenta ya existe un contacto con el identificador proporcionado, la cuenta quedará asociada a dicho contacto. En caso de no ser así, el contacto se creará de forma automática.
Podemos añadir una cuenta de la siguiente forma:
ContentValues values = new ContentValues();
values.put(ContactsContract.RawContacts.ACCOUNT_TYPE, tipo);
values.put(ContactsContract.RawContacts.ACCOUNT_NAME, nombre);
Uri rawContactUri = getContentResolver()
.insert(ContactsContract.RawContacts.CONTENT_URI, values);
Tras crear la cuenta, obtendremos una URI que nos dará acceso a ella. Podemos extraer de ella el identificador de la cuenta que se acaba de insertar:
long rawContactId = ContentUris.parseId(rawContactUri);
Tras insertar la cuenta, podremos añadir a ella distintos elementos de datos:
values.clear();
values.put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId);
values.put(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
values.put(
ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,
"Pepe García");
getContentResolver().insert(ContactsContract.Data.CONTENT_URI, values);
Sin embargo, la forma recomendada de realizar las inserciones es metiante una operación en batch que realice todas las operaciones de forma conjunta. Para ello crearemos una lista de objetos ContentProviderOperation, que definirán las operaciones que vamos a realizar en batch:
ArrayList<ContentProviderOperation> ops =
new ArrayList<ContentProviderOperation>();
ops.add(ContentProviderOperation
.newInsert(ContactsContract.RawContacts.CONTENT_URI)
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType) .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, accountName) .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,
"Pepe García") .build());
getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
Podemos ver que como la segunda operación depende del identificador generado por la primera (RAW_CONTACT_ID), este valor se lo pasamos con el método
withValueBackReference. Con ello le indicamos que como valor tome el generado por una operación anterior. Para indicar de qué operación queremos obtener el valor generado le proporcionamos como segundo parámetro el índice de dicha operación en la lista (en el caso anterior se le proporciona 0 porque nos interesa el resultado de la primera operación).
En versiones anteriores a la 2.0 (API 5) la inserción de contactos se realiza de la siguiente forma:
// Inserta contacto (previo a Android 2.0) ContentValues cv = new ContentValues();
cv.put(Contacts.People.NAME, nombre);
Uri uri = getContentResolver().insert(Contacts.People.CONTENT_URI, cv);
// Añade un teléfono
Uri phoneUri = Uri.withAppendedPath(uri, Contacts.People.Phones.CONTENT_DIRECTORY);
cv.clear();
cv.put(Contacts.People.Phones.TYPE, tipo);
cv.put(Contacts.People.Phones.NUMBER, telefono);
getContentResolver().insert(phoneUri, cv);
// Añade un e-mail
Uri emailUri = Uri.withAppendedPath(uri,
Contacts.People.ContactMethods.CONTENT_DIRECTORY);
cv.clear();
cv.put(Contacts.People.ContactMethods.KIND, Contacts.KIND_EMAIL);
cv.put(Contacts.People.ContactMethods.DATA, email);
cv.put(Contacts.People.ContactMethods.TYPE, Contacts.People.ContactMethods.TYPE_WORK);
getContentResolver().insert(emailUri, cv);
3.2. Calendario
El acceso al calendario no se ha estandarizado en Android hasta la versión 4.0 (Ice Cream Sandwich). Anteriormente se debían especificar las URI y los campos del proveedor sin ayuda de ninguna constante. Vamos a ver las dos formas de acceder, para poder mantener la compatibilidad con versiones anteriores.
Con Android 4.0 el acceso a calendarios se realizará mediante clases internas de
CalendarContract. En ellas podemos acceder a las distintas URIs que nos dan acceso a las tablas que contienen los datos de los calendarios. En versiones anteriores deberemos escribir las URIs directamente:
Versión URI
Hasta Android 2.1 "content://calendar/"
A partir de Android 2.2 "content://com.android.calendar/"
A partir de Android 4.0 CalendarContract.CONTENT_URI
Para poder acceder a los calendarios antes deberemos solicitar los permisos correspondiente enAndroidManifest.xml:
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
El proveedor de calendarios nos da acceso a multiples calendarios, almacenados en la tabla Calendars. Cada calendario contiene una serie de eventos, contenidos en la tabla Events, y estos eventos pueden contener recordatorios, que se almacenan en la tabla Reminders.
Tablas de calendarios
3.2.1. Selección del calendario
En el sistema podemos tener acceso a varios calendarios, por lo que lo primero que deberemos hacer es seleccionar el calendario con el que queramos trabajar. Para ello a partir de Android 4.0 tenemos la claseCalendarContract.Calendars, que contiene las constantes necesarias para acceder a la lista de calendario, como por ejemplo
CONTENT_URI.
En versiones anteriores deberemos especificar la URI manualmente:
Versión URI
Hasta Android 2.1 "content://calendar/calendars"
A partir de Android 2.2 "content://com.android.calendar/calendars"
A partir de Android 4.0 CalendarContract.Calendars.CONTENT_URI
En Android 4.0 podremos acceder a los calendarios con:
Cursor cursor = getContentResolver().query(Uri.parse(contentUri), new String[] { CalendarContract.Calendars._ID,
CalendarContract.Calendars.CALENDAR_DISPLAY_NAME }, null, null, null);
Sin embargo, en versiones anteriores deberemos especificar los campos manualmente también:
Cursor cursor = getContentResolver().query(Uri.parse(contentUri), new String[] { "_id", "displayname" }, null, null, null);
Advertencia
Hay que destacar que los nombres de los campos cambian en Android 4.0, por lo que el código de versiones antiguas dejará de funcionar. Por ejemplo, en lugar de"displayname"se utiliza
"calendar_displayName".
Si queremos conseguir una aplicación compatible con todas las versiones, deberemos detectar la versión que se está utilizando y en función de ésta ajustar los nombres de los campos:
Uri calendarsUri;
String calendarsId;
String calendarsName;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { calendarsUri = CalendarContract.Calendars.CONTENT_URI;
calendarsId = CalendarContract.Calendars._ID;
calendarsName = CalendarContract.Calendars.CALENDAR_DISPLAY_NAME;
} else {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) { calendarsUri =
Uri.parse("content://com.android.calendar/calendars");
} else {
calendarsUri = Uri.parse("content://calendar/calendars");
}
calendarsId = "_id";
calendarsName = "displayname";
}
De esta forma podemos obtener la lista de calendarios, y dejar que el usuario seleccione uno de ellos. Una vez seleccionado, podremos acceder a él y modificarlo, para por ejemplo añadir nuevos eventos.
3.2.2. Añadir eventos al calendario
La tabla de eventos tiene las siguientes URIs, dependiendo de la versión de Android:
Versión URI
Hasta Android 2.1 "content://calendar/events"
A partir de Android 2.2 "content://com.android.calendar/events"
A partir de Android 4.0 CalendarContract.Events.CONTENT_URI
Para crear un evento deberemos proporcionar la fecha y hora de inicio y de fin (en milisegundos), su título y descripción, la zona horaria, y el identificador del calendario al que lo queremos añadir:
Date dtStart = // Fecha y hora de inicio Date dtEnd = // Fecha y hora de fin
ContentResolver cr = getContentResolver();
ContentValues values = new ContentValues();
values.put(CalendarContract.Events.DTSTART, dtStart.getTime());
values.put(CalendarContract.Events.DTEND, dtEnd.getTime());
values.put(CalendarContract.Events.TITLE, "Reunión");
values.put(CalendarContract.Events.DESCRIPTION, "Preparación proyecto");
values.put(CalendarContract.Events.CALENDAR_ID, calID);
values.put(CalendarContract.Events.EVENT_TIMEZONE, "Europe/Madrid");
Uri uri = cr.insert(CalendarContract.Events.CONTENT_URI, values);
En caso de utilizar versiones anteriores de Android, se hará de la siguiente forma:
Date dtStart = // Fecha y hora de inicio Date dtEnd = // Fecha y hora de fin
ContentResolver cr = this.getContentResolver();
ContentValues values = new ContentValues();
values.put("dtstart", dtStart.getTime());
values.put("dtend", dtEnd.getTime());
values.put("title", "Reunión");
values.put("description", "Preparación proyecto");
values.put("calendar_id", calID);
values.put("eventTimezone", "Europe/Madrid");
Uri newEvent = cr.insert(contentUri, values);
3.2.3. Eventos recurrentes
En muchas ocasiones nos interesa agregar un evento que se repite semanalmente durante un periodo de tiempo (por ejemplo las clases de una asignatura). En este caso será conveniente añadirlo como evento recurrente, en lugar de añadirlos como eventos independientes. De esta forma si queremos eliminarlo podremos eliminar la serie entera mediante una única operación.
Para definir un evento recurrente deberemos:
• EnDTSTARTpondremos la fecha y la hora de inicio del primer evento de la serie, pero en este caso ya no utilizaremosDTEND.
• En lugar deDTEND, deberemos especificar la duración de los eventos de la serie en
DURATION. Se especificará mediante el formato RFC5545. Por ejemplo, una duración de 90 minutos se especifica con"P90M", y una duración de de dos semanas con
"P2W".
http://tools.ietf.org/html/rfc5545#section-3.8.2.5
• Por último, deberemos especificar la regla de recurrencia enRRULE. En ella
deberemos especificar la frecuencia con la que se repite y el número de repeticiones o fecha de finalización. Por ejemplo, si ponemos"FREQ=WEEKLY;COUNT=10"se repetirá 10 veces semanalmente. Si queremos fijar una fecha concreta de finalización, lo haremos con el formato"FREQ=WEEKLY;UNTIL=20130525T235959Z". Más información sobre el formato de las reglas:
http://tools.ietf.org/html/rfc5545#section-3.8.5.3
A continuación mostramos un ejemplo en el que se añade un evento recurrente para las clases de una asignatura que se imparte semanalmente hasta el fin del cuatrimestre (24 de
mayo de 2013):
Date dtStart = // Fecha y hora de inicio del primer evento Date dtEnd = // Fecha y hora de fin del primer evento // Duración de cada evento (formato RFC5545)
long duracionMillis = dtEnd.getTime() - dtStart.getTime();
int duracionMinutos = (int) (duracionMillis / (1000 * 60));
String duracion = "P" + duracionMinutos + "M";
// Reglas de la serie de eventos
String until = "20130525T235959Z"; // Formato: yyyyMMddThhmmssZ String rrule = "FREQ=WEEKLY;UNTIL=" + until;
ContentResolver cr = this.getContentResolver();
ContentValues values = new ContentValues();
values.put(CalendarContract.Events.CALENDAR_ID, calID);
values.put(CalendarContract.Events.TITLE, "Programación I");
values.put(CalendarContract.Events.DESCRIPTION, "Asignatura troncal");
values.put(CalendarContract.Events.EVENT_LOCATION, "Aula L18");
values.put(CalendarContract.Events.EVENT_TIMEZONE, "Europe/Madrid");
values.put(CalendarContract.Events.DTSTART, dtStart.getTime());
values.put(CalendarContract.Events.DURATION, duracion);
values.put(CalendarContract.Events.RRULE, rrule);
// Inserta el evento en el calendario
Uri event = cr.insert(CalendarContract.Events.CONTENT_URI, values);
Atención
Es importante no indicar el campoDTEND, ya que es incompatible conDURATION. Si ponemos los dos al mismo tiempo, obtendremos un error.
Con versiones anteriores de Android esto mismo se haría de la siguiente forma:
Date dtStart = // Fecha y hora de inicio del primer evento Date dtEnd = // Fecha y hora de fin del primer evento // Duración de cada evento (formato RFC5545)
long duracionMillis = dtEnd.getTime() - dtStart.getTime();
int duracionMinutos = (int) (duracionMillis / (1000 * 60));
String duracion = "P" + duracionMinutos + "M";
// Reglas de la serie de eventos
String until = "20130525T235959Z"; // Formato: yyyyMMddThhmmssZ String rrule = "FREQ=WEEKLY;UNTIL=" + until;
ContentResolver cr = this.getContentResolver();
ContentValues values = new ContentValues();
values.put("calendar_id", idCalendario);
values.put("title", "Programación I");
values.put("description", "Asignatura troncal");
values.put("eventLocation", "Aula L18");
values.put("eventTimezone", "Europe/Madrid");
values.put("dtstart", dtStart.getTime());
values.put("duration", duracion);
values.put("rrule", rrule);
// Inserta el evento en el calendario Uri event = cr.insert(contentUri, values);