Widgets en Android actualizables desde PHP

Esta vez tocó investigar como hacer un widget para una aplicación Android pero con una peculiaridad: tenía que actualizar su contenido desde un servidor web con PHP + MySQL.

Desarrollar un widget con Android Studio es muy sencillo. Simplemente pulsamos con el botón derecho sobre el package de nuestro proyecto y elegimos la opción New -> Widget -> AppWidget.

Una vez hecho esto, Android Studio nos pregunta algunas cosas sobre cómo queremos nuestro widget.

Destacamos aquí las más importantes:

  • Class Name. El nombre que daremos nuestro widget. Vamos a llamarlo EjemploWidget.
  • Placement. Donde lo queremos. Para simplificar vamos a seleccionar Home Screen.
  • Resizable. Si es posible para el usuario cambiar el tamaño del widget.
  • Minimum Width y Minimum Height: Las “casillas” que tendrá nuestro widget de ancho y de alto respectivamente.

Una vez que pulsemos el botón Finish Android Studio nos creará un widget que contiene un TextView y que al ejecutarlo mostrará en él la palabra EXAMPLE.

Vamos a analizar los ficheros que nos ha creado y a aprender a modificarlos para conseguir nuestros objetivos.

Por una parte tenemos el fichero ejemplo_widget_info.xml.

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialKeyguardLayout="@layout/ejemplo_widget"
    android:initialLayout="@layout/ejemplo_widget"
    android:minWidth="40dp"
    android:minHeight="40dp"
    android:previewImage="@drawable/example_appwidget_preview"
    android:resizeMode="horizontal|vertical"
    android:updatePeriodMillis="86400000"
    android:widgetCategory="home_screen">
</appwidget-provider>

Este fichero contiene la configuración básica de nuestro widget. Al crear el widget elegimos 1 como su tamaño mínimo de ancho y de alto. Como vemos estos se han trasladado a dps. Si queremos modificar estos valores lo podemos hacer teniendo en cuenta la siguiente fórmula:

70 x n – 30.

Es decir, si queremos que el widget sea de 4×3 bloque tenemos que poner:

minWidth = 70 x 4 – 30 = 250dp

minHeight = 70 x 3 – 30 = 180dp

Otro parámetro importante es el de android:previewImage. Aquí especificaremos una imagen que será la que el usuario vea al seleccionar el widget para incluirlo en su Home Screen.

Por último tenemos el valor android:updatePeriodMillis donde especificaremos cada cuántos milisegundos se actualizará el widget. En este caso será cada 24 horas.

Dentro de nuestra carpeta res/layout se nos habrá generado otro fichero con el nombre ejemplo_widget.xml que será el que defina el aspecto de nuestro widget al igual que sucede con el resto de nuestras Activities.

Podemos editar este fichero para darle el aspecto que queramos pero teniendo en cuenta que NO todos los controles y layouts estarán disponibles dentro de un widget. Concretamente podemos hacer uso de los layouts:

  • FrameLayout
  • LinearLayout
  • RelativeLayout
  • GridLayout

Y de las clases:

  • AnalogClock
  • Button
  • Chronometer
  • ImageButton
  • ImageView
  • ProgressBar
  • TextView
  • ViewFlipper
  • ListView
  • GridView
  • StackView
  • AdapterViewFlipper

Una vez conseguido el aspecto adecuado para nuestro widget pasamos a ver el código que se ha generado en el fichero EjemploWidget.java.

import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.widget.RemoteViews;

/**
 * Implementation of App Widget functionality.
 */
public class EjemploWidget extends AppWidgetProvider {

    static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
                                int appWidgetId) {

        CharSequence widgetText = context.getString(R.string.appwidget_text);
        // Construct the RemoteViews object
        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.ejemplo_widget);
        views.setTextViewText(R.id.appwidget_text, widgetText);

        // Instruct the widget manager to update the widget
        appWidgetManager.updateAppWidget(appWidgetId, views);
    }

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        // There may be multiple widgets active, so update all of them
        for (int appWidgetId : appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId);
        }
    }

    @Override
    public void onEnabled(Context context) {
        // Enter relevant functionality for when the first widget is created
    }

    @Override
    public void onDisabled(Context context) {
        // Enter relevant functionality for when the last widget is disabled
    }
}

Como vemos se han generado una serie de métodos dentro de una clase que extiende de AppWidgetProvider.

Dentro del método updateAppWidget es donde se modifica el contenido del TextView y se le escribe la palabra EXAMPLE. Concretamente en la línea:

views.setTextViewText(R.id.appwidget_text, widgetText);

El problema que yo tenía que resolver era que en ese TextView tenía que poner lo que devolviese un fichero PHP alojado en un servidor.

La primera aproximación fue usar las funciones Volley para obtener el resultado de la llamada al PHP pero eso no funciona directamente sobre el widget sino que tenemos que crear un servicio asociado que haga esa llamada de manera asíncrona.

Esta es la solución:

EjemploWidget.java

import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

/**
 * Implementation of App Widget functionality.
 */
public class EjemploWidget extends AppWidgetProvider {

    private static final String LOG = "com.tasos.widgettest";


    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager,
                         int[] appWidgetIds) {

        Log.w(LOG, "onUpdate method called");
        // Get all ids
        ComponentName thisWidget = new ComponentName(context, MeteoWidget.class);
        int[] allWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget);

        // Build the intent to call the service
        Intent intent = new Intent(context.getApplicationContext(),UpdateWidgetService.class);
        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, allWidgetIds);

        // Update the widgets via the service
        context.startService(intent);
    }
}

Como vemos, hemos cambiado la clase EjemploWidget para dejar solamente el método onUpdate. En este método lo único que haremos será iniciar un servicio llamado UpdateWidgetService que será el que obtenga los datos desde el servidor PHP.

UpdateWidgetService .java

import android.app.PendingIntent;
import android.app.Service;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.IBinder;
import android.widget.RemoteViews;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;

public class UpdateWidgetService extends Service {

    public String string;
    public String inputLine;
    public StringBuilder txt;
    public BufferedReader in;
    public URL url;

    @Override
    public void onStart(Intent intent, int startId) {

        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this.getApplicationContext());
        int[] allWidgetIds = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);

        for (int widgetId : allWidgetIds) {

            RemoteViews remoteViews = new RemoteViews(this.getApplicationContext().getPackageName(),R.layout.ejemplo_widget);
            Intent clickIntent = new Intent(this.getApplicationContext(), EjemploWidget.class);
            clickIntent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
            clickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS,allWidgetIds);

            // Se refresca el widget pulsando sobre el TextView
            PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(),0, clickIntent,PendingIntent.FLAG_UPDATE_CURRENT);
            remoteViews.setOnClickPendingIntent(R.id.appwidget_text, pendingIntent);

            new DownloadTask().execute();

            appWidgetManager.updateAppWidget(widgetId, remoteViews);
        }
        stopSelf();

        super.onStart(intent, startId);
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    public class DownloadTask extends AsyncTask<String, Void, String> {

        @Override
        protected String doInBackground(String... arg0) {

            try {
                url = new URL("https://www.angelcasas.es/escribe.php");
                in = new BufferedReader(new InputStreamReader(url.openStream()));
                inputLine = null;
                txt = new StringBuilder();
                while ((inputLine = in.readLine()) != null) {

                    string = in.readLine();
                    txt.append(inputLine);
                    txt.append('\n');
                }
                in.close();

            } catch (MalformedURLException e) {

                e.printStackTrace();

            } catch (IOException e) {

                e.printStackTrace();
            }
            return null;
        }

        @Override
        protected void onPostExecute(String result) {

            AppWidgetManager widgetManager = AppWidgetManager.getInstance(getApplication());

            // Obtener los ids de los widgets
            ComponentName widget = new ComponentName(getApplication(), EjemploWidget.class);
            int[] widgetIds = widgetManager.getAppWidgetIds(widget);

            // Actualizar el texto del widget
            RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.ejemplo_widget);
            remoteViews.setTextViewText(R.id.appwidget_text, txt);

            // Refrescar el widget para mostrar el texto
            widgetManager.updateAppWidget(widgetIds, remoteViews);
        }
    }
}

Dentro del método DownloadTask, que es una tarea asíncrona, es donde llamamos al fichero PHP (que devuelve la cadena Hola Mundo) y almacenamos su resultado en la variable txt.

Posteriormente, una vez que finaliza la tarea, se ejecuta el método onPostExecute donde actualizamos el contenido del TextView y refrescamos el widget.