Pull to refresh

Пишем виджет ХабраКарма ex-CarmaWidget для Android

Development for Android *
Вчера я таки обновил CarmaWidget, виджет, который отображает вашу карму на рабочем столе смартфона. Сегодня я расскажу о том, как написать виджет.

image

Принцип таков:
  • Класс — настройки для виджета, который запускается при добавлении последнего на рабочий стол.
  • Класс — провайдер информации для виджета, в котором живут все таймеры.
  • База данных — для хранения информации о пользователе.
  • Два layout'а — для виджета и настройщика.




Собственно, поехали!

Класс для работы с базой данных.



В этом классе у меня будут находится все имена столбцов, таблиц, самой базы. Методы которые создают таблицу, удаляют, делают апгрейд ее при необходимости. Так же у меня реализован один приемчик. В связи с тем, что база на Android — SQLite, следовательно мы не имеем возможности одновременно писать в одну и ту же базу из нескольких потоков. Я это обошел, но ценой того, что все вызовы записи теперь должны быть запущены не из потока интерфейста, иначе рискуем получить Application Not Responding.

Класс называется DatabasHelper и наследует SQLiteOpenHelper.

Задаем все константы:
	public static String DATABASE_NAME = "carmawidget.db";
	private static int DATABASE_VERSION = 1;
	public static String TABLE_NAME = "users";
	
	public static String USERS_ID = "_id";
	public static String USERS_NAME = "name";
	public static String USERS_KARMA = "karma";
	public static String USERS_RATE = "rate";
	public static String USERS_UPDATE = "_update";
	public static String USERS_ICON = "icon";
	public static String USERS_ICON_URL = "icon_url";

  • DATABASE_NAME — имя базы данных
  • DATABASE_VERSION — версия базы данных. При изменении этого значения вызывается метод onUpdate(), о котором чуть позже
  • TABLE_NAME — имя таблицы
  • USERS_ID — первый столбец таблицы. Содержит уникальный идентификатор пользователя, который по сути является еще и идентификатором виджета.
  • USERS_KARMA — карма.
  • USERS_RATE — рейтинг.
  • USERS_UPDATE — частота обновлений.
  • USERS_ICON_URL — ссылка на иконку пользователя. Если при обновлении она меняется, то обновляем и саму иконку.
  • USERS_ICON — иконка.


Традиционно, разжевываю)
USERS_ID — Когда пользователь добавляет виджет ему присваивается идентификатор и если будет добавлен еще один такой же виджет, то получить доступ к какому-то конкретному можно будет как раз с помощью этого идентификатора. Возможно сейчас еще не очень понятно, чуть ниже будет поподробней.
USERS_ICON и USERS_ICON_URL — в настройщике есть CheckBox. Если он отмечен, то мы загружаем иконку, если нет, то в базу для обоих столбцов кладем NULL. Затем при обновлении мы читаем из базы USERS_ICON_URL и если оно не NULL, то значит пользователь указал в настройках, что мы хотим его загрузить и сверяем с тем, что только что получили с сервера и если они не совпали или USERS_ICON содержит NULL, то мы собственно обновляем иконку.

Определяем конструктор.
	public DatabaseHelper(Context context) {
		super(context, DATABASE_NAME, null, DATABASE_VERSION);
	}

Так как база одна, я его немножко упростил.

Пишем метод для генерации SQL запроса на создание таблицы.
	private String usersCreate()
	{
		StringBuilder result = new StringBuilder("CREATE TABLE ");
		result.append(TABLE_NAME);
		result.append(" (");
		result.append(USERS_ID);
		result.append(" INTEGER PRIMARY KEY, ");
		result.append(USERS_NAME);
		result.append(" TEXT NOT NULL, ");
		result.append(USERS_KARMA);
		result.append(" TEXT DEFAULT '0,00', ");
		result.append(USERS_RATE);
		result.append(" TEXT DEFAULT '0,00', ");
		result.append(USERS_ICON_URL);
		result.append(" TEXT, ");
		result.append(USERS_UPDATE);
		result.append(" INTEGER DEFAULT '0', ");
		result.append(USERS_ICON);
		result.append(" BLOB);");
		return result.toString();
	}


Метод, который создает базу.
	@Override
	public void onCreate(SQLiteDatabase db) {
		db.execSQL(usersCreate());
	}


Метод onUpgrade, который вызывается при изменении версии таблицы.
	@Override
	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
		db.execSQL("DROP TABLE IF EXISTS "+TABLE_NAME);
		onCreate(db);
	}

К примеру в процессе разработки понадобилось нам добавить столбец какой-нибудь. Правим генератор запроса и увеличиваем версию. База создастся заново. Но это самый простой способ, по-хорошему надо без удаления его добавлять.

Метод, который возвращает базу на запись.
	@Override
	public SQLiteDatabase getWritableDatabase()
	{
		SQLiteDatabase db = null;
		while (db == null)
		{
			try
			{
				db = super.getWritableDatabase();	
			}
			catch(SQLiteException e)
			{
				try {
					Thread.sleep(500);
				} catch (InterruptedException e1) {
				}
			}	
		}
		return db;

Сразу оговорюсь, возможно решение не совсем правильное, но оно вроде бы работает. Если нет возможности получить доступ на запись, то ждем полсекунды и пробуем еще раз.

Пишем класс для работы с сервером Хабры.



Задаем константы
	private static String USER_URL = "http://<username>.habrahabr.ru/";
	private static String API_URL = "http://habrahabr.ru/api/profile/";
	public static String RESULT_OK = "ok";
	public static String RESULT_SERVER = "server";
	public static String RESULT_USER_NOT_FOUND = "not_found";
	public static String RESULT_NETWORK = "network";
	
	public static String RESULT_NO_USERPIC = "http://habrahabr.ru/i/avatars/stub-user-middle.gif";

RESULT_NO_USERPIC — это линк на иконку «без аватара» хабры.

Метод для получения кармы и рейтинга.
public String[] getStats(String username)
	{
		String[] result = new String[] {"0,00", "0,00", RESULT_OK};
		String url = API_URL + Uri.encode(username);
		String proxyHost = android.net.Proxy.getDefaultHost();
		int proxyPort = android.net.Proxy.getDefaultPort();
		HttpClient httpClient = new DefaultHttpClient();
		if (proxyPort > 0)
		{
			HttpHost proxy = new HttpHost(proxyHost, proxyPort);
			httpClient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
		}
		HttpGet httpGet = new HttpGet(url);
		try {
			HttpResponse response = httpClient.execute(httpGet);
			if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK)
			{
				BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
				StringBuilder sb = new StringBuilder();
				String line = null;
				while ((line = reader.readLine()) != null) {
					sb.append(line + System.getProperty("line.separator"));
				}
				String answerInputString = sb.toString();
				if (answerInputString.contains("<habrauser>"))
				{
					if (answerInputString.contains("<error>404</error>"))
					{
						result[2] = RESULT_USER_NOT_FOUND;
					}
					else
					{
						result[0] = answerInputString.substring(answerInputString.indexOf("<karma>") + "<karma>".length(), answerInputString.indexOf("</karma>"));
						result[1] = answerInputString.substring(answerInputString.indexOf("<rating>") + "<rating>".length(), answerInputString.indexOf("</rating>"));
						result[0] = formatter(result[0]);
						result[1] = formatter(result[1]);
					}
				}
				else
				{
					result[2] = RESULT_SERVER;
				}
			}
			else
			{
				result[2] = RESULT_SERVER;
			}
		} catch (Exception e) {
			result[2] = RESULT_NETWORK;
		}
		return result;	
	}

result — выходное значение метода, массив из трех строк, где первая — карма, вторая — рейтинг а третья — сообщение об ошибке.

Так как api хабра может вернуть карму и рейтинг, например 12, то надо привести ее к виду 12,00.
	private String formatter(String string)
	{
		string = string.replace(".", ",");
		if (!string.contains(","))
		{
			string = string + ",00";
		}
		for (int i = 0; i < 2 - string.split(",")[1].length(); i++)
		{
			string = string + "0";
		}
		return string;
	}


Получаем линк иконки. У api такого нет, приходится парсить профиль пользователя.
	public String getUserpicUrl(String username)
	{
		String result = "";
		result = RESULT_NO_USERPIC;
		String url = USER_URL.replace("<username>", username);
		String proxyHost = android.net.Proxy.getDefaultHost();
		int proxyPort = android.net.Proxy.getDefaultPort();
		HttpClient httpClient = new DefaultHttpClient();
		if (proxyPort > 0)
		{
			HttpHost proxy = new HttpHost(proxyHost, proxyPort);
			httpClient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
		}
		HttpGet httpGet = new HttpGet(url);
		try {
			HttpResponse response = httpClient.execute(httpGet);
			if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK)
			{
				BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
				StringBuilder sb = new StringBuilder();
				String line = null;
				while ((line = reader.readLine()) != null) {
					sb.append(line + System.getProperty("line.separator"));
				}
				String answer = sb.toString();
				result = RESULT_NO_USERPIC;
				answer = answer.substring(answer.indexOf("<h1 class=\"habrauserava\">"));
				answer = answer.substring(answer.indexOf("<img src=\"") + "<img src=\"".length(), answer.indexOf("\" alt"));
				result = answer;
			}
		} catch (Exception e)
		{
			
		}
		return result;
	}


И собственно загружаем иконку.
	public Bitmap imageDownloader(String url)
	{
		Bitmap result = null;
		
		String proxyHost = android.net.Proxy.getDefaultHost();
		int proxyPort = android.net.Proxy.getDefaultPort();
		
		try {
			URL bitmapUrl = new URL(url);
			HttpURLConnection connection;
			if (proxyPort > 0)
			{
				InetSocketAddress proxyAddr = new InetSocketAddress(proxyHost, proxyPort);
				Proxy proxy = new Proxy(Proxy.Type.HTTP, proxyAddr);
				connection = (HttpURLConnection) bitmapUrl.openConnection(proxy);
			}
			else
			{
				connection = (HttpURLConnection) bitmapUrl.openConnection();					
			}
			connection.setDoInput(true);
			connection.connect();
			InputStream inputStream = connection.getInputStream();
			result = BitmapFactory.decodeStream(inputStream);
		}
		catch(Exception e)
		{
			
		}
		return result;
	}


Настройщик виджета


image

Класс Config — обыкновенный Activity, но он должен возвращать результат своей работы виджету. То есть если мы ткнули добавить виджет, появится этот настройщик, если он корректно выполнил свою работу то мы выставляем результат как положительный и закрываем его, если он, к примеру на нашел пользователя, то мы об этом уведомляем и дальше пользователь либо пробует заново либо закрывает. Так вот закрытие без успешного ввода данных должно выставлять результат как отмененный. Чуть ниже на деле.

public class Config extends Activity {
    /** Called when the activity is first created. */
	Context context;
    Thread updaterThread = new Thread();
    ProgressDialog progressDialog;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        context = this;
        setResult(RESULT_CANCELED);
        progressDialog = new ProgressDialog(this);
        progressDialog.setMessage(getApplicationContext().getResources().getString(R.string.loading));
        Button ready = (Button) findViewById(R.id.submit);
        ready.setOnClickListener(new OnClickListener(){

			@Override
			public void onClick(View v) {
				// TODO Auto-generated method stub
				if (updaterThread.getState() == State.NEW || updaterThread.getState() == State.TERMINATED)
				{
					updaterThread = new Thread(updater);
					updaterThread.start();
					progressDialog.show();
				}
			}
        
        });
    }
}

Выставляем интерфейс и пишем обработчик нажатия на кнопку Готово. setResult(RESULT_CANCELLED) — как раз первоначально выставляет результат действия как отмененный. Если все прошло хорошо, то мы поменяем его.

Добавляем сюда Runnable updater — отдельный поток, в котором будет происходить первоначальная загрузка данных.
   Runnable updater = new Runnable(){

		@Override
		public void run() {
			// TODO Auto-generated method stub
			String username = ((EditText) findViewById(R.id.username)).getText().toString();
			int update = ((Spinner) findViewById(R.id.update_value)).getSelectedItemPosition();
			boolean picupdate = ((CheckBox) findViewById(R.id.load_pic)).isChecked();
			String[] values = new HabrahabrAPI().getStats(username);
			if (values[2].equals(HabrahabrAPI.RESULT_OK))
			{
				Intent intent = getIntent();
				if (intent.getExtras() != null)
				{
					int appWidgetId = intent.getExtras().getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
					if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID)
					{
						ContentValues contentValues = new ContentValues();
						contentValues.put(DatabaseHelper.USERS_ID, appWidgetId);
						contentValues.put(DatabaseHelper.USERS_NAME, username);
						contentValues.put(DatabaseHelper.USERS_KARMA, values[0]);
						contentValues.put(DatabaseHelper.USERS_RATE, values[1]);
						contentValues.put(DatabaseHelper.USERS_UPDATE, update);
						AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(getApplicationContext());
						RemoteViews views = new RemoteViews(getApplicationContext().getPackageName(), R.layout.widget);
						if (picupdate == true)
						{
							String icon = new HabrahabrAPI().getUserpicUrl(username);
							contentValues.put(DatabaseHelper.USERS_ICON_URL, icon);
							if (!icon.equals(HabrahabrAPI.RESULT_NO_USERPIC))
							{
								Bitmap userpic = new HabrahabrAPI().imageDownloader(icon);
								if (userpic != null)
								{
									ByteArrayOutputStream baos = new ByteArrayOutputStream();
									userpic.compress(Bitmap.CompressFormat.PNG, 100, baos);
									contentValues.put(DatabaseHelper.USERS_ICON, baos.toByteArray());
									views.setBitmap(R.id.icon, "setImageBitmap", userpic);
								}
							}
							else
							{
								views.setBitmap(R.id.icon, "setImageBitmap", BitmapFactory.decodeResource(getApplicationContext().getResources(), R.drawable.userpic));
								ByteArrayOutputStream baos = new ByteArrayOutputStream();
								BitmapFactory.decodeResource(getApplicationContext().getResources(), R.drawable.userpic).compress(Bitmap.CompressFormat.PNG, 100, baos);
								contentValues.put(DatabaseHelper.USERS_ICON, baos.toByteArray());
							}
						}
						else
						{
							contentValues.putNull(DatabaseHelper.USERS_ICON);
							contentValues.putNull(DatabaseHelper.USERS_ICON_URL);
						}
						SQLiteDatabase db = new DatabaseHelper(getApplicationContext()).getWritableDatabase();
						db.insert(DatabaseHelper.TABLE_NAME, null, contentValues);
						db.close();
						contentValues.clear();
						views.setTextViewText(R.id.karma, values[0]);
						views.setTextViewText(R.id.rating, values[1]);
						appWidgetManager.updateAppWidget(appWidgetId, views);
						Intent resultValue = new Intent();
						resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
						setResult(RESULT_OK, resultValue);
						
						Intent updaterIntent = new Intent();
						updaterIntent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
						updaterIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] {appWidgetId});
						updaterIntent.setData(Uri.withAppendedPath(Uri.parse(Widget.URI_SCHEME + "://widget/id/"), String.valueOf(appWidgetId)));
						PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, updaterIntent, PendingIntent.FLAG_UPDATE_CURRENT);
						AlarmManager alarmManager = (AlarmManager) getApplicationContext().getSystemService(Context.ALARM_SERVICE);
						alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + (Widget.updatePeriodsMinutes[update] * 60000), Widget.updatePeriodsMinutes[update] * 60000, pendingIntent);
					}
				}
			}
			Bundle data = new Bundle();
			data.putString("RESULT", values[2]);
			Message message = new Message();
			message.setData(data);
			handler.sendMessage(message);
		}
    	
    };

Сначала мы получаем введенные данные из элементов интерфейса. И загружаем карму для %юзернейма%. Затем из intent'а получаем идентификатор виджета. Создаем переменную класса ContentValues, для того, чтоб положить в базу данных результат. Затем, с помощью RemoteViews, мы обновляем вид виджета, для которого делаем конфигурацию и кладем это добро в базу. В самом конце мы создаем Intent updaterIntent, с помощью которого мы и будем делать обновления. Android работает таком образом, что, если б мы не добавляли в этот intent данные setData(Uri), то он бы считал, что такой intent уже был и использовал бы его повторно. setData делается только для того, чтоб он был уникальным.

С помощью AlarmManager'а задаю время следующего обновления. Вообще devGuide говорит нам использовать время обновления через описание провайдера виджета, об этом чуть ниже. Но это не даст нам настраивать разное время обновлений. Можно использовать TimerTask, но при включении экрана он сразу запустит обновления за то время, пока аппарат был с выключенным экраном. AlarmManager в этом плане очень удобен, он не вызывает обновлений пока аппарат в суспенде, но если за это время должен был случиться хотя бы один апдейт, то он вызовет его один раз после запуска.

В конце этого runnable я вызываю Handler, в котором будет происходить обработка результатов и передаю ему третью строку из метода getStats(username), которая содержит сообщение об ошибке.


Обработчик результатов.
    Handler handler = new Handler(){
    	
    	@Override
    	public void handleMessage(Message message)
    	{
    		progressDialog.dismiss();
			AlertDialog.Builder builder = new AlertDialog.Builder(context);
			builder.setIcon(android.R.drawable.ic_dialog_alert);
			builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener(){

				@Override
				public void onClick(DialogInterface dialog, int which) {
					// TODO Auto-generated method stub
					dialog.dismiss();
				}
				
			});
    		String result = message.getData().getString("RESULT");
    		if (result.equals(HabrahabrAPI.RESULT_NETWORK))
    		{
    			builder.setTitle(R.string.error_network);
    			builder.setMessage(R.string.try_later);
    		}
    		if (result.equals(HabrahabrAPI.RESULT_USER_NOT_FOUND))
    		{
    			builder.setTitle(R.string.error_user);
    			builder.setMessage(R.string.try_user);
    		}
    		if (result.equals(HabrahabrAPI.RESULT_SERVER))
    		{
    			builder.setTitle(R.string.error_server);
    			builder.setMessage(R.string.try_later);
    		}
    		if (result.equals(HabrahabrAPI.RESULT_OK))
    		{
    			finish();
    		}
    		else
    		{
    			builder.show();
    		}
    	}
    };

В зависимости от этого результата мы выдаем соответствующее сообщение, но если результат RESULT_OK, заданный в классе с api хабра, то мы только закрываем этот Activity.

AppWidgetProvider


Основной класс, где происходит обновление виджетов.

Задаем константы:
	public static int[] updatePeriodsMinutes = new int[] {30, 60, 180, 360, 720}; 
	public static String URI_SCHEME = "karma_widget";

Массив в данном случае содержит значения минут для SpinnerView в классе Config. Нужен он для того, чтоб текстовые значения спиннера перевести в миллисекунды для AlarmManager'а. В Config мы берем индекс выбранного элемента и кладем его в базу. В провайдере мы берем элемент из массива с индексом из базы и умножаем его на 60000. Получаем миллисекунды.

Метод onEnabled()
	@Override
	public void onEnabled(Context context)
	{
		SQLiteDatabase db = new DatabaseHelper(context).getReadableDatabase();
		Cursor cursor = db.query(DatabaseHelper.TABLE_NAME, new String[] {DatabaseHelper.USERS_ID, DatabaseHelper.USERS_KARMA, DatabaseHelper.USERS_RATE, DatabaseHelper.USERS_UPDATE, DatabaseHelper.USERS_ICON}, null, null, null, null, null);
		while (cursor.moveToNext())
		{
			RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget);
			int id = cursor.getInt(cursor.getColumnIndex(DatabaseHelper.USERS_ID));
			String karma = cursor.getString(cursor.getColumnIndex(DatabaseHelper.USERS_KARMA));
			String rate = cursor.getString(cursor.getColumnIndex(DatabaseHelper.USERS_RATE));
			byte[] icon = cursor.getBlob(cursor.getColumnIndex(DatabaseHelper.USERS_ICON));
			int update = cursor.getInt(cursor.getColumnIndex(DatabaseHelper.USERS_UPDATE));
			views.setTextViewText(R.id.karma, karma);
			views.setTextViewText(R.id.rating, rate);
			if (icon == null)
			{
				views.setBitmap(R.id.icon, "setImageBitmap", BitmapFactory.decodeResource(context.getResources(), R.drawable.userpic));	
			}
			else
			{
				views.setBitmap(R.id.icon, "setImageBitmap", BitmapFactory.decodeByteArray(icon, 0, icon.length));
			}
			AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
			appWidgetManager.updateAppWidget(id, views);
			Intent updaterIntent = new Intent();
			updaterIntent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
			updaterIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] {id});
			updaterIntent.setData(Uri.withAppendedPath(Uri.parse(URI_SCHEME + "://widget/id/"), String.valueOf(id)));
			PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, updaterIntent, PendingIntent.FLAG_UPDATE_CURRENT);
			AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
			alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime(), updatePeriodsMinutes[update] * 60000, pendingIntent);
		} 
		cursor.close();
		db.close();
		super.onEnabled(context);
	}

Вообще вызывается он один раз при старте этого провайдера. Тут здаются все таймеры для уже существующих элементов. К примеру, мы только установили виджет. Метод вызовется, но так как база пустая, ничего не будет запущено. Но при рестарте телефона, он вызовет этот метод, и если там были элементы, то он запустит таймеры для обновлений и выставит им прошлые значения из базы данных.

Метод onDeleted()
	@Override
	public void onDeleted(Context ctxt, int[] ids)
	{
		final int[] appWidgetIds = ids;
		final Context context = ctxt;
		new Thread(new Runnable(){
			@Override
			public void run() {
				for (int i = 0; i < appWidgetIds.length; i++)
				{
					Intent intent = new Intent();
					intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
					intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] {appWidgetIds[i]});
					intent.setData(Uri.withAppendedPath(Uri.parse(URI_SCHEME + "://widget/id/"), String.valueOf(appWidgetIds[i])));
					PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
					AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
					alarmManager.cancel(pendingIntent);
					
					SQLiteDatabase db = new DatabaseHelper(context).getWritableDatabase();
					db.delete(DatabaseHelper.TABLE_NAME, DatabaseHelper.USERS_ID + " = " + appWidgetIds[i], null);
					db.close();
				}
			}
		}).start();
		super.onDeleted(ctxt, ids);
	}

Вызывается, когда мы удаляем виджет с рабочего стола. Ну тут мы просто останавливаем таймер и удаляем его из базы данных.

Метод onUpdate()
	@Override
	public void onUpdate(Context ctxt, AppWidgetManager mgr, int[] appWidgetIds)
	{
		final Context context = ctxt;
		final AppWidgetManager appWidgetManager = mgr;
		final int[] ids = appWidgetIds;
		new Thread(new Runnable(){

			@Override
			public void run() {
				// TODO Auto-generated method stub
				for (int i = 0; i < ids.length; i++)
				{
					appWidgetManager.updateAppWidget(ids[i], buildUpdate(context, ids[i]));
				}
			}
		}).start();
		super.onUpdate(ctxt, mgr, appWidgetIds);
	}

AlarmManager будет вызывать этот метод, когда нужно будет обновиться.

Метод buildUpdate()
	public RemoteViews buildUpdate(Context context, int id)
	{
		RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget);
		SQLiteDatabase db = new DatabaseHelper(context).getReadableDatabase();
		Cursor cursor = db.query(DatabaseHelper.TABLE_NAME, new String[] {DatabaseHelper.USERS_ID, DatabaseHelper.USERS_KARMA, DatabaseHelper.USERS_RATE, DatabaseHelper.USERS_ICON, DatabaseHelper.USERS_ICON_URL, DatabaseHelper.USERS_NAME}, DatabaseHelper.USERS_ID + " = " + String.valueOf(id), null, null, null, null);
		if (cursor.getCount() != 0)
		{
			ContentValues contentValues = new ContentValues();
			cursor.moveToFirst();
			String username = cursor.getString(cursor.getColumnIndex(DatabaseHelper.USERS_NAME));
			String karma = cursor.getString(cursor.getColumnIndex(DatabaseHelper.USERS_KARMA));
			String rate = cursor.getString(cursor.getColumnIndex(DatabaseHelper.USERS_RATE));
			String icon_url = cursor.getString(cursor.getColumnIndex(DatabaseHelper.USERS_ICON_URL));
			byte[] icon = cursor.getBlob(cursor.getColumnIndex(DatabaseHelper.USERS_ICON));
			String[] updated = new HabrahabrAPI().getStats(username);
			if (updated[2].equals(HabrahabrAPI.RESULT_OK))
			{
				if (!updated[0].equals(karma) || !updated[1].equals(rate))
				{
					karma = updated[0];
					rate = updated[1];
					contentValues.put(DatabaseHelper.USERS_KARMA, karma);
					contentValues.put(DatabaseHelper.USERS_RATE, rate);
				}	
			}
			views.setTextViewText(R.id.karma, karma);
			views.setTextViewText(R.id.rating, rate);
			if (icon_url != null)
			{
				String updatedIconUrl = new HabrahabrAPI().getUserpicUrl(username);
				if ((icon == null || !icon_url.equals(updatedIconUrl)) && !updatedIconUrl.equals(HabrahabrAPI.RESULT_NO_USERPIC))
				{
					icon_url = updatedIconUrl;
					Log.d("CarmaWidget", "Downloaded new userpic");
					Bitmap iconBitmap = new HabrahabrAPI().imageDownloader(icon_url);
					if (iconBitmap != null)
					{
						ByteArrayOutputStream baos = new ByteArrayOutputStream();
						iconBitmap.compress(CompressFormat.PNG, 100, baos);
						icon = baos.toByteArray();
						contentValues.put(DatabaseHelper.USERS_ICON_URL, icon_url);
						contentValues.put(DatabaseHelper.USERS_ICON, icon);
						views.setBitmap(R.id.icon, "setImageBitmap", iconBitmap);	
					}
				}
			}
			else
			{
				if (icon == null)
				{
					views.setBitmap(R.id.icon, "setImageBitmap", BitmapFactory.decodeResource(context.getResources(), R.drawable.userpic));	
				}
				else
				{
					views.setBitmap(R.id.icon, "setImageBitmap", BitmapFactory.decodeByteArray(icon, 0, icon.length));
				}
			}
			cursor.close();
			db.close();
			if (contentValues.size() != 0)
			{
				db = new DatabaseHelper(context).getWritableDatabase();
				db.update(DatabaseHelper.TABLE_NAME, contentValues, DatabaseHelper.USERS_ID + " = " + String.valueOf(id), null);
				db.close();
			}
		}
		else
		{
			cursor.close();
			db.close();
		}
		return views;
	}

В нем и происходит обновление, он достаточно похож на обновлялку класса Config

Затем нам необходимо выставить настройщик, ширину, высоту и layout нашего виджета. Для этого в папке res/xml надо создать xml файл такого вида:
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
	android:initialLayout="@layout/widget"
	android:minHeight="72dp"
	android:minWidth="146dp"
	android:configure="com.nixan.carmawidget.Config"/>

Сюда же можно добавить и время обновления, но, еще раз повторюсь, тогда оно будет одинаковым для всех виджетов.

И финал — правим AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.nixan.carmawidget"
      android:versionCode="3"
      android:versionName="1.2">
    <application android:icon="@drawable/icon" android:label="@string/app_name" android:theme="@android:style/Theme.Light">
        <activity android:name=".Config" android:screenOrientation="portrait">
            <intent-filter>
    			<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
    		</intent-filter>
        </activity>
		<receiver android:name=".Widget"
		    android:label="@string/app_name">
		    	<intent-filter>
		    		<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
		    	</intent-filter>	
		    	<meta-data android:name="android.appwidget.provider" android:resource="@xml/carmawidget_provider" />
		</receiver>
		<receiver android:name=".Widget"
		    android:label="@string/app_name">
		    	<intent-filter>
		    		<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
		    		<data android:scheme="karma_widget" />
		    	</intent-filter>	
		    	<meta-data android:name="android.appwidget.provider" android:resource="@xml/carmawidget_provider" />
		</receiver>
    </application>
    <uses-sdk android:minSdkVersion="3" />
	<uses-permission android:name="android.permission.INTERNET"/>
</manifest>


Оформление интерфейса лежит в исходниках, если что-то непонятно, то можно почитать тут и тут

Скачать исходники (172 Кб)
Скачать приложение (51 Кб)
Либо в маркете pub:nixan

Спасибо за внимание)
Tags: androidwidgetcarmawidget
Hubs: Development for Android
Total votes 87: ↑62 and ↓25 +37
Comments 27
Comments Comments 27

Popular right now