martes, 24 de septiembre de 2013

Método de cifrado inventado (en Android)

En este caso voy a publicar una pequeña aplicación muy parecida a la de la publicación anterior, al menos en apariencia. En este caso, la principal diferencia con respecto a la anterior radica en el algoritmo de cifrado que se aplica. Se trata de un método creado por mi mismo mezclando sustitución y transposición en el mismo algoritmo.
Este algoritmo consiste en dos pasos:

  •  Primero se sustituye cada carácter del mensaje por el correspondiente a su posición en el alfabeto con un desplazamiento 9 si se trata de una letra y 5 si es un dígito. En caso de ser otro carácter no se modificará.
  • En segundo lugar, divide el mensaje resultante de aplicar el proceso anterior en partes de 3 caracteres y se invierte el orden de los caracteres en cada grupo. Después se vuelven a agrupar y así se obtiene el mensaje final cifrado.
La implementación del algoritmo de cifrado/descifrado se hace en una clase independiente con lo que es muy fácil cambiar el algoritmo utilizado sin tener que modificar la interfaz de usuario, o haciendo cambios mínimos.
A continuación podéis ver el código y descargar un fichero para instalarla en un dispositivo Android.


package com.example.firstapp;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;

public class MainActivity extends Activity {
private final String abc = new String("ABCDEFGHIJKLMNÑOPQRSTUVWXYZ");


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
     
       
        Button btnConvertir = (Button)findViewById(R.id.btnConvertir);
        Button btnLimpiar = (Button)findViewById(R.id.btnLimpiar);
     
        final EditText etPlano = (EditText)findViewById(R.id.etPlano);
        final EditText etCifrado = (EditText)findViewById(R.id.etCifrado);
     
        btnLimpiar.setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View v) {
etPlano.setText("");
etCifrado.setText("");
}
});
     
        btnConvertir.setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View v) {
String textoPlano = etPlano.getText().toString();
String textoCifrado = etCifrado.getText().toString();
int txtLen = etPlano.length();
int cifrLen = etCifrado.length();

ECipherDecipher ecd = new ECipherDecipher();

if ((txtLen != 0) || (cifrLen !=0))
{
if (txtLen > 0)
{
String contenidoCifrado = ecd.cifrar(textoPlano);
etCifrado.setText(contenidoCifrado);
}
else
{
String contenidoPlano = ecd.descifrar(textoCifrado);
etPlano.setText(contenidoPlano);
}
}
}
});
    }

}

El código correspondiente a la implementación del algorirtmo se muestra en el código siguiente.

package com.example.firstapp;

public class ECipherDecipher {
private final String abc = new String("ABCDEFGHIJKLMNÑOPQRSTUVWXYZ");
private final String abc2 = new String("0123456789");

//Definición del desplazamiento en la parte de cifrado por sustitución (César)
private int DESPLAZAMIENTO_CARACTERES = 9;
private int DESPLAZAMIENTO_DIGITOS = 5;

public ECipherDecipher()
{

}

public String cifrar(String cadena)
{
char caracterCifrado;
int posCaracterPlano;
int posCaracterCifrado;
int endPosSubCadena;
StringBuilder sbTextoIntermedio = new StringBuilder();
StringBuilder sbTextoCifrado = new StringBuilder();
StringBuilder subCadena3;

//Convertimos a mayúsculas
String texto = cadena.toUpperCase();

//Para cada caracter del texto
for (int i=0; i<texto.length(); i++)
{
char caracter = texto.charAt(i);
//Si es alfabético
if (Character.isLetter(caracter))
{
//Desplazamiento de cada carácter 9 posiciones
posCaracterPlano = abc.indexOf(caracter);
posCaracterCifrado = (posCaracterPlano + DESPLAZAMIENTO_CARACTERES) % abc.length();
caracterCifrado = abc.charAt(posCaracterCifrado);
sbTextoIntermedio.append(caracterCifrado);
}
else if (Character.isDigit(caracter))
{
//Si es numérico
//Desplazamiento de cada dígito 5 posiciones
posCaracterPlano = abc2.indexOf(caracter);
posCaracterCifrado = (posCaracterPlano + DESPLAZAMIENTO_DIGITOS) % abc2.length();
caracterCifrado = abc2.charAt(posCaracterCifrado);
sbTextoIntermedio.append(caracterCifrado);

}
else
{
//En otro caso no modificar el carácter
sbTextoIntermedio.append(caracter);
}
}

//Con el texto convertido anteriormente cogemos subcadenas de longitud 3
//y los invertimos.
while (sbTextoIntermedio.length()>0)
{
endPosSubCadena = 3;

if (sbTextoIntermedio.length()< 3)
{
endPosSubCadena = sbTextoIntermedio.length();
}
subCadena3 = new StringBuilder(sbTextoIntermedio.substring(0, endPosSubCadena));
sbTextoIntermedio = sbTextoIntermedio.delete(0, endPosSubCadena);
subCadena3 = subCadena3.reverse();
sbTextoCifrado.append(subCadena3);
}

return sbTextoCifrado.toString();
}

public String descifrar(String texto)
{
char caracterDescifrado;
int posCaracterCifrado;
int posCaracterDescifrado;
int endPosSubCadena;
StringBuilder sbTextoIntermedio = new StringBuilder();
StringBuilder sbTextoCifrado = new StringBuilder();
StringBuilder sbTextoPlano = new StringBuilder();
StringBuilder subCadena3;

sbTextoCifrado = new StringBuilder(texto.toUpperCase());

//Con el texto cogemos subcadenas de longitud 3 y los invertimos.
while (sbTextoCifrado.length()>0)
{
//Posición de fin de la subcadena
endPosSubCadena = 3;

if (sbTextoCifrado.length()< 3)
{
endPosSubCadena = sbTextoCifrado.length();
}
subCadena3 = new StringBuilder(sbTextoCifrado.substring(0, endPosSubCadena));
sbTextoCifrado = sbTextoCifrado.delete(0, endPosSubCadena);
subCadena3 = subCadena3.reverse();
sbTextoIntermedio.append(subCadena3);
}

//Para cada caracter del texto
texto = sbTextoIntermedio.toString();

for (int i=0; i<texto.length(); i++)
{

char caracter = texto.charAt(i);
//Si es alfabético
if (Character.isLetter(caracter))
{
//Desplazamiento de cada carácter -9 posiciones
posCaracterCifrado = abc.indexOf(caracter);
posCaracterDescifrado = (posCaracterCifrado - DESPLAZAMIENTO_CARACTERES +
(abc.length())) % abc.length();
caracterDescifrado = abc.charAt(posCaracterDescifrado);
sbTextoPlano.append(caracterDescifrado);
}
else if (Character.isDigit(caracter))
{
//Si es numérico
//Desplazamiento de cada dígito -5 posiciones
posCaracterCifrado = abc2.indexOf(caracter);
posCaracterDescifrado = (posCaracterCifrado - DESPLAZAMIENTO_DIGITOS +
(abc2.length())) % abc2.length();
caracterDescifrado = abc2.charAt(posCaracterDescifrado);
sbTextoPlano.append(caracterDescifrado);
}
else
{
//En otro caso no modificar el carácter
sbTextoPlano.append(caracter);
}
}

return sbTextoPlano.toString();
}
}

La apariencia de la aplicación es casi igual a la de la aplicación descrita en la entrada anterior del blog.


El fichero de instalación se puede descargar aquí.

Cifrado César (aplicación en Android)

En esta entrada voy a mostrar una pequeña aplicación que permite cifrar mensajes utilizando el método César. Del mismo modo, permite descifrar los mensajes cifrados utilizando el proceso inverso. En este caso la interfaz de usuario es muy sencilla, la parte un poco más compleja es el código que realiza las operaciones de cifrado/descifrado.
A continuación os muestro el código y alguna imagen de la aplicación en funcionamiento. En primer lugar se muestra el código correspondiente a la actividad principal.

package com.example.firstapp;

import android.app.Activity;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;

public class MainActivity extends Activity {
private final String abc = new String("ABCDEFGHIJKLMNÑOPQRSTUVWXYZ");
private int desplazamiento = 0;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
     
        Spinner spin1 = (Spinner)findViewById(R.id.spin1);
     
        ArrayAdapter<CharSequence> adaptador = ArrayAdapter.createFromResource(this, R.array.desplazamientos,
        android.R.layout.simple_spinner_item);
        adaptador.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        spin1.setAdapter(adaptador);
     
     
        Button btnConvertir = (Button)findViewById(R.id.btnConvertir);
        Button btnLimpiar = (Button)findViewById(R.id.btnLimpiar);
     
        final EditText etPlano = (EditText)findViewById(R.id.etPlano);
        final EditText etCifrado = (EditText)findViewById(R.id.etCifrado);
     
        btnLimpiar.setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View v) {
etPlano.setText("");
etCifrado.setText("");
}
});
     
        etCifrado.setOnKeyListener(new EditText.OnKeyListener() {
@Override
public boolean onKey(View arg0, int arg1, KeyEvent arg2) {
if (etPlano.getText().toString().compareTo("") != 0)
{
etPlano.setText("");
}
return false;
}
        });
     
        spin1.setOnItemSelectedListener(new OnItemSelectedListener() {

@Override
public void onItemSelected(AdapterView<?> parent, View view,
int pos, long id) {
desplazamiento = pos;
}

@Override
public void onNothingSelected(AdapterView<?> arg0) {
desplazamiento = 0;
}
       
        });
     
        btnConvertir.setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View v) {
String textoPlano = etPlano.getText().toString();
String textoCifrado = etCifrado.getText().toString();
int txtLen = etPlano.length();
int cifrLen = etCifrado.length();
CifradorDescifradorSustitucion cifDecif = new CifradorDescifradorSustitucion(desplazamiento);

if ((txtLen != 0) || (cifrLen !=0))
{
if (txtLen > 0)
{
String contenidoCifrado = cifDecif.cifrar(textoPlano);
etCifrado.setText(contenidoCifrado);
}
else
{
String contenidoPlano = cifDecif.descifrar(textoCifrado);
etPlano.setText(contenidoPlano);
}
}
}
});
    }

}

En segundo lugar se muestra el código de la clase dónde se implementa el cifrado y descifrado mediante el método César.

package com.example.firstapp;

public class CifradorDescifradorSustitucion {
private final String abc = new String("ABCDEFGHIJKLMNÑOPQRSTUVWXYZ0123456789");

private int desplazamiento;
private String textoPlano;
private String textoCifrado;

public CifradorDescifradorSustitucion() {
desplazamiento = 0;
}

public CifradorDescifradorSustitucion(int desplazamiento) {
this.desplazamiento = desplazamiento;
}

public String descifrar(String textoCifrado) {
char caracterCifrado = ' ';
char caracterPlano = ' ';

textoCifrado = textoCifrado.toUpperCase();
StringBuilder textoPlano = new StringBuilder(textoCifrado.length());

for (int i = 0; i < textoCifrado.length(); i++) {
caracterCifrado = textoCifrado.charAt(i);
if (abc.contains(String.valueOf(caracterCifrado))) {
int posCaracterCifrado = abc.indexOf(caracterCifrado);
int posCaracterPlano = ((posCaracterCifrado - desplazamiento) + (abc
.length() + 1)) % (abc.length());
caracterPlano = abc.charAt(posCaracterPlano);
} else {
caracterPlano = caracterCifrado;
}
textoPlano.append(caracterPlano);
}

return textoPlano.toString();
}

public String cifrar(String textoPlano) {
char caracterCifrado = ' ';
char caracterPlano = ' ';

textoPlano = textoPlano.toUpperCase();
StringBuilder textoCifrado = new StringBuilder(textoPlano.length());

for (int i = 0; i < textoPlano.length(); i++) {
caracterPlano = textoPlano.charAt(i);
if (abc.contains(String.valueOf(caracterPlano))) {
int posCaracterPlano = abc.indexOf(caracterPlano);
int posCaracterCifrado = (posCaracterPlano + desplazamiento)
% (abc.length());
caracterCifrado = abc.charAt(posCaracterCifrado);
} else {
caracterCifrado = caracterPlano;
}
textoCifrado.append(caracterCifrado);
}
return textoCifrado.toString();
}

public int getDesplazamiento() {
return desplazamiento;
}

public void setDesplazamiento(int desplazamiento) {
this.desplazamiento = desplazamiento;
}

public String getTextoPlano() {
return textoPlano;
}

public void setTextoPlano(String textoPlano) {
this.textoPlano = textoPlano;
}

public String getTextoCifrado() {
return textoCifrado;
}

public void setTextoCifrado(String textoCifrado) {
this.textoCifrado = textoCifrado;
}

}

Por último se puede ver el código xml correspondiente al layout (muy sencillo como ya comenté anteriormente).

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <Spinner
        android:id="@+id/spin1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignRight="@+id/textView3"
        android:layout_below="@+id/textView1" />

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:text="@string/titulo_spin1" />

    <EditText
        android:id="@+id/etPlano"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_below="@+id/textView2"
        android:ems="10"
        android:hint="Texto plano aquí..."
        android:lines="4" >

        <requestFocus />
    </EditText>

    <TextView
        android:id="@+id/textView3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/etCifrado"
        android:layout_below="@+id/etPlano"
        android:layout_marginTop="45dp"
        android:text="@string/texto_cifrado" />

    <EditText
        android:id="@+id/etCifrado"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/etPlano"
        android:layout_below="@+id/textView3"
        android:ems="10"
        android:hint="Texto cifrado aquí..."
        android:lines="4" />

    <Button
        android:id="@+id/btnConvertir"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignRight="@+id/etCifrado"
        android:layout_below="@+id/etCifrado"
        android:layout_marginTop="17dp"
        android:layout_toRightOf="@+id/textView3"
        android:text="@string/convertir" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/spin1"
        android:layout_below="@+id/spin1"
        android:text="@string/texto_plano" />

    <Button
        android:id="@+id/btnLimpiar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/btnConvertir"
        android:layout_alignBottom="@+id/btnConvertir"
        android:layout_alignLeft="@+id/etCifrado"
        android:text="Limpiar" />

</RelativeLayout>

El archivo de instalación podéis descargarlo aquí.

sábado, 22 de junio de 2013

BroadcastReceiver en Android

He estado trabajando en una aplicación para tener los móviles siempre localizados y poder obtener su localización en caso de pérdida o robo. Para conseguir el objetivo, uno de los medios que he utilizado ha sido el BroadcastReceiver que proporciona la plataforma Android.
Los BroadcastReceiver nos permiten ejecutar el código que consideremos oportuno cuando se produce determinada acción en el sistema (en el móvil en cuestión). Se pueden utilizar para monitorizar una gran cantidad de acciones, aunque yo en este ejemplo sólo lo utilizo para monitorizar la recepción de un mensaje personalizado (un Intent definido por mí).
A continuación os dejo unas muestras de código que permiten ver el uso de este componente de Android.
En primer lugar tenemos el código correspondiente al BroadcastReceiver, que estará incluido en una clase que hereda de BroadcastReceiver.

public class Receptor extends BroadcastReceiver {

    @Override
    public void onReceive(Context ctx, Intent intent) {
        Bundle datos = intent.getExtras();
        String mensaje = datos.getString("mensaje");
       
        Activity activity = MainActivity.getActivity();
        Toast.makeText(ctx, "Mensaje recibido: " + mensaje, Toast.LENGTH_LONG).show();
        TextView tvTexto = (TextView)activity.findViewById(R.id.tvTexto);
        tvTexto.setText(tvTexto.getText() + "\n" + mensaje);
    }

}

A continuación, podemos ver el código necesario para enviar un mensaje que active el receptor anterior.

btnSend.setOnClickListener(new View.OnClickListener() {
    @Override
     public void onClick(View v) {
         Intent intent =
                 new Intent("not.ebacelo.testbroadcastreceiver01.retransmision");
         Bundle datos = new Bundle();
         datos.putString("mensaje",
                   "Este mensaje debe recibirlo el BroadcastReceiver");
         intent.putExtras(datos);
         sendBroadcast(intent);
 });

Finalmente, necesitamos registrar nuestra clase para detectar los eventos en el AndroidManifest.

<receiver android:name=".Receptor">
    <intent-filter>
        <action android:name="not.ebacelo.testbroadcastreceiver01.retransmision" />
    </intent-filter>
</receiver>

Como se puede ver, este es un componente fácil de utilizar y que, al mismo tiempo, ofrece un potencial enorme en nuestras aplicaciones.

jueves, 23 de mayo de 2013

Configurando CodeIgniter

Estoy aprendiendo a utilizar CodeIgniter para programar en PHP utilizando el patrón MVC (Modelo-Vista-Controlador). Este framework resulta muy cómodo y fácil de aprender a utilizarlo. Hasta el momento lo que voy a mostrar en este post es lo que más me costó resolver, porque tienen una documentación muy buena, clara, sencilla y con ejemplos de uso. Sin embargo, por defecto, en las URLs de los proyectos realizados con CodeIgniter aparece un index.php un poco indeseado, aunque se puede eliminar con el módulo mod_rewrite de Apache. Desafortunadamente, yo no conozco mucho sobre la administración de Apache (tendré que ponerme con eso también, en algún momento) y estuve probando las soluciones que encontraba por la web, aunque ninguna me funcionó salvo la esta. Por lo tanto, he decidido hacer una referencia a la misma para acordarme y no tener que recordar la página en el futuro.
El contenido del artículo que se puede ver en el enlace lo reproduzco a continuación:

1.- Creamos un archivo .htaccess con el siguiente contenido y lo guardamos en la raíz de nuestra aplicación

RewriteEngine On
RewriteBase /
#Removes access to the system folder by users.
#Additionally this will allow you to create a System.php controller,
#previously this would not have been possible.
#'system' can be replaced if you have renamed your system folder.
RewriteCond %{REQUEST_URI} ^system.*
RewriteRule ^(.*)$ /index.php/$1 [L]
#Checks to see if the user is attempting to access a valid file,
#such as an image or css document, if this isn't true it sends the
#request to index.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php/$1 [L]
# If we don't have mod_rewrite installed, all 404's
# can be sent to index.php, and everything works as normal.
# Submitted by: ElliotHaughin
ErrorDocument 404 /index.php

2.- Cambiar en la segunda línea del archivo anterior / por la carpeta que aloja nuestro proyecto si este no está en la raíz del servidor web.
3.- En el archivo system/application/config/config.php  nos aseguramos de que la línea correspondiente a index_page aparezca de este modo

 $config['index_page'] = '';

4.- Yo como estoy trabajando con XAMPP sólo necesité reiniciar el servidor Apache. En otro caso puedes ver todos los detalles en el enlace en el que se basa esta información.

Estos cambios hay que hacerlos cada vez que creamos una nueva aplicación con el framework CodeIgniter (si queremos que las URL aparezcan lo más sencillas posible) por eso los documento aquí, para que no se me olviden, y también para ayudar a cualquiera que tenga el mismo problema.


martes, 14 de mayo de 2013

Cronómetro en Android

Una de las aplicaciones que podemos encontrar con facilidad en los móviles Android es el cronómetro. Además existen una gran variedad de ellos para descargar e instalar. Sin embargo, yo decidí hacer mi propia versión del mismo. Hace algún tiempo que hice una primera versión empleando la clase Handler para gestionar el hilo secundario. En esta ocasión, decidí utilizar la clase AsyncTask para dicha función, y además aproveché para hacer algunas mejoras.
A continuación podéis ver el código de mi aplicación, así como algunas imágenes de la misma en funcionamiento.

También podéis descargar el fichero para instalar a través de este enlace.

//// Actividad principal


package not.ebacelo.android.crono02;

import android.app.Activity;
import android.content.pm.ActivityInfo;
import android.graphics.Color;
import android.opengl.Visibility;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.*;

public class Crono02Activity extends Activity {
LinearLayout layTiempos, layTableHeader;
TextView tvCount, tvLaps;
Button btnStart, btnLap, btnStop, btnResume;
TareaCronometro tareaCronometro;

@Override
protected void onPause() {
// TODO Auto-generated method stub
super.onPause();

}

/**
 * Método que se ejecuta al crear la Actividad (se
 * debe consultar ciclo de vida de Actividad en caso
 * de dudas sobre este tema)
 */
@Override
public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);
setContentView(R.layout.main);

//Definimos la orientación de la pantalla para
//que no se modifique. De otro modo debemos guardar
//el estado de la aplicación y restaurarla cada
//vez que se cambie la orientación.
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);

//Cargamos las referencias a los controles de la UI
initialize();

//Deshabilitamos los botones Stop y Lap hasta que se
//pulse el botón Start.
btnStop.setEnabled(false);
btnLap.setEnabled(false);
//Ocultamos los títulos de la tabla de tiempos por vuelta
layTableHeader.setVisibility(View.INVISIBLE);

//Creamos una nueva instancia de nuestra tarea AsyncTask
tareaCronometro = new TareaCronometro();

//Qué hacemos cuando se pulsa el botón Start
btnStart.setOnClickListener(new View.OnClickListener() {

public void onClick(View v) {
Button btn = (Button) v;

if (btn.getText().toString().compareTo("Start") == 0) {
//Habilitamos los botones Stop y Lap
btnStop.setEnabled(true);
btnLap.setEnabled(true);

if (tareaCronometro.getStatus() != AsyncTask.Status.RUNNING) {
//Arrancamos nuestra tarea asíncrona
tareaCronometro.execute();
}
//Iniciamos el cronómetro
tareaCronometro.iniciar();

} else {
if (btn.getText().toString().compareTo("Resume") == 0) {
//Continuamos el cronómetro cuando ha sido parado
tareaCronometro.continuar();
}
}
}
});

btnStop.setOnClickListener(new View.OnClickListener() {

public void onClick(View v) {
Button btn = (Button) v;

if (btn.getText().toString().compareTo("Stop") == 0) {
//Paramos el cronómetro (la tarea continua en
//ejecución).
tareaCronometro.parar();
} else {
if (btn.getText().toString().compareTo("Restore") == 0) {
//Reiniciamos el cronómetro
tareaCronometro.reiniciar();
//Deshabilitamos los botones Stop y Lap
btnStop.setEnabled(false);
btnLap.setEnabled(false);
//Ocultamos cabecera tabla tiempos
layTableHeader.setVisibility(View.INVISIBLE);
}
}
}
});

//Qué hacemos cuando se pulsa el botón Lap
btnLap.setOnClickListener(new View.OnClickListener() {

public void onClick(View v) {
//Registramos el tiempo actual
tareaCronometro.marcarTiempo();
layTableHeader.setVisibility(View.VISIBLE);
}
});

}

//Inicializamos las referencias a los controles de la
//interfaz de usuario de la Actividad.
private void initialize() {
tvCount = (TextView) findViewById(R.id.tvCount);
btnStart = (Button) findViewById(R.id.btnStart);
btnStop = (Button) findViewById(R.id.btnStop);
btnLap = (Button) findViewById(R.id.btnLap);
btnLap = (Button) findViewById(R.id.btnLap);
layTiempos = (LinearLayout) findViewById(R.id.layTiempos);
layTableHeader = (LinearLayout)findViewById(R.id.layTableHeader);

}

/**
 *
 * @param startTime
 * @param timeNow
 * @param timeStopped
 * @return String
 *
 * Este método nos devuelve en una cadena de texto con el
 * formato adecuado el tiempo transcurrido entre los
 * momentos indicados por los 2 primeros parámetros de
 * entrada. El tercer parámetro indica el tiempo a
 * descontar si el cronómetro estuvo parado durante el
 * intervalo definido por los dos parámetros anteriores.
 */
private String obtenerTiempoTranscurrido(long startTime, long timeNow,
long timeStopped) {
long diferencia, stopTime;
int hours, minutes, seconds, millis;
String strMillis;

diferencia = timeNow - startTime - timeStopped;
hours = (int) diferencia / 3600000;
diferencia = diferencia % 3600000;
minutes = (int) diferencia / 60000;
diferencia = diferencia % 60000;
seconds = (int) diferencia / 1000;
diferencia = diferencia % 1000;
millis = (int) diferencia;
try {
strMillis = String.format("%03d", millis).substring(0, 2);
millis = Integer.parseInt(strMillis);
} catch (IndexOutOfBoundsException e) {
Log.d("DEPURANDO", "IndexOutOfBoundsException catched");
}

String res = String.format("%d:%02d:%02d.%02d", hours, minutes,
seconds, millis);
return res;
}

/**
 *
 * @author ebacelo
 *
 * Clase interna derivada de AsyncTask que nos permite realizar una
 * tarea en un hilo secundario.
 */
class TareaCronometro extends AsyncTask<Void, String, Void> {
private boolean salir = false;
private boolean parado = true;
private boolean reiniciado = false;
private boolean lap = false;
private boolean iniciar = false;
private long timeStart = 0;
private long resumeTime = 0;
private long stopTime = 0;
private long timeStopped = 0;
private long timeNow = 0;
private long timeLastLap = 0;
private long timeLap = 0;
private int lap_num = 1;

public void iniciar() {
// e0
if (parado != false) {
parado = false;
timeStart = System.currentTimeMillis();
timeLastLap = timeStart;
timeStopped = 0;
}
}

public void parar() {
parado = true;
stopTime = System.currentTimeMillis();
// Ocultamos el botón Lap
// Cambiamos el texto de Stop a Restore, y el de Start a Resume
btnLap.setVisibility(View.INVISIBLE);
btnStart.setText("Resume");
btnStop.setText("Restore");
}

public void continuar() {
btnStart.setText("Start");
btnStop.setText("Stop");
btnLap.setVisibility(View.VISIBLE);
parado = false;
resumeTime = System.currentTimeMillis();
timeStopped += resumeTime - stopTime;
}

public void reiniciar() {
btnStart.setText("Start");
btnStop.setText("Stop");
btnLap.setVisibility(View.VISIBLE);
////
stopTime = System.currentTimeMillis();
timeStopped = 0;
parado = true;
iniciar = true;
reiniciado = true;

//Limpiamos los tiempos de vuelta
layTiempos.removeAllViews();
lap_num = 1;
}

@Override
protected void onCancelled() {
super.onCancelled();

salir = true;
finish();
}

//Tareas a realizar después de comenzar la tarea en el
//hilo secundario.
@Override
protected void onPostExecute(Void result) {
super.onPostExecute(result);
}

//Tareas a realizar antes de comenzar la tarea en el
//hilo secundario.
@Override
protected void onPreExecute() {
super.onPreExecute();
parado = true;
iniciar = false;
}

//Este método se ejecutará en el hilo de la interfaz de usuario
//cada vez que desde el método doInBackground() se invoque el
//método publishProgress(). De este modo podemos transmitir
//información entre los dos hilos de ejecución de forma
//sencilla.
@Override
protected void onProgressUpdate(String... values) {

super.onProgressUpdate(values);
// Si no esta parado actualizamos el tiempo en la pantalla
// (obtenerTiempoTranscurrido)

//Mostramos el tiempo transcurrido en la pantalla del
//cronómetro.
tvCount.setText(values[0]);

if (lap) {
// Añadimos en la lista de vueltas el tiempo que va a ser
// publicado
TextView tvTiempoVuelta = new TextView(layTiempos.getContext());
tvTiempoVuelta.setBackgroundColor(Color.argb(45, 55, 120, 55));

if (lap_num % 2 == 0)
tvTiempoVuelta.setTextColor(Color.argb(100, 33, 230, 230));
else
tvTiempoVuelta.setTextColor(Color.argb(100, 230, 230, 33));

tvTiempoVuelta.setTextSize(20);
tvTiempoVuelta.setPadding(0, 1, 0, 1);

//Calcular el tiempo de la vuelta con los datos recibidos
timeNow = Long.parseLong(values[1]);
timeLap = timeNow - timeLastLap;
tvTiempoVuelta.setText(fijarLongCadena(String.valueOf(lap_num),2) + " - " +
values[0] + "   ---   " + String.valueOf(obtenerTiempoTranscurrido(0, timeLap, 0)));

//Añadimos el nuevo tiempo de vuelta al layout del interfaz
//de usuario para visualizarlo.
layTiempos.addView(tvTiempoVuelta, 0);
timeLastLap = timeNow;
lap_num++;
lap = false;
}
}


//El contenido de este método son las tareas que se
//realizarán en el hilo secundario (sólo estas, todos
//los demás métodos se ejecutan en el hilo de la
//interfaz de usuario).
@Override
protected Void doInBackground(Void... arg0) {

while (!salir) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!parado) {
timeNow = System.currentTimeMillis();
String values[] = new String[2];
if (reiniciado) {
timeStart = timeNow;
reiniciado = false;
}
values[0] = obtenerTiempoTranscurrido(timeStart,
timeNow, timeStopped);
values[1] = String.valueOf(timeNow);

publishProgress(values);
}

if (iniciar) {
timeStopped = 0;
timeLastLap = 0;
timeNow = timeStart;
String values[] = new String[2];
values[0] = obtenerTiempoTranscurrido(timeStart,
timeNow, timeStopped);
values[1] = String.valueOf(timeNow);
publishProgress(values);
iniciar = false;
parado = true;
}

}
return null;
}

//Permite indicar al hilo secundario que hay que marcar un tiempo
public void marcarTiempo() {
lap = true;

return;
}

public String fijarLongCadena(String cadena, int longitud) {
String resultado = "";
String relleno = "";

if (cadena.length() < longitud) {
for (int i=0; i< longitud - cadena.length(); i++) {
relleno = relleno.concat("  ");
}
resultado = relleno.concat(cadena);
Log.d("DEPURANDO", "Cadena1: |" + resultado + "|");
return resultado;
}

Log.d("DEPURANDO", "Cadena2: |" + cadena + "|");
return cadena;
}
}

}

//// Layout de la actividad



<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <LinearLayout
        android:id="@+id/linearLayout1"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dip"
        android:gravity="center" >

        <TextView
            android:id="@+id/tvCount"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="5dip"
            android:layout_marginTop="5dip"
            android:text="0:00:00.00"
            android:textColor="#ff22ee22"
            android:textSize="44sp" />
    </LinearLayout>


    <LinearLayout
        android:id="@+id/linearLayout2"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:orientation="horizontal" >

        <Button
            android:id="@+id/btnStart"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="0.5"
            android:padding="10dip"
            android:text="Start"
            android:textSize="24sp" />

        <Button
            android:id="@+id/btnStop"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="5dip"
            android:layout_marginTop="5dip"
            android:layout_weight="0.5"
            android:padding="10dip"
            android:text="Stop"
            android:textSize="24sp" />

        <Button
            android:id="@+id/btnLap"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="5dip"
            android:layout_marginLeft="5dip"
            android:layout_marginTop="5dip"
            android:layout_weight="0.5"
            android:padding="10dip"
            android:text="Lap"
            android:textSize="24sp" />
    </LinearLayout>

    <LinearLayout
        android:id="@+id/layTableHeader"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:background="#304930" >

        <TextView
            android:id="@+id/textView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="2dp"
            android:text="Lap"
            android:textAppearance="?android:attr/textAppearanceMedium"
            android:textColor="#eeeeee" />

        <TextView
            android:id="@+id/textView2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="14dp"
            android:text="Absolute time"
            android:textAppearance="?android:attr/textAppearanceMedium"
            android:textColor="#eeeeee" />

        <TextView
            android:id="@+id/textView3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="36dp"
            android:text="Lap time"
            android:textAppearance="?android:attr/textAppearanceMedium"
            android:textColor="#eeeeee" />
     
    </LinearLayout>
 
    <ScrollView
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" >

        <LinearLayout
            android:id="@+id/layTiempos"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:orientation="vertical"
            android:padding="4dip" >
        </LinearLayout>
    </ScrollView>

</LinearLayout>

//// AndroidManifest.xml



<manifest android:versioncode="1" android:versionname="1.0" package="not.ebacelo.android.crono02" xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-sdk android:minsdkversion="3">

    <application android:icon="@drawable/chrono2" android:label="@string/app_name">
        <activity android:configchanges="orientation|keyboardHidden|keyboard" android:label="@string/app_name" android:name=".Crono02Activity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN">

                <category android:name="android.intent.category.LAUNCHER">
            </category></action></intent-filter>
        </activity>
    </application>

</uses-sdk></manifest>