«

»

abr 17 2013

Cómo hacer una extensión para DashClock Widget

Probablemente en las últimas semanas habréis oído hablar del widget DashClock, y no es para menos. Este widget pensado para usar en la pantalla de bloqueo de Android Jelly Bean 4.2 fue sacado al público recientemente por Roman Nurik, uno de los diseñadores del equipo de Google. Es una muestra de como hacer aprovechar la nueva función de widgets en la pantalla de bloqueo, de forma útil, minimalista y atractiva.

androcode_dashclock

Pero hay una característica de este widget que a nosotros nos interesa especialmente: se pueden crear extensiones. Con relativamente poco esfuerzo podemos añadir funcionalidad al widget mediante aplicaciones externas, usando un sistema de módulos que el usuario puede añadir y quitar a su antojo. Hoy vamos a ver cómo podemos aprovechar la API de este magnífico widget.

Qué podemos hacer

La API de extensiones del widget es bastante simple y está muy bien documentada. Pero veamos un resumen de lo que podremos hacer. Aunque como todo widget puede colocarse en el escritorio, vamos a considerar durante todo el tutorial que lo usamos en la pantalla de bloqueo, pues es para lo que está diseñado.

Partes del widget y las extensiones

El widget tiene 2 modos: contraído y expandido. Cuando está contraído muestra la hora y a su derecha un máximo de 3 extensiones, con su icono y un (muy) breve texto. Al expandirlo se mostrará en su lugar un botón para abrir las preferencias, y debajo de la hora saldrán todas las extensiones activas en una lista vertical con el icono, un título y un mensaje opcional.

lockscreen

Las extensiones por lo tanto están compuestas por:

  • Un icono, sencillo y monocolor.
  • Un estado, una cadena de pocos caracteres (3-4 aprox.). Si contiene un y sólo un salto de línea, el estado usará una fuente más pequeña y ocupará 2 líneas. Aunque el autor recomienda evitarlo si es posible.
  • Un título, que se usará en la forma expandida. Similar al estado, pero más largo. Puede contener varias líneas, aunque el widget limitará las que se muestran.
  • Un cuerpo o mensaje, que se mostrará debajo del título en la forma expandida. Su función es mostrar información secundaria o adicional. Puede contener múltiples líneas, aunque el widget limitará las que se muestran.
  • Un Intent, que se lanzará para abrir una actividad cuando se pulse en la extensión, tanto en la forma expandida como la contraída.
  • Una actividad de ajustes, para personalizar parámetros de la extensión si lo necesitamos. Se abre desde la propia pantalla de configuración del widget.

Todos estos parámetros (salvo la actividad de ajustes) se recogen en la clase ExtensionData, que se usa para actualizar la información mostrada por el widget.

Por otra parte, los métodos y acciones más importantes que podemos usar y sobreescribir del widget son:

  • onInitialize(boolean isReconect): Se llama cuando la extensión establece conexión con el widget. Aquí debemos hacer cualquier tarea de inicialización antes de que el widget empiece a pedir información a la extensión.
  • onUpdateData(int reason): Se llama cuando el widget solicita información actualizada a la extensión. Aquí haremos las operaciones oportunas y le pasaremos al widget la información actualizada que queramos mostrar. Por defecto se llama aproximadamente una vez cada hora, pero puede llamarse en otras circunstancias. El motivo de la petición se indica mediante una constante que se le pasa como parámetro.
  • publishUpdate(ExtensionData data): A través de este método actualizamos la información mostrada en el Widget. Como parámetro le pasamos la información que vimos en el apartado anterior.
  • Dos métodos que en la mayoría de casos no serán necesarios: setUpdateWhenScreenOn y addWatchContentUris. El primero hace que se ejecute el onUpdateData al encender la pantalla, y el segundo lo hace cuando el contenido en una URI ha cambiado.

Con esto deberíamos tener una idea general de lo que nos permite hacer el Widget y cómo podemos aprovecharlo en nuestra aplicación.

Ejemplo: monitor de Twitter

Veamos un ejemplo sencillo de cómo desarrollaríamos una extensión para DashClock. Haremos una extensión que consulte periódicamente los resultados de una búsqueda en Twitter concreta para monitorizar lo que se habla de ello: el nombre de nuestra web, de nuestra aplicación, una marca, una persona…

ejemplo

Queremos que cuando encuentre nuevos Tweets nos aparezca una entrada en DashClock avisándonos. Lo ideal sería tener una aplicación completa haciendo las tareas y que ésta actualice el Widget cuando sea necesario, pero para el ejemplo nos limitaremos a hacerlo todo en la propia extensión.

El código completo de la aplicación está en un repositorio de GitHub, donde podéis consultar todos los detalles. A continuación voy a explicar las partes más significativas del código.

Error!

Con la API 1.1 de Twitter fuera de juego, este código de ejemplo no funcionará correctamente, porque la consulta a Twitter devolverá un error. Para reproducir el comportamiento del ejemplo sería necesario adaptar el código para hacer uso de la nueva api con autenticación, lo cual queda fuera del propósito de este tutorial. Aun así, el código relativo a DashClock sigue siendo el mismo y sigue sirviendo como ejemplo válido.

1. Importar librería

La API de DashClock viene en forma de librería jar que debemos importar en nuestro proyecto. Sólo hay que bajarla de la página de descargas alojada en Google Code, y añadirla a la carpeta libs del proyecto. Automáticamente debería añadirse al Build Path y ya estaremos listos para empezar a programar.

2. Registrar la extensión en el AndroidManifest.xml

En nuestro omnipresente AndroidManifest.xml añadimos una entrada de tipo service, pues eso es la extensión, como se muestra a continuación. Debemos asegurarnos de requerir el permiso READ_EXTENSION_DATA para que sólo DashClock pueda hacer uso de la extensión, además de varios campos meta-data para la versión del protocolo (de momento sólo hay 1), la descripción de la extensión (para mostrarse en la configuración del Widget), y la Activity de configuración de nuestra extensión (opcional).

<service
            android:name=".TwitterSearchExtension"
            android:icon="@drawable/ic_extension"
            android:label="@string/extension_title"
            android:permission="com.google.android.apps.dashclock.permission.READ_EXTENSION_DATA" >
            <intent-filter>
                <action android:name="com.google.android.apps.dashclock.Extension" />
            </intent-filter>

            <meta-data
                android:name="protocolVersion"
                android:value="1" />
            <meta-data
                android:name="description"
                android:value="@string/extension_description" />
            <!-- A settings activity is optional -->
            <meta-data
                android:name="settingsActivity"
                android:value=".ConfigActivity" />
        </service>

3. Extender DashClockExtension e implementar métodos

Ahora debemos extender la clase DashClockExtension y sobreescribir, como mínimo, el método onUpdateData. En nuestra clase también sobreescribiremos el onInitialize, pero sólo lo haremos para inicialización de propiedades.

@Override
    protected void onInitialize(boolean isReconnect) {
        /*
         * Aquí llamaríamos a setUpdateWhenScreenOn(boolean) o addWatchContentUris(String[]) si quisiéramos usar dichas funciones.
         * En este caso no lo hacemos
         */
        mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
    }

    @Override
    protected void onUpdateData(int reason) {
        String busqueda = mPrefs.getString(QUERY, "androcode");

        // Creamos el objeto ExtensionData que rellenaremos con los datos que queremos mostrar en el Widget
        ExtensionData data = new ExtensionData();
        data.icon(R.drawable.ic_extension);
        data.clickIntent(new Intent(ACTION_RESULTS));

        try {
            // Hacemos la búsqueda en Twitter. Esto es independiente de nuestra extensión
            JSONArray tweets = getNewTweets(busqueda);
            Integer cantidad = tweets.length();
            String cantidadMostrar = (cantidad <= MAX_TWEETS) ? cantidad.toString() : MAX_TWEETS + "+";

            // Ahora sí, configuramos los datos del Widget
            data.status(cantidadMostrar);
            data.expandedTitle(getResources().getQuantityString(R.plurals.extension_content_title, cantidad, cantidadMostrar));
            data.expandedBody(String.format(getString(R.string.extension_content_body), busqueda));
            if (cantidad < 1) {
                data.visible(false); // Mientras no haya información ocultamos la extensión para dejar sitio a otras
            } else {
                data.visible(true);
            }
        } catch (Exception e) {
            // Normalmente haríamos otra cosa al capturar la excepción, pero esto viene bien para debug
            data.visible(true)
                    .expandedTitle(e.getClass().getCanonicalName())
                    .expandedBody(e.getMessage())
                    .status("ERROR");
        }

        // Y finalmente le decimos al Widget que actualice los datos
        publishUpdate(data);
    }

El método onUpdateData es bastante auto explicativo y está comentado. Pero repasemos brevemente lo que hace:

  • Crea un nuevo objeto de tipo ExtensionData, que es el que debemos rellenar con los datos que queremos mostrar en el Widget y que ya vimos al principio del artículo. Le asigna el icono y un clickIntent, que se llamará cuando pulsemos la entrada de la extensión en el Widget. Más adelante veremos para qué.
  • Obtiene un array de JSON que contiene los Tweets resultantes de la búsqueda. El método encargado de esta tarea no se muestra arriba porque no es relevante en cuanto a lo que quiero enseñar con este ejemplo, pero puedes consultarlo completo aquí.
  • En el campo de estado de la extensión escribe el número de tweets nuevos encontrados. Recordad que éste debe ser un texto muy breve, debemos poner algo corto y relevante que se mostrará junto al icono de la extensión.
  • En el título y cuerpo expandidos escribe un texto indicando el número de resultados y la cadena que se buscó, respectivamente. Estos textos pueden ser más largos y descriptivos que el estado. Podríamos incluso poner en el cuerpo el contenido de los primeros Tweets si quisiéramos.
  • Establece como visible la extensión si hay más de 1 tweet nuevo. De lo contrario la oculta para no estorbar, siguiendo la premisa de “si no tienes nada bueno que decir, cállate”.
  • Además captura las posibles excepciones (lanzadas por los métodos de consulta de Twitter), y de ocurrir alguna la mostramos en la propia extensión. Esto NO lo deberíamos hacer jamás para una extensión de verdad. En este caso es útil para hacer debug durante el desarrollo, pero en producción deberíamos tratar la excepción de forma más cuidadosa.
  • Finalmente actualiza la información del Widget enviando los datos recogidos en nuestro ExtensionData mediante el método publishUpdate. Ahora el Widget se encarga del resto, y mostrará lo que le hemos mandado.

4. Activity de configuración y de respuesta

Por último, y de forma opcional, vamos a crear dos actividades: una para configurar la extensión y otra para abrirla cuando pulsemos sobre la extensión en el Widget.

settingsLa primera es muy simple, solamente tendrá un campo de texto donde configuraremos el texto que queremos que la extensión busque en Twitter. Haremos que automáticamente se guarde al salir de la Activity.

public class ConfigActivity extends Activity {

    private SharedPreferences mPrefs;
    private TextView mQuery;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_config);
        getActionBar().setDisplayHomeAsUpEnabled(true);

        mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
        mQuery = (TextView) findViewById(R.id.query);

        // Coloca el texto que actualmente está guardado
        mQuery.setText(mPrefs.getString(TwitterSearchExtension.QUERY, "androcode"));
    }

    @Override
    protected void onPause() {
        super.onPause();
        mPrefs.edit()
                .putString(TwitterSearchExtension.QUERY, mQuery.getText().toString().trim()) //Guarda el texto para las búsquedas
                // Y elimina las referencias al último tweet consultado y último visto, puesto que la búsqueda es distinta.
                .putLong(TwitterSearchExtension.ULTIMO_TWEET_CONSULTADO, 0)
                .putLong(TwitterSearchExtension.ULTIMO_TWEET_VISTO, 0)
                .commit();
    }

    //...

}

La segunda Activity será más bien de pega. Si este fuera un caso real y estuviésemos integrando nuestra aplicación ya existente con DashClock seguramente querríamos mostrar una Activity de nuestra aplicación donde mostremos la lista de resultados, para llevar control de los resultados leídos y no leídos, entre otras cosas. Pero en nuestro ejemplo, lo único que haremos será abrir el navegador con la página de Twitter correspondiente a la búsqueda. Además, guardamos el valor del último Tweet consultado por la extensión como “visto”, para hacer las próximas buscas a partir de él.

public class ResultsActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        finish();

        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);

        // Marcamos el último tweet consultado como último visto
        long ultimoConsultado = prefs.getLong(TwitterSearchExtension.ULTIMO_TWEET_CONSULTADO, 0);
        prefs.edit().putLong(TwitterSearchExtension.ULTIMO_TWEET_VISTO, ultimoConsultado).commit();

        // Abrimos la página de twitter, por hacer algo
        String query = prefs.getString(TwitterSearchExtension.QUERY, "androcode");
        startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://twitter.com/search?q=" + query)));
    }
}

Por suerte DashClock lanza una petición de actualización sobre las extensiones tanto al pulsar sobre la extensión como al salir de los ajustes, por lo que no tenemos que preocuparnos de reiniciar los datos mostrados en ambos casos. Cuando justo después se ejecute el onUpdateData ya estaremos preparados para devolver los datos correctos.

5. Resultado

Y con esto podemos instalar la aplicación y añadir nuestra extensión a DashClock para conseguir lo que veis en la captura del principio. ¿A que no ha sido para tanto?

Play StoreComo dije antes, el código completo de la aplicación se encuentra publicado en este repositorio de GitHub. Además, si quieres probarla sin tener que compilar el código, puedes descargarla desde el Play Store aquí mismo.

Conclusión

DashClock nos ofrece grandes posibilidades a lo desarrolladores, puesto que ningún usuario en su sano juicio pondrá un widget completo en su pantalla de bloqueo (o incluso en el escritorio) para algo tan simple como lo que hemos visto en el ejemplo, pero quizás sí les gustaría centralizar pequeños fragmentos de información en un sitio. Es la gran ventaja que nos ofrece la barra de notificaciones de Android frente a mostrar un cuadro de diálogo modal interrumpiendo nuestro trabajo, pero llevado al área de la pantalla de bloqueo. Personalmente, habría preferido que Android 4.2 trajera por defecto algo como DashClock (con una API un poco más potente), en vez de eso de poner widgets completos.

Espero que con este artículo os animéis a crear extensiones de utilidad. Y sobre todo, si tenéis aplicaciones que podrían hacer uso del Widget, integrarlas. Es una buena forma darle visibilidad a tu aplicación frente al resto, y seguro que tus usuarios agradecen tener esta opción.

Enlaces: Descarga DashClock | Documentación oficial | Repositorio ejemplo

Acerca del autor

Rafa Vázquez

Estudio Ingeniería Informática del Software en Sevilla. Me considero geek sin dinero, amante y desarrollador novato de Android. He creado algunas aplicaciones como SeviBus, TicTacDroide, Kill Bieber y Traductor Hoygan, si es que se puede llamar aplicaciones a estas dos últimas ;) Ganas de aprender más y más no me faltan, e intentaré compartir mis experiencias con vosotros en la medida de lo posible.

  • http://www.forgottenprojects.com/ Fernando F. Gallego

    Genial! ya tengo unas cuantas ideas de widgets para el dashclock

  • http://twitter.com/inyaki_mwc iñaki villar (@inyaki_mwc)

    muy bueno el artículo!!!