Interceptar “clics” de un link en un TextView

Se me solicitó en un proyecto, mostrar una ventana de dialogo al usuario, cuando este hiciera clic en un link contenido en un TextView, con la idea de notificarle que el link va a ser abierto en otro programa (el browser) y confirmar que quiere abrirlo.

Me puse a buscar, y encontré que no es demasiado “hacky” a mi criterio.

Primero hay que crear una clase que herede de ClickableSpan para poder interceptar el clic en un “Span”

public class InternalURLSpan extends ClickableSpan {
 private OnClickListener mListener;
public InternalURLSpan(OnClickListener listener) {
 mListener = listener;
 }
@Override
 public void onClick(View view) {
 mListener.onClick(view);
 }
}

Después solo toca asignar un SpannableString al TextView con el contenido HTML. Eso sí, nos tocará “buscar” los links, para ir asignando los diferentes “spans” que harán clickeable el link, utilizando nuestra clase, que nos permitirá al fin, interceptar el clic, y hacer con él lo que querramos.

Yo lo que hice fue implementar un método estático en una clase utiilitaria para poder utilizarlo en diferentes partes del proyecto.

    public static void setHtmlText(TextView textview, String htmlStr, OnClickListener listener) {
        SpannableString f = new SpannableString(htmlStr);<br /><br />        int start = 0;
        int end = 0;
        int offset = 0;<br /><br />        while ((start = htmlStr.indexOf("http", offset)) != -1) {
            end = htmlStr.indexOf(" ", start);
            if (end == -1) {
                end = htmlStr.length();  //Llegamos al final del string original
            }
            f.setSpan(new InternalURLSpan(listener), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            offset = end;
        }<br /><br />        MovementMethod m = textview.getMovementMethod();
        if (m == null || !(m instanceof LinkMovementMethod)) {
            if (textview.getLinksClickable()) {
                textview.setMovementMethod(LinkMovementMethod.getInstance());
            }
        }
        textview.setText(f);
    }

Ahora solo nos falta “interpretar” ese click. Como se aprecia, solo vamos a saber que se hizo clic sobre el TextView. Por fortuna, al hacer clic sobre un link en un TextView, esto provoca que el link en sí mismo sea seleccionado, por lo que a la hora de interceptar el clic, solo se ocupa preguntarle al TextView cual es el área seleccionada para obtener el link y utilizarlo según convenga.

        ViewUtil.setHtmlText(myTextView, "Esto es una prueba, consulte http://www.google.com o http://developer.android.com", new OnClickListener() {

            @Override
            public void onClick(View v) {
                if (v instanceof TextView){
                    TextView tv = (TextView) v;
                String url =  tv.getText().subSequence(tv.getSelectionStart(), tv.getSelectionEnd()).toString();
                Toast.makeText(context, url, Toast.LENGTH_SHORT).show();
                }
            }
        });

Fuente: http://blog.elsdoerfer.name/2009/10/29/clickable-urls-in-android-textviews/

Google IO 2012

Segundo año que tengo la oportunidad para asistir al Google IO. Con un poco de suerte y ahorrando para  este evento se logró. La verdad que lo recomiendo a cualquier entusiasta de las tecnologías de Google. No solo por los “regalitos” sino también por la experiencia de asistir a un evento de este calibre, networking y hasta para poder conocer un poco más sobre la ciudad de San Francisco.

No voy a comentar sobre lo que sucedió durante el evento. Mucho de ello lo pueden ver en muchísimos sitios o en los videos de los dos keynotes y sessiones.

Simplemente les quiero compartir las fotos que tomé durante este viaje.

Dia 1:

Google IO 2012 – Day 1

Dia 2:

Dia 3:

Por cierto, aqui pueden encontrar todas las sesiones grabadas: https://developers.google.com/events/io/

Manejando cambio de orientación con un Progress Dialog

Puede que tengamos una pantalla, que en algún momento ejecute un cambio o solicitud de manera asíncrona. Para esto debemos mostrarle al usuario algo mientras, ya sea de manera determinada (progreso del trabajo o tarea) o indeterminada (el famoso spinner).

Cuando hay un cambio de orientación, si la actividad no maneja el cambio de orientación, probablemente sea destruida y recreada. Entonces ¿Qué pasa con el ProgressDialog?.

Iniciemos con dos reglas que debemos seguir:

  1. Si va a crear el AsyncTask dentro de la actividad como un inner class, asegúrese de que sea estática. Las inner class no estáticas, guardan una referencia de la outer class, en nuestro caso, la Actividad. Esto quiere decir que estamos “filtrando” (leaking) memoria, pues esa referencia de la actividad queda ahi mientras siga vivo el AsyncTask, y esto hay que evitarlo.
  2. Para crear el ProgressDialog, utilice los métodos que tiene disponible la actividad. Me refiero a onCreateDialog, showDialog y dismissDialog. Esto por que a la hora de cambiar la orientación de la pantalla y al destruir y recrear la actividad, Android va a mantener el estado de los dialogs que estaban presentes y se encargará de mostrarlos nuevamente cuando la actividad es recreada.
Ahora bien, aún siguiendo estas reglas, el principal problema es que la actividad que creó el AsyncTask puede que se destruya por lo que el AsyncTask pierda la referencia (y así debe ser). La solución es simplemente desacoplar la actividad del AsyncTask. Empleando los métodos del ciclo de vida de la activad, es posible “notificar” o “actualizar” el AsyncTask, para que se percate del cambio de actividad y pueda mostrar o ocultar el ProgressDialog.

El consenso es utilizar el método onRetainNonConfigurationInstance, para “guardar” la referencia del AsynctTask, así, cuando la nueva actividad es creada, ella se dará cuenta si la tarea todavia está en progreso, para poder “acoplarse” a ella.

public class EjemploProgressDialogActivity extends Activity {
    public static final String TAG = "EXAMPLE_DIALOG";

    public static final int PROGRESS_DIALOG = 1;

    MyTask task;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Log.d(TAG, "onCreate");
        Object obj = getLastNonConfigurationInstance();
        if (obj != null && obj instanceof MyTask) {
            Log.d(TAG, "Tarea previa ejecutandose");
            task = (MyTask) obj;
            task.attach(this);
        } else {
            task = new MyTask(this);
            task.execute(10);
            Log.d(TAG, "Nueva tarea creada y ejecutada");
        }
    }

    @Override
    protected Dialog onCreateDialog(int id) {

        switch (id) {
        case PROGRESS_DIALOG:
            ProgressDialog pd = new ProgressDialog(this);
            pd.setTitle("Trabajando");
            pd.setMessage("Por favor espere...");
            return pd;

        default:
            break;
        }
        return super.onCreateDialog(id);
    }

    @Override
    protected void onPause() {
        super.onPause();
        Log.d(TAG, "onPause");
    }

    @Override
    protected void onRestart() {
        super.onRestart();
        Log.d(TAG, "onRestart");

    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.d(TAG, "onResume");
    }

    @Override
    protected void onStart() {
        super.onStart();
        Log.d(TAG, "onStart");
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.d(TAG, "onStop");
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        // Aqui es donde se hace la magia
        if (task != null) {
            task.deattach();
            return task;
        }
        return super.onRetainNonConfigurationInstance();
    }

    private static class MyTask extends AsyncTask {
        WeakReference ctx;

        public MyTask(Activity activity) {
            super();
            attach(activity);
        }

        @Override
        protected void onPreExecute() {
            Activity activity = ctx.get();
            if (activity != null && !activity.isFinishing()) {
                Log.d(TAG, "Mostrando Progress Dialog");
                activity.showDialog(PROGRESS_DIALOG);
            }
        }

        @Override
        protected Void doInBackground(Integer... params) {
            int seconds = params[0];
            try {
                Log.d(TAG, "Tarea va a durar " + seconds + " segundos");
                Thread.sleep(seconds * 1000);
                Log.d(TAG, "Tarea Lista");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return null;
        }

        @Override
        protected void onPostExecute(Void result) {
            if (ctx != null && ctx.get() != null) {
                Activity activity = ctx.get();
                if (!activity.isFinishing()) {
                    Log.d(TAG, "Ocultando Progress Dialog");
                    activity.dismissDialog(PROGRESS_DIALOG);
                }
            }
        }

        public void attach(Activity activity) {
            this.ctx = new WeakReference(activity);
        }

        public void deattach() {
            ctx = null;
        }

    }
}

En la línea 14, pueden ver que en el método onCreate se pregunta si se salvó algún objeto previamente y luego se hacen los chequeos respectivos para asegurarse que el objeto sea el AsyncTask. En este ejemplo en particular, se crea el AsyncTask en el método onCreate, por lo que en el caso de que no exista, se crea.

En la línea 18 se ejecuta el método attach,para que el AsyncTask tenga la nueva referencia de la Actividad.

En la línea 77 que es el momento en que la actividad da su último suspiro, el AsyncTask se desacopla y se “salva” la referencia al AsyncTask para que la siguiente actividad (si es el caso) la retome, como vimos en el método onCreate.

Si ven la implementación del AsyncTask, primero que todo, se guarda la referencia a la actividad dentro de un WeakReference, para así evitar “filtrar” memoria.

En los métodos onPreExecute y onPostExecute se hacen validaciones para asegurarse de que la referencia de la actividad sea válidad (que exista la referencia y que la actividad no esté en proceso de morirse).

Si ejecutáramos éste código,  sin mover el dispositivo, esta sería la salida en la bitácora:

10-04 21:54:46.254: DEBUG/EXAMPLE_DIALOG(6155): onCreate
10-04 21:54:46.294: DEBUG/EXAMPLE_DIALOG(6155): Mostrando Progress Dialog
10-04 21:54:46.514: DEBUG/EXAMPLE_DIALOG(6155): Tarea va a durar 10 segundos
10-04 21:54:46.514: DEBUG/EXAMPLE_DIALOG(6155): Nueva tarea creada y ejecutada
10-04 21:54:46.514: DEBUG/EXAMPLE_DIALOG(6155): onStart
10-04 21:54:46.524: DEBUG/EXAMPLE_DIALOG(6155): onResume
10-04 21:54:56.518: DEBUG/EXAMPLE_DIALOG(6155): Tarea Lista
10-04 21:54:56.524: DEBUG/EXAMPLE_DIALOG(6155): Ocultando Progress Dialog
10-04 21:55:12.395: DEBUG/EXAMPLE_DIALOG(6155): onPause
10-04 21:55:12.554: DEBUG/EXAMPLE_DIALOG(6155): onStop

Pero si cambiamos la orientación del dispositivo, esto sería el resultado:

te
10-04 21:56:08.584: DEBUG/EXAMPLE_DIALOG(6155): Mostrando Progress Dialog
10-04 21:56:08.754: DEBUG/EXAMPLE_DIALOG(6155): Nueva tarea creada y ejecutada
10-04 21:56:08.754: DEBUG/EXAMPLE_DIALOG(6155): onStart
10-04 21:56:08.754: DEBUG/EXAMPLE_DIALOG(6155): Tarea va a durar 10 segundos
10-04 21:56:08.764: DEBUG/EXAMPLE_DIALOG(6155): onResume
10-04 21:56:10.424: DEBUG/EXAMPLE_DIALOG(6155): onPause
10-04 21:56:10.424: DEBUG/EXAMPLE_DIALOG(6155): onStop
10-04 21:56:10.544: DEBUG/EXAMPLE_DIALOG(6155): onCreate
10-04 21:56:10.544: DEBUG/EXAMPLE_DIALOG(6155): Tarea previa ejecutandose
10-04 21:56:10.544: DEBUG/EXAMPLE_DIALOG(6155): onStart
10-04 21:56:10.704: DEBUG/EXAMPLE_DIALOG(6155): onResume
10-04 21:56:18.755: DEBUG/EXAMPLE_DIALOG(6155): Tarea Lista
10-04 21:56:18.755: DEBUG/EXAMPLE_DIALOG(6155): Ocultando Progress Dialog
10-04 21:56:21.626: DEBUG/EXAMPLE_DIALOG(6155): onPause
10-04 21:56:21.784: DEBUG/EXAMPLE_DIALOG(6155): onStop

Imprimí además cuando se ejecutan otros métodos del ciclo de vida, para tener una referencia de cuando ocurre qué.

Charla sobre el Android Market

Hoy a las 12pm, estaré dando una charla sobre el Comercialización de Aplicaciones Android en la ULACIT, en un curso sobre Android que imparte mi amigo Julio Marín (sí, el de El ChouTV).

Aqui les dejo además algunos links de interes (algunos incluidos en la presentación como QR code)