Пишем виджет ХабраКарма ex-CarmaWidget для 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

    Спасибо за внимание)
    Поделиться публикацией

    Похожие публикации

    Комментарии 27
      +3
      Да сколько же можно карму дергать-то?
        +4
        Пока не достигнешь экстаза =))) и не забрызгаешь окружающихподелишься с окружающими своей кармой
        +1
        Разумнее было бы если при отказе грузить аватарку — виджет автоматом становился бы 1х1
          0
          Кстати. Есть идея сделать простенький виджетик, который бы показывал текущую цену на нефть и динамику стрелочкой. В нашей стране он бы пользовался успехом :)
            0
            Тогда уж лучше любой числовой показатель, которые можно извлечь с любой веб страницы.
            0
            А минусовая на черном фоне? =)
            • НЛО прилетело и опубликовало эту надпись здесь
                0
                Спасибо за комментарий, на досуге обязательно поправлю.

                А по поводу обновлялки, вывел ее в тред, т.к. в примерах у гугла IntentSerivce используется, и вроде бы написано что провайдер в уи треде живет. В любом случае, надо будет попробовать.
                +3
                За статью спасибо, как раз начал с андройдом разбираться.
                Но вот виджет бесполезный совсем)
                И подсветки синтаксиса не хватает.
                  +3
                  Ну не погодный же виджет делать, в конце-то концов :) Автор молодец :) Я сейчас в процессе написания некоторого приложения для Android, с виджетами только планировал разбираться. А тут такой удачный топик плюс грамотный комментарий.
                +4
                Кармадрочерство выходит на новый уровень! :)
                  +1
                  дрочи с нами, дрочи как мы, дрочи лучше нас!!! )
                  +7
                  В последнее время все больше постов про Android. Очень рад данному факту.
                    +1
                    Ну и зачем она вам нужна???

                    Зависимость от цифр?

                    Вас при помощи этих цифр заставляют писать сюда. Наполнять контент. Забирают ваше драгоценное время, хотя вы сами добровольно отдаёте его.
                      +1
                      Мне будет стыдно на свою карму смотреть)
                        +1
                        Приятно, что теперь все чаще люди пишут статьи по программированию :)
                          +1
                          Хочу предостеречь по поводу метода BitmapFactory.decodeStream(inputStream). У него есть бага при закачке некоторых картинок (напоролся, когда работал с Flickr).

                          Лучше использовать BitmapFactory.decodeByteArray(data, 0, data.length), где data это массив байтов, полученный из inputStream'a.
                            0
                            Сталкивался, но на герое с 1.5 все заработало, поэтому я так и оставил, но спасибо.
                            0
                            Ы! Я на скриншоте. :)
                              0
                              Господи, как же это прекрасно! Теперь дрочить на карму можно будет даже в общественном транспорте!
                                0
                                1. Статья хорошая, и замечания по поводу кода чрезвычайно ценны.

                                2. Все таки не хватает расцветки синтаксиса, было бы проще читать

                                3. В методе usersCreate необходимость использования StringBuilder для меня не очевидна. Да, выполнятся быстрее, да, меньше расходы на создание новых объектов, да, методологически более верный метод. Но этот код выполняется только один раз, так что выигрыш получается не очень большой, а читать сложнее. Я бы просто использовал оператор конкатенации для получения текста SQL запроса
                                  0
                                  хм, кто взял мой аватар и перевернул его?.. ;)
                                    0
                                    Тремя ответами выше
                                      0
                                      Не прокатит, я сам его рисовал в гимпе довольно давно :)
                                        0
                                        Брат!!!11
                                      0
                                      Большое спасибо за подробное описание. Делая свой виджет столкнулся с проблемой, а благодаря вашему посту решил ее =)

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

                                      Самое читаемое