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