Création d'un widget paramétrable sur Android

Un des principaux avantage d'Android ce sont les widgets ! Ces "modules" offrent une possibilité à l'utilisateur de consulter rapidement une information ou bien d'effectuer une action. Malheureusement ils ne sont pas assez mis en place par les développeurs ! Mais c'est de l'histoire ancienne, grâce à ce tutoriel vous allez pouvoir créer un widget paramétrable dans votre application !

Ce tutoriel aborde plusieurs points importants d'android, je vous conseille d'avoir un minimum de connaissance dans le développement avec le petit bonhome vert. Nous allons travailler sur un widget permettant de récupérer une image ainsi qu'un texte sur un serveur distant et l'afficher sur "la home" du téléphone de l'utilisateur.

Fonctionnement des widgets

La création des widgets est disponible sur Android à partir de la version 1.5 grâce au framework App Widget.

Voici les quelques méthodes disponibles dans cette classe :

OnEnabled() : Appelé quand votre Widget est créé sur le bureau. Vous pouvez par exemple définir des préférences lors de cette initialisation.
OnUpdates() : Appelé quand le Widget nécessite une mise à jour de son interface.
OnReceive() : Permet de recevoir des actions à effectuer. Je ne les utilisent pas dans ce tutoriel
OnDisabled() : Appelé juste avant que le Widget soit supprimé de la home et suivi de la méthode OnDeleted()
OnDeleted() : Appelé quand le widget est supprimé.

Création du widget

Commençons par créer un nouveau projet sous Eclipse et nommez le ImageWidget.

* Build Target / Version du SDK : Version 2.1
* Nom de l'application : Image Widget
* Package : fr.maraumax.imagewidget
* Activité : ImageWidgetActivity

Création du projet - Etape 1

http://www.maraumax.fr/medias/Billets/image-widget/screen-creation-eclipse-1.jpg

Création du projet - Etape 2

http://www.maraumax.fr/medias/Billets/image-widget/screen-creation-eclipse-2.jpg

Création du projet - Etape 3

http://www.maraumax.fr/medias/Billets/image-widget/screen-creation-eclipse-3.jpg
Créer un nouveau fichier widget.xml de le répertoire layout de votre projet contenant :

<?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"
    android:padding="8dip"
    android:layout_margin="5dip"
    android:background="@drawable/image_widget"
    >
 
    <ImageView
        android:id="@+id/widget_image"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
       />
 
</LinearLayout>

Il s'agit du contenu de votre widget, une simple image dans cet exemple. Vous pouvez ajouter un TextView si vous le souhaitez, il faudra le modifier dans le service.

J'ai appliqué un background présent dans le dossier drawable, vous devez créer de répertoire dans res et ajouter un fichier "image_widget.xml" contenant :

<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >
 
    <stroke
        android:width="2dp"
        android:color="#ff959595" />
 
    <gradient
        android:angle="225"
        android:endColor="#DD1908ff"
        android:startColor="#DD0bd4ff" />
 
    <corners
        android:bottomLeftRadius="5dp"
        android:bottomRightRadius="5dp"
        android:topLeftRadius="5dp"
        android:topRightRadius="5dp" />
 
</shape>

Créer ensuite un répertoire res/xml et ajoutez-y un fichier image_widget_provider.xml avec le contenu suivant :

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
   		 android:minWidth="220dip"
   		 android:minHeight="146dip"
   		 android:updatePeriodMillis="0"
   		 android:initialLayout="@layout/widget"
   		 android:configure="fr.maraumax.imagewidget.ImageWidgetConfigure"
/>

Quelques explication sur ce fichier.
* android:minWidth : Largeur minimum du widget.
* android:minHeight : La hauteur minimum du widget.
* android:updatePeriodMillis : La fréquence de mise à jour du widget. Si elle est égale à 0 alors il n'y a pas de mise à jour. Nous allons utiliser les alarmes afin de mettre à jour notre widget, vous pouvez donc conserver cette valeur à zéro.
* android:initialLayout : Le layout du widget précédament crée.
* android:configure : L'activité qui sera appelé lors de la création du widget

Pour calculer la taille du widget utilisez la "formule" suivante :
Taille minimum = (nombre de cellules * 74dip) - 2 dip

Voici à quoi doit ressembler votre
arborescence pour l'instant.

http://www.maraumax.fr/medias/Billets/image-widget/screen-structure-projet-1.jpg

Nous devons ensuite indiquer à Android que notre projet possède un widget. Nous allons donc ajouter quelques lignes dans notre AndroidManifest.xml. Voici le fichier :

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="fr.maraumax.imagewidget"
    android:versionCode="1"
    android:versionName="1.0" >
 
    <uses-sdk android:minSdkVersion="7" />
 
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:label="@string/app_name"
            android:name=".ImageWidgetActivity" >
            <intent-filter >
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
 
         <!-- Activité de configuration -->
		<activity android:name=".ImageWidgetConfigure">
			<intent-filter>
				<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
			</intent-filter>
		</activity>
 
        <!-- Déclaration du widget -->
        <receiver android:name=".ImageWidgetProvider">
			<intent-filter>
				<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
			</intent-filter>
			<meta-data android:name="android.appwidget.provider"
				android:resource="@xml/image_widget_provider" />
		</receiver>
 
        <!-- Service du widget -->
		<service android:name=".ImageWidgetService"></service>
    </application>
 
    <!-- Permissions -->
    <uses-permission android:name="android.permission.INTERNET"></uses-permission>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
</manifest>

Le plus important est dans la balise receiver mais j'ai aussi ajouté quelques lignes :
* Une activité pour la configuration du widget
* Un service permettant de mettre à jour notre widget depuis le provider
* Deux autorisations android (android.permission.INTERNET pour télécharger les images et android.permission.WRITE_EXTERNAL_STORAGE pour sauvegarder les images en cache)

Il nous reste actuellement 3 fichiers java à créer :
* ImageWidgetProvider : Qui va permettre de créer et envoyer les information au service
* ImageWidgetService : Créer à partir du provider il va récupérer l'image et mettre à jour le widget
* ImageWidgetConfigure : Permettant de configurer notre widget.

Voici le contenu du fichier ImageWidgetProvider.java, à créer dans le répertoire src/fr.maraumax.imagewidget/ :

package fr.maraumax.imagewidget;
 
import java.util.Calendar;
 
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.net.Uri;
import android.preference.PreferenceManager;
 
public class ImageWidgetProvider extends AppWidgetProvider {
 
	SharedPreferences prefs;
 
	private PendingIntent service = null; 
 
	@Override
	public void onUpdate(Context context, AppWidgetManager appWidgetManager,
			int[] appWidgetIds)
	{
		// On récupère les préférences
		this.prefs = PreferenceManager.getDefaultSharedPreferences(context);
 
		// Création de l'alarme pour mettre à jour le widget
		final AlarmManager m = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
 
		final Calendar TIME = Calendar.getInstance();
		TIME.set(Calendar.MINUTE, 0);
		TIME.set(Calendar.SECOND, 0);
		TIME.set(Calendar.MILLISECOND, 0);
 
		for(int wid : appWidgetIds)
		{
			// Création de l'intent du service
			final Intent i = new Intent(context, ImageWidgetService.class);
			// On passe l'id du widget
			i.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { wid });
 
    		/*
    		 * Cette ligne de code permet de corriger un problème sur android
    		 * qui ne met à jour uniquement le dernier widget lorsque vous en ajoutez
    		 * plusieurs sur votre bureau. Ne le supprimez pas ;)
    		 */
			i.setData(Uri.withAppendedPath( Uri.parse("imgwidget://widget/id/"), String.valueOf(wid)));
 
			service = PendingIntent.getService(context, 0, i, PendingIntent.FLAG_CANCEL_CURRENT);  
 
			// Par défaut interval de 60 minutes
			long time = Long.valueOf(this.prefs.getString("widget"+wid+"time", "60"));
 
			m.setRepeating(AlarmManager.RTC, TIME.getTime().getTime(), time * 1000 * 60, service);
		}
	}
 
	@Override
	public void onDeleted(Context context, int[] appWidgetIds)
	{
		this.prefs = PreferenceManager.getDefaultSharedPreferences(context);
		Editor prefeditor = this.prefs.edit();
 
		for(int wid : appWidgetIds)
		{
			// On supprime les préférences
			prefeditor.remove("widget"+wid+"time");
 
			// On récupère l'intent à supprimer
			final Intent i = new Intent(context, ImageWidgetService.class);
			i.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { wid });
			i.setData(Uri.withAppendedPath( Uri.parse("imgwidget://widget/id/"), String.valueOf(wid)));
 
			service = PendingIntent.getService(context, 0, i, PendingIntent.FLAG_CANCEL_CURRENT);
 
			final AlarmManager m = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
 
			// On supprime l'alarme
			m.cancel(service); 
		}
 
		prefeditor.commit();
 
		super.onDeleted(context, appWidgetIds);
	}
}

Et voici le contenu du fichier ImageWidgetConfigure.java :

package fr.maraumax.imagewidget;
 
import java.util.Calendar;
 
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.net.Uri;
import android.preference.PreferenceManager;
 
public class ImageWidgetProvider extends AppWidgetProvider {
 
	SharedPreferences prefs;
 
	private PendingIntent service = null; 
 
	@Override
	public void onUpdate(Context context, AppWidgetManager appWidgetManager,
			int[] appWidgetIds)
	{
		// On récupère les préférences
		this.prefs = PreferenceManager.getDefaultSharedPreferences(context);
 
		// Création de l'alarme pour mettre à jour le widget
		final AlarmManager m = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
 
		final Calendar TIME = Calendar.getInstance();
		TIME.set(Calendar.MINUTE, 0);
		TIME.set(Calendar.SECOND, 0);
		TIME.set(Calendar.MILLISECOND, 0);
 
		for(int wid : appWidgetIds)
		{
			// Création de l'intent du service
			final Intent i = new Intent(context, ImageWidgetService.class);
			// On passe l'id du widget
			i.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { wid });
 
    		/*
    		 * Cette ligne de code permet de corriger un problème sur android
    		 * qui ne met à jour uniquement le dernier widget lorsque vous en ajoutez
    		 * plusieurs sur votre bureau. Ne le supprimez pas ;)
    		 */
			i.setData(Uri.withAppendedPath( Uri.parse("imgwidget://widget/id/"), String.valueOf(wid)));
 
			service = PendingIntent.getService(context, 0, i, PendingIntent.FLAG_CANCEL_CURRENT);  
 
			// Par défaut interval de 60 minutes
			long time = Long.valueOf(this.prefs.getString("widget"+wid+"time", "60"));
 
			m.setRepeating(AlarmManager.RTC, TIME.getTime().getTime(), time * 1000 * 60, service);
		}
	}
 
	@Override
	public void onDeleted(Context context, int[] appWidgetIds)
	{
		this.prefs = PreferenceManager.getDefaultSharedPreferences(context);
		Editor prefeditor = this.prefs.edit();
 
		for(int wid : appWidgetIds)
		{
			// On supprime les préférences
			prefeditor.remove("widget"+wid+"time");
 
			// On récupère l'intent à supprimer
			final Intent i = new Intent(context, ImageWidgetService.class);
			i.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { wid });
			i.setData(Uri.withAppendedPath( Uri.parse("imgwidget://widget/id/"), String.valueOf(wid)));
 
			service = PendingIntent.getService(context, 0, i, PendingIntent.FLAG_CANCEL_CURRENT);
 
			final AlarmManager m = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
 
			// On supprime l'alarme
			m.cancel(service); 
		}
 
		prefeditor.commit();
 
		super.onDeleted(context, appWidgetIds);
	}
}

Le fichier de configuration du widget, dans le même répertoire :

package fr.maraumax.imagewidget;
 
import android.appwidget.AppWidgetManager;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.preference.ListPreference;
import android.preference.PreferenceManager;
import android.preference.PreferenceActivity;
 
public class ImageWidgetConfigure extends PreferenceActivity {
    int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
 
    ListPreference listeTimePref;
 
    SharedPreferences prefs;
 
    public ImageWidgetConfigure() {
        super();
    }
 
    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
 
        setResult(RESULT_CANCELED);
 
		addPreferencesFromResource(R.xml.image_widget_preferences);
 
		this.listeTimePref = (ListPreference) findPreference("widget_time");
 
        Intent intent = getIntent();
        Bundle extras = intent.getExtras();
        if (extras != null) {
            mAppWidgetId = extras.getInt(
                    AppWidgetManager.EXTRA_APPWIDGET_ID,
                    AppWidgetManager.INVALID_APPWIDGET_ID);
        }
 
        // Si l'id du widget == 0
        if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
            finish();
        }
 
		this.prefs = PreferenceManager.getDefaultSharedPreferences(this);
 
		// Préférences par défault
		updateWidgetDefaultPrefs();
    }
 
	private void updateWidgetPrefs()
	{
		Editor prefeditor = this.prefs.edit();
 
        // Time
        prefeditor.putString("widget"+mAppWidgetId+"time", this.listeTimePref.getValue());
 
        // Set default values
        prefeditor.putString("widget_time", "");
 
        prefeditor.commit();
	}
 
	/*
	 * Valeur par défaut
	 */
	private void updateWidgetDefaultPrefs()
	{
		if(this.prefs.contains("widget"+mAppWidgetId+"time"))
			this.listeTimePref.setValue(this.prefs.getString("widget"+mAppWidgetId+"time", ""));
		else
			this.listeTimePref.setValue("60");
	}
 
	/*
	 * Mise à jour du widget
	 */
    private void confirm()
    {
		Intent resultValue = new Intent();
		resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
		setResult(RESULT_OK, resultValue);
		finish();
 
        new ImageWidgetProvider()
        .onUpdate(this,
                  AppWidgetManager.getInstance(this),
                  new int[] { mAppWidgetId }
         );
    }
 
    @Override
    public void onBackPressed()
    {
    	this.updateWidgetPrefs();
 
    	this.confirm();
 
    	super.onBackPressed();
    }
}

Ainsi que notre fichier des préférences dans le répertoire /res/xml :

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
    android:title="Paramètres du widget">
 
	<PreferenceCategory
		android:key="widget_image_preferences_category_settings"
		android:title="Paramètres"
	>
		<ListPreference  
			android:title="Fréquence de mise à jour"
			android:summary="Sélectionnez la fréquence de mise à jour des images."
			android:entries="@array/widgetListOptionsTimeLabels"
			android:entryValues="@array/widgetListOptionsTimeValues"
			android:negativeButtonText="Annuler"
			android:key="widget_time"
			android:defaultValue="60"
			>
		</ListPreference>
 
	</PreferenceCategory>
 
</PreferenceScreen>

Nos deux tableaux dans un nouveau fichier arrays.xml du répertoire res/values/ :

<?xml version="1.0" encoding="utf-8"?>
<resources>
 
	<string-array name="widgetListOptionsTimeLabels">
		<item>15 minutes</item>
		<item>30 minutes</item>
		<item>1 heure</item>
		<item>2 heures</item>
	</string-array>
    <string-array name="widgetListOptionsTimeValues">
		<item>15</item>
		<item>30</item>
		<item>60</item>
		<item>120</item>
	</string-array>
 
</resources>

Et pour terminer notre fichier ImageWidgetService.java :

package fr.maraumax.imagewidget;
 
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Random;
 
import android.app.PendingIntent;
import android.app.Service;
import android.appwidget.AppWidgetManager;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.Log;
import android.widget.RemoteViews;
 
public class ImageWidgetService extends Service {
 
	AsyncImageLoader loader;
	protected boolean isLoaded = false;
	AppWidgetManager appWidgetManager;
 
	protected Handler handler = new Handler();
 
	SharedPreferences prefs;
 
	@Override
	public void onStart(Intent intent, int startId)
	{
		this.prefs = PreferenceManager.getDefaultSharedPreferences(this);
 
		this.appWidgetManager = AppWidgetManager.getInstance(this
				.getApplicationContext());
 
		int[] allWidgetIds = intent
				.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
 
		for(int widgetId : allWidgetIds)
		{
			if(widgetId == 0)
				continue;
 
			RemoteViews remoteViews = new RemoteViews(this
					.getApplicationContext().getPackageName(),
					R.layout.widget);
 
			// Chargement
			remoteViews.setImageViewResource(R.id.widget_image, android.R.color.transparent);
			this.appWidgetManager.updateAppWidget(widgetId, remoteViews);
 
			// Clique sur l'image
			Intent clickIntentRefresh = new Intent(this.getApplicationContext(),
					ImageWidgetProvider.class);
			clickIntentRefresh.setData(Uri.withAppendedPath(Uri.parse("imgwidget://widget/id/"), String.valueOf(widgetId)));
 
			clickIntentRefresh.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
			clickIntentRefresh.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS,
					new int[] { widgetId });
 
			PendingIntent pendingIntentRefresh = PendingIntent.getBroadcast(
					getApplicationContext(), 0, clickIntentRefresh,
					PendingIntent.FLAG_UPDATE_CURRENT);
 
			remoteViews.setOnClickPendingIntent(R.id.widget_image, pendingIntentRefresh);
 
			// Notre image aléatoire
			loadImage(remoteViews, widgetId, this.randomImg());
		}
 
		stopSelf();
 
		super.onStart(intent, startId);
	}
 
	@Override
	public IBinder onBind(Intent intent) {
		return null;
	}
 
	private void loadImage(RemoteViews remoteViews, int widgetId, String url)
	{
		this.loader = new AsyncImageLoader(this, getFile(), this.handler, widgetId, remoteViews);
		this.loader.execute(url);
	}
 
	/*
	 * Récupération d'une image aléatoire
	 */
	private String randomImg()
	{
		/*
		 * Notez que l'android virtual devide (avd) semble poser
		 * problème avec les domaines. (Soucis de DNS)
		 * Utilisez des ip direct pour vos test si vous en avez sinon
		 * vous pouvez utiliser votre ip local.
		 */
		ArrayList<String> urls = new ArrayList<String> ();
		urls.add("http://91.121.222.207/divers/android-1.jpg");
		urls.add("http://91.121.222.207/divers/android-2.jpg");
		urls.add("http://91.121.222.207/divers/android-3.jpg");
		urls.add("http://91.121.222.207/divers/android-4.jpg");
 
		return urls.get(new Random().nextInt(urls.size()));
	}
 
	private File getFile()
	{
		File cachedir = null;
		if (android.os.Environment.getExternalStorageState().equals( android.os.Environment.MEDIA_MOUNTED ))
			cachedir = new File( android.os.Environment.getExternalStorageDirectory(), "cache.jpg" );
		else
			cachedir = getApplicationContext().getCacheDir();
 
		try {
			cachedir.createNewFile();
		} catch (IOException e) {
			e.printStackTrace();
		}
 
		synchronized (cachedir)
		{
			return cachedir;
		}
	}
 
	public void updateAppWidgetImage(int widgetId, RemoteViews remoteViews, Bitmap bitmap)
	{
		remoteViews.setImageViewBitmap(R.id.widget_image, bitmap);
		this.appWidgetManager.updateAppWidget(widgetId, remoteViews);
	}
 
	private static class AsyncImageLoader
		extends AsyncTask < String, Integer, Integer >
	{
		private static final int RESULT_SUCCESS = 0x00;
		private static final int RESULT_FAILED = 0x01;
		private static final int RESULT_CANCELLED = 0x02;
 
		ImageWidgetService activity;
		File file;
 
		private RemoteViews rv;
		private int viewId;
 
		private Handler handler;
 
		public void init(ImageWidgetService widgetImageUpdateService)
		{
			activity = widgetImageUpdateService;
		}
 
		public AsyncImageLoader(ImageWidgetService widgetImageUpdateService, File f, Handler h, int vid, RemoteViews remoteViews)
		{
			this.handler = h;
			file = f;
			this.rv = remoteViews;
			this.viewId = vid;
 
			init( widgetImageUpdateService );
		}
 
		@Override
		protected Integer doInBackground(String... params)
		{
			if (params.length != 1)
				return RESULT_FAILED;
 
			URL url;
			try
			{
				url = new URL( params[0] );
				HttpURLConnection conn = (HttpURLConnection) url.openConnection();
 
				if (isCancelled())
					return RESULT_CANCELLED;
 
				InputStream input = conn.getInputStream();
				OutputStream output = new FileOutputStream( file );
 
				byte[] bytes = new byte[4096];
				for (;;)
				{
					int count = input.read( bytes );
					if (count == -1)
						break;
 
					if (isCancelled())
					{
						output.close();
						file.delete();
						return RESULT_CANCELLED;
					}
 
					output.write( bytes, 0, count );
				}
				output.close();
			}
			catch (MalformedURLException e)
			{
				return RESULT_FAILED;
			}
			catch (IOException e)
			{
				return RESULT_FAILED;
			}
			return RESULT_SUCCESS;
		}
 
		@Override
		protected void onPostExecute(Integer result)
		{
			switch (result)
			{
			case RESULT_SUCCESS:
				final Bitmap bitmap = getBitmapFromFile( file );
				if (bitmap != null)
				{
					 this.handler.postDelayed(new Runnable() {
						public void run()
						{
							activity.updateAppWidgetImage(viewId, rv, bitmap);
						}
					 }, 100);
 
					activity.isLoaded = true;
				}
				break;
			case RESULT_FAILED:
				break;
			case RESULT_CANCELLED:
				break;
			}
 
			activity.loader = null;
		}
	}
 
	public static Bitmap getBitmapFromFile(File file)
	{
		try
		{
			BitmapFactory.Options opt = new BitmapFactory.Options();
			opt.inPreferredConfig = Bitmap.Config.RGB_565;
 
			opt.inJustDecodeBounds = true;
			BitmapFactory.decodeFile( file.getAbsolutePath(), opt );
 
			opt.inSampleSize = computeSampleSize( opt, -1, 2048 * 2048 );
			opt.inJustDecodeBounds = false;
			opt.inPurgeable = true;
			opt.inInputShareable = true;
 
			return BitmapFactory.decodeFile( file.getAbsolutePath(), opt );
		}
		catch (NullPointerException e)
		{
			Log.d("", "decode failed, NullPointerException occured.", e);
		}
		catch (OutOfMemoryError ex)
		{
			Log.d("", "decode failed, OutOfMemory occured." );
		}
		return null;
	}
 
 
	private static int computeSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels)
	{
		int initialSize = computeInitialSampleSize( options, minSideLength, maxNumOfPixels );
 
		int roundedSize;
		if (initialSize <= 8)
		{
			roundedSize = 1;
			while (roundedSize < initialSize)
				roundedSize <<= 1;
		}
		else
			roundedSize = (initialSize + 7) / 8 * 8;
 
		return roundedSize;
	}
 
	private static int computeInitialSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels)
	{
		double w = options.outWidth;
		double h = options.outHeight;
 
		int lowerBound = (maxNumOfPixels == -1) ? 1 : (int) Math.ceil( Math.sqrt( w * h / maxNumOfPixels ) );
		int upperBound = (minSideLength == -1) ? 128 : (int) Math.min( Math.floor( w / minSideLength ), Math.floor( h / minSideLength ) );
 
		if (upperBound < lowerBound)
			return lowerBound;
 
		if ((maxNumOfPixels == -1) && (minSideLength == -1))
			return 1;
		else if (minSideLength == -1)
			return lowerBound;
		else
			return upperBound;
	}
}

J'ai commenté le code pour éviter de compliquer les choses en reprenant parties par parties pour commenter.

Attention : les images que vous souhaitez afficher dans votre widget ne doivent pas excéder la taille de 600 x 480 pixels environ (d'après mes tests) sinon elles ne seront pas affichés ! Faites bien attention à ce point.

Lancez ensuite votre application, une vue avec "Hello World, ImageWidgetActivity!" devrait apparaitre. Vous pouvez quitter l'application pour vous rendre sur la home. Faites un appui long sur le bureau et sélectionnez "Widgets".
http://www.maraumax.fr/medias/Billets/image-widget/screen-avd-1.jpg
Rechercher le widget Image Widget dans la liste et sélectionnez le.
http://www.maraumax.fr/medias/Billets/image-widget/screen-avd-2.jpg
Ensuite se lance l'activité de configuration du widget, vous pouvez modifier la fréquence de mise à jour si vous le souhaitez.
http://www.maraumax.fr/medias/Billets/image-widget/screen-avd-3.jpg
Cliquez sur retour et votre widget apparait sur le bureau !
http://www.maraumax.fr/medias/Billets/image-widget/screen-avd-4.jpg

Votre widget se met désormais à jour en fonction de la fréquence sélectionné avec une image aléatoire ! Vous pouvez cependant le mettre à jour manuellement en cliquant dessus.

Vous pouvez télécharger les sources du tutoriel à cette adresse, n'hésitez pas à critiquer ou me poser des questions sur le billet du blog. En espérant vous avoir été utile ! Bon développements !