Poner un reloj en la barra de título(ActionBar) de los settings de tu aplicación Android

Introducción

Hay ocasiones que nos interesa poner un relog en los settings, por ejemplo cuando tenemos una pantalla en la que se programan unos horários de funcionamiento. Cuando los estas configurando te interesar saber la hora actual para poder ajustar al máximo las configuraciones.

En este artículo lo vamos ha realizar aprovechanbdo el subtítulo del ActionBar, se podría hacer poniendo algo más vistoso, pero eso sería más costoso y lo que buscamos es que sea practico y facil de implementar.

Funcionamiento

Lo que vamos a hacer es poner un timer que llame a un método cada segundo.

El método compondrá la cadena de texto a poner en el subtitulo y lanzara un evento para que lo campure el handrereceiver y actualice dicho subtítulo. El método no puede modificar directamente el subtítulo por que un hilo de segundo plano no tiene acceso a los objetos gráficos del primer plano.

Como podéis ver es sencillo.

Creación del Timer

Vamos a definir un atributo para que el timer séa accesible desde cualquier método y así poder eliminarlo cuando sea necesario.

También vamos a crear un atributo para tener la referencia del ActionBar y no tener que solicitarla cada segundo, esto consumiría muchos recursos y proceso.

	private Timer TimerReloj = null; // timer que se utiliza para actualizar el reloj
	private ActionBar actionBar = null; // la utilizo para actualizar el subtitulo con el reloj
	private static int SET_HORA = 1150;

Tenemos que modificar el método “onCreate” u “onPostCreate” para añadir la inicialización del timer y la asignación del actionBar a nuestro atributo.

	actionBar = getActionBar();
        // ponemos un reloj en la actionBar
        TimerReloj  = new Timer();
        TimerReloj.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() 
            {
        	    lanzaEsperaRefrescoReloj();
            }
        }, 0, 1000); // cada 1 segundos
	    lanzaEsperaRefrescoReloj();

El método lanzaEsperaRefrescoReloj()

Este método como indicaba al principio se encarga de calcular la hora actual para componer una cadena de carácteres y lanza en handler para que se actualice el subtítulo.

	/**
	 * Se encarga de refrescar la hora en la actionBar
	 */
	public void lanzaEsperaRefrescoReloj()
	{
		try {
			// Compone la cadena con la hora actual.
			Calendar cal = Calendar.getInstance();
			cal.setTime(new Date());
			String sHora = Integer.toString(cal.get(Calendar.HOUR_OF_DAY));
			if(sHora.length() [b] 1)
				sHora = "0" + sHora;

			String sMinuto = Integer.toString(cal.get(Calendar.MINUTE));
			if(sMinuto.length() [b] 1)
				sMinuto = "0" + sMinuto;

			String sSegundo = Integer.toString(cal.get(Calendar.SECOND));
			if(sSegundo.length() [b] 1)
				sSegundo = "0" + sSegundo;

			// Lanza el handler
			Intent i = new Intent(Intent.ACTION_VIEW);            
			i.putExtra("accion", SET_HORA);
			i.putExtra("sHora", sHora + ":" + sMinuto + ":" + sSegundo);
			sendOrderedBroadcast(i, null);
		} catch(Exception e)
		{
			;
		}
	}

Creación y destrucción del receiver

El receiver es el encargado de actualizar el subtítulo con lo que se le indica. El primer paso es añadir al método “onCreate” o el método “onPostCreate” la creación del receiver antes de crear el timer. la línea a poner sería.

	// registramos el receiver para que se puedan comunicar la parte servidora y el cliente y recibir la actualización de la hora 
        registerReceiver(receiver, new IntentFilter(Intent.ACTION_VIEW));

El receiver se tiene que destruri y crear cada vez que tengamos un pause o un resume, también se tiene que destruir cuando tenemos un “onDestroy”.
En el siguiente código podemos ver estos métodos.

	/**
	 * Lo sobreescribo para matar el hilo del reloj y liberar el receiver 
	 */
	protected void onDestroy(){
		super.onDestroy();

		// eliminamos el timer
		if(TimerReloj != null)
		{
			TimerReloj.cancel();
			TimerReloj.purge();
			TimerReloj = null;
		}

		try { unregisterReceiver(receiver); } catch(Exception e) { ; }
	}

	protected void onResume() {
		super.onResume();

		// si estaba arrancado lo matamos para empezar de cero.
		if(TimerReloj != null)
		{
			TimerReloj.cancel();
			TimerReloj.purge();
			TimerReloj = null;

			// liberamos el receiver
			try { unregisterReceiver(receiver); } catch(Exception e) { ; }
		}

		// registramos el receiver para que se puedan comunicar la parte servidora y el cliente y recibir la actualización de la hora 
		registerReceiver(receiver, new IntentFilter(Intent.ACTION_VIEW));
		// ponemos un reloj en la actionBar
		TimerReloj  = new Timer();
		TimerReloj.scheduleAtFixedRate(new TimerTask() {
		    @Override
		    public void run() 
		    {
			    lanzaEsperaRefrescoReloj();
		    }
		}, 0, 1000); // cada 1 segundos
		lanzaEsperaRefrescoReloj();
	}

	/**
	 * Sobreescribimos onResume para que se la lista deje de hacer cosas
	 */
	protected void onPause() {
		super.onPause();
		if (getListAdapter() instanceof CDIPrefsHeaderAdapter)
			((CDIPrefsHeaderAdapter) getListAdapter()).pause();

		// eliminamos el timer
		if(TimerReloj != null)
		{
			TimerReloj.cancel();
			TimerReloj.purge();
			TimerReloj = null;
		}

		// liberamos el receiver
		try { unregisterReceiver(receiver); } catch(Exception e) { ; }
	}

A continuación tenemos el receiver que recoge el parámetro extra “sHora” y lo utiliza para actualizar la hora.

para diferenciar un evento de otro, se pone el extra “accion” con el código que nos interesa que en este caso es el valor de SET_HORA.

	/**
	 * Receiber que se utiliza para recibir las actualizaciones de la hora
	 */
	private BroadcastReceiver receiver = new BroadcastReceiver() {
		@Override
		public void onReceive(Context context, Intent intent) {
			try {
			    if(intent.getIntExtra("accion", 0) [b] SET_HORA)
		    		    actionBar.setSubtitle(intent.getStringExtra("sHora"));
			}catch(Exception e){
		    		;
			}
		}
	};

Botón On/Off en las preferencias de nuestra aplicación Android

Botón On/Off en las preferencias de nuestra aplicación Android
Desde la versión 14 del API se pueden utilizar SwitchPreference para poner botones On/Off en nuestra preferencias. Utilizarlos es muy sencillo, la cosa se complica cuando lo que queremos hacer es poner un botón de estos para desactivar toda una sección de nuestra preferencias y que aparezca dicho botón en el header que corresponda.

En este artículo doy por echo que tenemos conocimientos previos de programar nuestros setting en android, sino este artículo sería larguísimo.

Si no tenéis los conocimientos básicos mirar la siguiente documentación de android http://developer.android.com/guide/topics/ui/settings.html. Eclipse te lo pone muy fácil para crear una actividad del tipo settings, con todo lo que se explica en la documentación de android.

Crear los settings

Lo primero que voy a hacer es crear un proyecto para este artículo y le pondré una actividad del tipo settings.

Pulsamos en “File/New/Android Application proyect” para crear el proyecto. una vez creado creamos una actividad pulsando con el botón derecho del ratón encima del paquete que le hemos dado a nuestro proyecto, por ejemplo “com.piensayactua.settings”. Seleccionamos “New/Other/Android/Android Activity” Nos aparece varias tipos de actividad, seleccionamos “Settings Activity”.

NewActivitySettings1.pngNewActivitySettings2.png

Eclipse nos crea un montón de archivos para poder hacer los settings. No voy a entrar a describirlos, no es el objetivo de este artículo.

Poner un botón On/Off en las preferencias

Para poner el botón utilizamos el switchPreference. Cogeremos el ejemplo que nos crea eclipse y pondremos el botón en las preferencias de notificaciones.

Abrimos el archivo “pref_notification.xml” y lo modificamos cambiar el checkbox por un “switchPreference”, este tiene que estar el primero de las preferencias.

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
    <SwitchPreference 
        android:defaultValue="true"
        android:key="notifications_new_message"
        android:dependency="notificaciones"
        android:title="@string/pref_title_new_message_notifications">
        <extra android:name="esBoolean" android:value="true" />
    </SwitchPreference>
    <RingtonePreference
        android:defaultValue="content://settings/system/notification_sound"
        android:dependency="notifications_new_message"
        android:key="notifications_new_message_ringtone"
        android:ringtoneType="notification"
        android:title="@string/pref_title_ringtone" />
    <CheckBoxPreference
        android:defaultValue="true"
        android:dependency="notifications_new_message"
        android:key="notifications_new_message_vibrate"
        android:title="@string/pref_title_vibrate" />
</PreferenceScreen>

También tenemos que indicar que todas las preferencias depende de este switch para que estén deshabilitadas si esta en Off. Esto se hace con

android:dependency="notifications_new_message"

En el ejemplo que hemos cogido ya vienen puestas las dependencias.

Poner el Switch en el header

Hasta este punto sólo hemos cambiado el aspecto del checkbox y poco más. El siguiente paso es hacer que el header nos presente el switch.

Este paso es más complicado y requiere de muchos cambios y clases nuevas en nuestro código.

Android para la presentación de los headers utiliza un ArrayAdapter. Para que dicho ArrayAdapter contemple nuestro switch tenemos que sobreescribirlo y modificar el método getView.

A continuación podemos ver el código de “PrefsHeaderAdapter.java” que es autoexplicativo.

package com.piensayactua.appsettings;

import java.util.List;

import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.preference.PreferenceActivity.Header;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.Switch;
import android.widget.TextView;

import com.piensayactua.appsettings.R;

public class PrefsHeaderAdapter extends ArrayAdapter<Header> {
	// estos atributos los utilizamos para diferenciar el tipo de header.
	static final int HEADER_TYPE_CATEGORY = 0;
	static final int HEADER_TYPE_NORMAL = 1;
	static final int HEADER_TYPE_SWITCH = 2;

	private LayoutInflater mInflater; // se utiliza para cargar el row de la lista de headers

	/**
	 * Contructor de la clase
	 * @param context
	 * @param objects Listado de headers
	 */
	public PrefsHeaderAdapter(Context context, List<Header> objects) 
	{
		super(context, 0, objects);

		// preparamos el inflater para utilizarlo después
		mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
	}

	/**
	 * Método llamado cuando se quiere visualizar una porción de la lista
	 */
	public View getView(int position, View convertView, ViewGroup parent) 
	{
		Header header = getItem(position); // hacemos un cast por que sabemos con certeza que es un header
		int headerType = getHeaderType(header); // sacamos el tipo de header. Ver el método más abajo.
		View view = null;

		switch (headerType) {
		case HEADER_TYPE_CATEGORY:
			// Es del tipo categoria, inflamos el row por defecto que ya tenemos en android. por eso 
			// Utilizamos android.R.layout, en lugar de R.layout
			view = mInflater.inflate(android.R.layout.preference_category, parent, false);
			// relena los elementos del row
			((TextView) view.findViewById(android.R.id.title)).setText(header.getTitle(getContext()
					.getResources()));
			break;

		case HEADER_TYPE_SWITCH:
			// Es del tipo switch, inflamos un row que tenemos en nuestro layout que contiene el switch.
			// El siguiente trozo de código contiene el xml del layout
			// Utilizamos R.layout por que el layout es nuestro
			view = mInflater.inflate(R.layout.preference_header_switch_item, parent, false);

			// rellena los elementos del row
			((ImageView) view.findViewById(android.R.id.icon)).setImageResource(header.iconRes);
			((TextView) view.findViewById(android.R.id.title)).setText(header.getTitle(getContext()
					.getResources()));
			((TextView) view.findViewById(android.R.id.summary)).setText(header
					.getSummary(getContext().getResources()));

			// aqui tenemos lo importante. Comprobamos si el id es el nuestro, pues podríamos tener varios
			// para eso tenemos que modificar pref_headers.xml y le ponemos un id al header de las notificaciones.
			if (header.id == R.id.header_notificacion)
			{
				// necesitamos el sharepreference para poder actualizar la información en las preferencias si se pulsa el botón que estamos colocando
				SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
				// sacamos el settings, esto es util si tenemos varios header con el botón On/Off
				String extra = header.fragmentArguments.getString("settings");
				if(extra != null)
				{
					try {
						// recogemos el switch que tenemos en el row
						Switch sswitch = (Switch) view.findViewById(R.id.switchWidget);
						// le marcamos en el estado que tenemos en las preferencias
						// si no hacemos esto el botón siempre estaría en On
						sswitch.setChecked(prefs.getBoolean("notifications_new_message", false));

						// añadimos un listener para poder capturar el cambio de estado de este switch y asi cambiar las preferencias 
						// y si se da el caso actualizar el switch que tenemos cargadas en ese momento
						// En los parámetros le estamos indicando cual es su etiqueta para que sea capaz de actualizarla
						sswitch.setOnCheckedChangeListener( new SettingsCheckedChangeListener(
										getContext(),
										sswitch,
										"notifications_new_message"));
					} catch(Exception e)
					{
						;
					}
				}
			}
			break;

		case HEADER_TYPE_NORMAL:
			// Es del tipo normal, inflamos un row que tenemos en nuestro layout.
			// El siguiente trozo de código contiene el xml del layout
			// Utilizamos R.layout por que el layout es nuestro
			view = mInflater.inflate(R.layout.preference_header_item, parent, false);
			// rellena los elementos del row
			((ImageView) view.findViewById(android.R.id.icon)).setImageResource(header.iconRes);
			((TextView) view.findViewById(android.R.id.title)).setText(header.getTitle(getContext()
					.getResources()));
			((TextView) view.findViewById(android.R.id.summary)).setText(header
					.getSummary(getContext().getResources()));
			break;
		}

		return view;
	}

	/**
	 * Retorna el tipo de header, para que se puede presentar los on/off en los header que corresponda
	 * @param header
	 * @return
	 */
	public static int getHeaderType(Header header) {
		if ((header.fragment == null) && (header.intent == null)) 
		{
			return HEADER_TYPE_CATEGORY;
		} 
		else if (header.id == R.id.header_notificacion) 
		{
			return HEADER_TYPE_SWITCH;
		} 
		else 
		{
			return HEADER_TYPE_NORMAL;
		}
	}
}

El siguiente código es el del adaptador para headers normales preference_header_item.xml que lo pondremos en la carpeta layout

<?xml version="1.0" encoding="utf-8"?>
<!--
     Copyright (C) 2006 The Android Open Source Project

     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at

          http://www.apache.org/licenses/LICENSE-2.0

     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
-->

<!-- Layout of a header item in PreferenceActivity. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="?android:attr/activatedBackgroundIndicator"
    android:gravity="center_vertical"
    android:minHeight="48dp"
    android:paddingRight="?android:attr/scrollbarSize" >

    <ImageView
        android:id="@+android:id/icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginLeft="6dip"
        android:layout_marginRight="6dip" />

    <RelativeLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="6dip"
        android:layout_marginLeft="2dip"
        android:layout_marginRight="6dip"
        android:layout_marginTop="6dip"
        android:layout_weight="1" >

        <TextView
            android:id="@+android:id/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:ellipsize="marquee"
            android:fadingEdge="horizontal"
            android:singleLine="true"
            android:textAppearance="?android:attr/textAppearanceMedium" />

        <TextView
            android:id="@+android:id/summary"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignLeft="@android:id/title"
            android:layout_below="@android:id/title"
            android:ellipsize="end"
            android:maxLines="2"
            android:textAppearance="?android:attr/textAppearanceSmall" />
    </RelativeLayout>

</LinearLayout>

El siguiente código es el del adaptador para headers tipo switch preference_header_switch_item.xml que lo pondremos en la carpeta layout

<?xml version="1.0" encoding="utf-8"?>
<!--
     Copyright (C) 2006 The Android Open Source Project

     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at

          http://www.apache.org/licenses/LICENSE-2.0

     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
-->

<!-- Layout of a header item in PreferenceActivity. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="?android:attr/activatedBackgroundIndicator"
    android:gravity="center_vertical"
    android:minHeight="48dp"
    android:paddingRight="?android:attr/scrollbarSize" >

    <ImageView
        android:id="@+android:id/icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginLeft="6dip"
        android:layout_marginRight="6dip" />

    <RelativeLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="6dip"
        android:layout_marginLeft="2dip"
        android:layout_marginRight="6dip"
        android:layout_marginTop="6dip"
        android:layout_weight="1" >

        <TextView
            android:id="@+android:id/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:ellipsize="marquee"
            android:fadingEdge="horizontal"
            android:singleLine="true"
            android:textAppearance="?android:attr/textAppearanceMedium" />

        <TextView
            android:id="@+android:id/summary"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignLeft="@android:id/title"
            android:layout_below="@android:id/title"
            android:ellipsize="end"
            android:maxLines="2"
            android:textAppearance="?android:attr/textAppearanceSmall" />
    </RelativeLayout>

    <Switch
        android:id="@+id/switchWidget"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:clickable="true"
        android:focusable="false"
        android:padding="16dip" />

</LinearLayout>

Tenemos que modificar el archivo pref_headers.xml y le ponemos un id al header de las notificaciones para poder identificarlo y utilizarlo desde el ArrayAdapter.

<preference-headers xmlns:android="http://schemas.android.com/apk/res/android" >

    <!-- These settings headers are only used on tablets. -->

    <header
        android:fragment="com.piensayactua.appsettings.SettingsActivity$GeneralPreferenceFragment"
        android:title="@string/pref_header_general" />
    <header
        android:fragment="com.piensayactua.appsettings.SettingsActivity$NotificationPreferenceFragment"
        android:id="@+id/header_notificacion"
        android:title="@string/pref_header_notifications" >
    	<extra android:name="settings" android:value="notificaciones" />
    </header>
    <header
        android:fragment="com.piensayactua.appsettings.SettingsActivity$DataSyncPreferenceFragment"
        android:title="@string/pref_header_data_sync" />

</preference-headers>

En este ejemplo le he puesto el id “header_notificacion”. También podemos observar que le hemos puesto un extra para manejar después.

Gestión de eventos del switch

Hemos creado un adaptador para los header y nos falta crear un escuchador para el cambio de estado de los switch. Este escuchador nos permitirá guardar en las preferencias el nuevo valor y actualizar la vista hija si esta esta cargada.

A continuación podemos ver el código de la clase SettingsCheckedChangeListener

package com.piensayactua.appsettings;

import java.util.HashMap;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.preference.PreferenceManager;
import android.widget.CompoundButton;
import android.widget.Switch;
import android.widget.CompoundButton.OnCheckedChangeListener;

/**
 * Se encarga de actualizar la información de las preferencias de los botones tipo on/off que 
 * tenemos puesto en un header de los settings
 */
public class SettingsCheckedChangeListener implements OnCheckedChangeListener {
	private static Context contexto = null;
	private String propiedadSetting = "";
	private Switch mswitch = null;

	/*
	 * Atributos estáticos para gestionar todos los headers del tipo switch y actualizar la información cuando
	 * Se ha cambiado dentro del hijo
	 */
	private static HashMap<String, SettingsCheckedChangeListener> hmHeaders = new HashMap<String, SettingsCheckedChangeListener>();

	public SettingsCheckedChangeListener(Context contexto, Switch mswitch, 
			String propiedadSetting)
	{
		this.contexto = contexto;
		this.mswitch = mswitch;
		this.propiedadSetting = propiedadSetting;

		// nos añadimos al hashmap estático
		if(hmHeaders.containsKey(propiedadSetting))
			hmHeaders.remove(propiedadSetting);

		hmHeaders.put(propiedadSetting, this);
	}

	/**
	 * Método que es llamado cuando se pulsa sobre un botón on/off
	 * Cambia el valor del preference y actualiza el hijo si esta cargado
	 */
	@Override
	public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
		// actualizamos primero las preferencias
		SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(contexto);
		Editor editor = prefs.edit();
		editor.putBoolean(propiedadSetting, isChecked);
		editor.commit();

		// nos faltaría refrescar la vista.
		// aqui le pasamos el valor que le hemos puesto en el extra del header en pref_headers.xml
		SettingsActivity.cambiaEstadoCheck("notificaciones", propiedadSetting, isChecked);
	}

	/**
	 * Refresca el estado del boton del header que se le indica como parámetro
	 * Se utiliza para actualizar desde el hijo al padre
	 */
	public static void setChecked(String clave, boolean isChecked)
	{
		if(hmHeaders.containsKey(clave))
			hmHeaders.get(clave).mswitch.setChecked(isChecked);
	}

}

Para que el código anterior nos sirva de algo tenemos que añadir el método “cambiaEstadoCheck” a la clase “SettingsActivity”.

Añadimos el método y dos variables que nos servirán para tener controlado cual es el la preferencia hija que esta cargada en ese momento.

	private static NotificationPreferenceFragment SettingsFragmentActual = null;
	private static String NombreSettingsFragmentActual = "";

	/**
	 * Cambia el estado del check en las preferencias si esta visible
	 * @param clave
	 * @param isChecked
	 */
	public static void cambiaEstadoCheck(String clave, String claveSwitch, Boolean isChecked)
	{
		if(NombreSettingsFragmentActual.equals(clave))
		{
			((SwitchPreference)SettingsFragmentActual.getPreferenceManager().findPreference(claveSwitch)).setChecked(isChecked);
		}
	}

Tenemos que modificar en la clase “SettingsActivity” la subclase “NotificationPreferenceFragment” para que actualice nuestras dos variables declaradas en el código anterior.

	public static class NotificationPreferenceFragment extends
			PreferenceFragment {
		@Override
		public void onCreate(Bundle savedInstanceState) {
			super.onCreate(savedInstanceState);
			addPreferencesFromResource(R.xml.pref_notification);

			// Recogemos el extra que le pusimos al header para diferenciar entre
			// varios header que tengan boton
			String settings = "";
			if(getArguments() != null)
				settings = getArguments().getString("settings");
			if(settings == null)
				settings = "";
			// asignamos esta clase recien creada a la variable estática. Siempre tendremos la última clase creada.
			SettingsFragmentActual = this;  
			NombreSettingsFragmentActual = settings;

			// Bind the summaries of EditText/List/Dialog/Ringtone preferences
			// to their values. When their values change, their summaries are
			// updated to reflect the new value, per the Android Design
			// guidelines.
			bindPreferenceSummaryToValue(findPreference("notifications_new_message"));
			bindPreferenceSummaryToValue(findPreference("notifications_new_message_ringtone"));
		}
	}

Fijaros que al método también le he añadido “bindPreferenceSummaryToValue(findPreference(”notifications_new_message”));” para que notifique el cambio de sumario de este elemento.

Encajando las piezas

Aunque es un trabajo largo, estamos apunto de finalizarlo. Sólo nos falta encajar unas pocas piezas.

Tenemos que indicar que se utilice el ArrayList que hemos creado, para eso vamos a modificar la clase “SettingsActivity”.

Primero añadimos al método “onBuildHeaders” una línea en la que inicializamos la variable “mHeaders” con el target que recogemos en este método.

Esta variable nos permitirá pasarle la lista de header al arraylist y poder hacer cosas con ella.

	public void onBuildHeaders(List<Header> target) {
		if (!isSimplePreferences(this)) {
			loadHeadersFromResource(R.xml.pref_headers, target);
		}

		mHeaders = target; // recoje todos los headers que tenemos en las preferencias para poder gestionarlos en el listadapter
	}

Después añadimos al final de la clase las siguientes líneas.

	/**
	 * La siguiente variable se utiliza para poder añadir al listadapter todos los headers que existen.
	 * Se rellena en onBuildHeaders que recibimos un (List<Header> target con todos los headers.
	 */
	private static List<Header> mHeaders;

	/**
	 * Cambiamos el listadapter de los header por uno propio y asi podemos poner el botón on/off
	 * @param Adaptador de listas para cabeceras 
	 */
	public void setListAdapter(ListAdapter adapter) 
	{
		int i, count;

		// Si no hemos recogido antes todas las cabeceras, las recogemos ahora para poder utilizarlas en el listadapter
		// Cuando se guarda el estado onBuildHeaders no es llamado y por tanto mHeaders no se inicializa
		if (mHeaders == null) 
		{
			mHeaders = new ArrayList<Header>();
			count = adapter.getCount();
			for (i = 0; i < count; ++i)
				mHeaders.add((Header) adapter.getItem(i));
		}

		super.setListAdapter(new PrefsHeaderAdapter(this, mHeaders));
	}

El método “setListAdapter” nos permite cambiar el listaAdapter por el nuestro. Si no esta inicializado “mHeaders” la inicializa.

Vamos a modificar el método “OnPreferenceChangeListener” en la misma clase para que cuando cambie el sumario nos fuerce el cambio en Switch del header.

	private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() {
		@Override
		public boolean onPreferenceChange(Preference preference, Object value) {
			String stringValue = value.toString();

			if (preference instanceof ListPreference) {
				// For list preferences, look up the correct display value in
				// the preference's 'entries' list.
				ListPreference listPreference = (ListPreference) preference;
				int index = listPreference.findIndexOfValue(stringValue);

				// Set the summary to reflect the new value.
				preference
						.setSummary(index >= 0 ? listPreference.getEntries()[index]
								: null);

			} else if (preference instanceof RingtonePreference) {
				// For ringtone preferences, look up the correct display value
				// using RingtoneManager.
				if (TextUtils.isEmpty(stringValue)) {
					// Empty values correspond to 'silent' (no ringtone).
					preference.setSummary(R.string.pref_ringtone_silent);

				} else {
					Ringtone ringtone = RingtoneManager.getRingtone(
							preference.getContext(), Uri.parse(stringValue));

					if (ringtone == null) {
						// Clear the summary if there was a lookup error.
						preference.setSummary(null);
					} else {
						// Set the summary to reflect the new ringtone display
						// name.
						String name = ringtone
								.getTitle(preference.getContext());
						preference.setSummary(name);
					}
				}

			} else if (preference instanceof SwitchPreference) {
				// tenemos que actualizar al padre
				SettingsCheckedChangeListener.setChecked(preference.getKey(),(Boolean)value);

				preference.setSummary(stringValue);
			} else {
				// For all other preferences, set the summary to the value's
				// simple string representation.
				preference.setSummary(stringValue);
			}
			return true;
		}
	};

Para que el código anterior funcione tenemos que indicar que el switch tiene que actualizar su sumario, lo hacemos añadiendo

		bindPreferenceSummaryToValue(findPreference("notifications_new_message"));

en el método “setupSimplePreferencesScreen” entre los otros “bindPreferenceSumaryToValue”.

El último paso será modificar el método “bindPreferenceSummaryToValue” para que recoja el valor correctamente.

	private static void bindPreferenceSummaryToValue(Preference preference) {
		// Set the listener to watch for value changes.
		preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener);

		// Se lanza inmediatamente cuando se pretende visualizar toda la pestaña de preferencias
		// una vez por preferencia.

		/* el siguiente extra lo sacamos del xml. Buscamos un extra que se llama esBooleano.
		 * Podemos ver un ejemplo a continuación
			<CheckBoxPreference
		        android:defaultValue="true"
		        android:key="diaDomingo3"
		        android:summary="@string/pref_description_domingo"
		        android:title="@string/pref_title_domingo" >
		        <extra android:name="esBoolean" android:value="true" />
		    </CheckBoxPreference>
		    
		 * Si queremos utilizar checkbox tendremos que definir este extra para que no de error al sacar el getString
		 * y saque correctamente el getBoolean
		*/
		Object valor = "";
		SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(preference.getContext());
		Bundle extra = preference.getExtras();
		if(extra != null && extra.containsKey("esBoolean") && extra.getBoolean("esBoolean"))
		{
			valor = prefs.getBoolean(preference.getKey(), false);
		}
		else
		{
			valor = prefs.getString(preference.getKey(), "");
		}

		sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,valor);
	}

Por si no os funciona os dejo el proyecto entero para que lo podáis descargar.
attachs/borrarSettings.tar.gz

Gestión del GPS en Android

La finalidad de este arítulo es comprobar si el GPS esta activo y si no lo esta pedir la activación para poder recoger las coordenadas de localización.

Un detalle importante a tener en cuenta en esta artículo es que no buscamos precisión en la localización, dando por buena la que nos da la red. Si necesitamos precisión tendríamos que utilizar sólo la parte GPS y no la network_location.

Primero añadimos los permisos en el “AndroidManifest.xml”.

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

Utilizamos el locationmanager para comprobarlo. Si no esta activo lanzamos una alerta con un “AlertDialog”.

Si indicamos que queremos activarlo, se lanza la configuración del GPS.

  final AlertDialog alert = null;
  final LocationManager manager = (LocationManager) getSystemService( Context.LOCATION_SERVICE );

  if ( !manager.isProviderEnabled( LocationManager.GPS_PROVIDER ) ) {
      AlertNoGps();
  }

  private void AlertNoGps() {
    final AlertDialog.Builder builder = new AlertDialog.Builder(this);
    builder.setMessage("El sistema GPS esta desactivado, ¿Desea activarlo?")
           .setCancelable(false)
           .setPositiveButton("Si", new DialogInterface.OnClickListener() {
               public void onClick(@SuppressWarnings("unused") final DialogInterface dialog, @SuppressWarnings("unused") final int id) {
                   startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
               }
           })
           .setNegativeButton("No", new DialogInterface.OnClickListener() {
               public void onClick(final DialogInterface dialog, @SuppressWarnings("unused") final int id) {
                    dialog.cancel();
               }
           });
    alert = builder.create();
    alert.show();
  }

Se tiene que declarar la alerta para toda la actividad para que pueda descartarla en OnDestroy para evitar una pérdida de memoria

onDestroy()
{
  if(alert != null) 
  {
      alert.dismiss ();
  }
}[code]

[b]Recepción de nuevas localizaciones[/b]

Si queremos algo más elaborado que nos permita registrarnos para recibir los cambios de posición. Lo que se suele hacer es tener una variable con la locación y poner un listener al locationmanager para que la actualice cada vez que sea necesario.

Primero definimos un atributo de nuestra clase con un objeto del tipo [i]MiLocationListener[/i] que veremos más adelante.

[code]private LocationListener locListener =  null;

En el método onCreate inicializamos el listener con la clase MiLocationListener.

	// inicializamos el GPS
	if(locListener == null)
		locListener = new MiLocationListener(this);

Si queremos sacar la localización, desde cualquier parte de nuestro código podemos escribir lo siguiente.

	// Para sacar la localización
	Location loc = locListener.getLocation();

A continuación podemos ver la clase MiLocationListener. Esta clase es autoexplicativa.

/**
 * Esta clase se utiliza para recoger los eventos de localización
 * @author seni
 *
 */
public class MiLocationListener implements LocationListener 
{
	private String TAG = "MiLocationListener";
	private Activity actividad;
	private LocationManager locManager = null;
	private Location locGPS = null; // Es la localización del GPS

	public MiLocationListener(Activity actividad)
	{
		this.actividad = actividad;
		inicializaGPS();
	}

	@Override
	public void onLocationChanged(Location location) 
	{
		locGPS = location;
	}
	@Override
	public void onProviderDisabled(String provider) 
	{
	}
	@Override
	public void onProviderEnabled(String provider) 
	{
	}
	@Override
	public void onStatusChanged(String provider, int status, Bundle extras) 
	{
	}

	/**
	 * Des registra el location listener 
	 */
    public void unregisterLocationListener() {
    	locManager.removeUpdates( this );
    }

    /**
     * Registra el location listener
     */
    public void registerLocationListener() {
    	locManager.requestLocationUpdates( LocationManager.GPS_PROVIDER, 1000, 20, this );
    }

	/**
	 * Inicializa el GPS para que tengamos las coordenadas GPS actualizadas
	 * Se puede utilizar cualquiera de las siguientes opciones
	 * 
	 * NETWORK_PROVIDER: Determina la posición del usuario basándose en la celda telefónica en la que se 
	 * encuentre y, si dispone de conexión wifi, de la combinación del punto de acceso al que se encuentre 
	 * conectado.
	 * 
	 * GPS_PROVIDER: Determina la posición el usuario usando satélites GPS. Dependiendo de las condiciones 
	 * en las que se encuentre el usuario puede tardar más tiempo en devolver una posición buena, por 
	 * ejemplo, si se encuentra en espacios interiores.
	 * 
	 * PASSIVE_PROVIDER: Determina la posición del usuario basándose en la recogida de posiciones de otras 
	 * aplicaciones. Por ejemplo, si Google Latitude necesita recoger la posición GPS para enviarla a un 
	 * servicio web, este proveedor se aprovecha de esa acción para obtener la coordenada. Si necesitamos 
	 * saber la posición de un usuario en un determinado momento, este es el proveedor menos adecuado. 
	 * Sin embargo, este es el proveedor que menos batería gasta.
	 */
	private void inicializaGPS() {
		try {
			//Obtenemos una referencia al LocationManager
			locManager = (LocationManager)actividad.getSystemService(Context.LOCATION_SERVICE);
			registerLocationListener();
			//Obtenemos la última posición conocida
			getLastKnownLocation(locManager);
        }catch(Exception e){
    		CDILog.e(TAG,"inizializaGPS Error: " + e.getMessage(),e);
        }
	}

	/**
	 * Cogemos la última posición conocida
	 * @param manager
	 */
    private void getLastKnownLocation( final LocationManager manager ) 
    {
		locGPS = manager.getLastKnownLocation( LocationManager.GPS_PROVIDER );

        if ( locGPS == null ) {
        	locGPS = manager.getLastKnownLocation( LocationManager.NETWORK_PROVIDER );
        }

        if ( locGPS == null ) {
        	locGPS = manager.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER);
        }
    }

}

Se podría utilizar onProviderDisabled y onProviderEnabled para poner la variable loc a null.

Si queremos que se lance un evento cada vez que tengamos cambios de localización, podríamos crear nuestro propio listener dentro de MiLocationListener que llamara a un método onLocacizaciónCambiada(Location location).

Por ejemplo.

private Vector escuchadores = new Vector();
...
public void addLocationListener(MiLocationListener.LocationListener escuchador)
{
    escuchadores.addElement(escuchador);
}

public void removeLocationListener(MiLocationListener.LocationListener escuchador)
{
    escuchadores.removeElement(escuchador);
}

public interface LocationListener {
    public void onLocacizacionCambiada(Location location);
}

modificamos onLocationChanged por

	public void onLocationChanged(Location location) 
	{
		locGPS = location;
		for(int i = 0; limite = escuchadores.length();i < limite; i++)
		    escuchadores.elementAt(i).onLocacizacionCambiada(locGPS);
	}