Разработка виджета для отображения курса bitcoin

от автора

Привет, хаброчитатели!

Предыстория: с недавних пор отслеживаю рост курса криптовалюты bitcoin(BTC). Раньше для просмотра котировок заглядывал на сайт какой-нибудь биржи, но гораздо удобнее иметь на рабочем экране смартфона небольшой виджет, который отобразит актуальную информацию.

На маркетах представлена куча разнообразных виджетов, показывающих курсы криптовалют. Но гораздо интереснее создать что-то свое:)
В этой статье я кратко опишу свой опыт создания виджета для гаджета под управлением ОС Android.

Для понимания материала статьи желательно начальное знакомство с Java и Android-разработкой в IDE Android Studio. Если нет — Вам сюда:) Android developers.

Мой небольшой виджет будет отображать цену биткоина в долларах и время, на которое актуальна данная цена. Информация будет загружаться с русскоязычной биржи BTC-e, поскольку эта площадка отдает курс в удобном JSON-формате в ответ на get-запрос по url btc-e.nz/api/3/ticker/btc_usd.
Пример ответа биржи:

{"btc_usd":{"high":880,"low":836,"avg":858,"vol":3774491.17766,"vol_cur":4368.01172,"last":871.999,"buy":872,"sell":870.701,"updated":1482754417}}

Итак, для начала разработки создаем в IDE новый проект с помощью подходящего шаблона. Выбираем «Start a new Android Studio project», затем вводим имя и расположение проекта, выбираем целевое устройство и версию API, далее нужен пункт «Add no activity».

Скриншоты

image
image
image

После того, как откроется IDE workspace, создадим пустой виджет с помощью встроенных шаблонов. Для этого нужно в дереве файлов проекта вызвать контекстное меню на папке app и выбрать пункт New->Widget->App Widget.

Скриншот

image

Будут созданы несколько файлов, из которых особенно интересны три.
Первый — xml-файл (res->xml->btcwidget_info.xml) с основными параметрами виджета:

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

Параметр initialLayout задает имя xml-файла с визуальной разметкой виждета. minHeight и min-Width — минимальные размеры добавляемого виджета, updatePeriodMillis — время обновления информации в мс, но не чаще раза в полчаса (параметр 10 мс все равно воспринимается как минимальные 30 мин).

Второй xml-файл (res->layout->btcwidget.xml) содержит параметры визуального отображения виджета (разметка визуальных элементов).
В нем находится описание одного визуального элемента TextView внутри разметки RelativeLayout (Layouts):

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"     android:layout_width="match_parent"     android:layout_height="match_parent"     android:background="#09C"     android:padding="@dimen/widget_margin">     <TextView         android:id="@+id/appwidget_text"         android:layout_width="wrap_content"         android:layout_height="wrap_content"         android:layout_centerHorizontal="true"         android:layout_centerVertical="true"         android:layout_margin="8dp"         android:background="#09C"         android:contentDescription="@string/appwidget_text"         android:text="@string/appwidget_text"         android:textColor="#ffffff"         android:textSize="20sp"         android:textStyle="bold|italic"         /> </RelativeLayout>

Самое интересное — третий файл с шаблоном исходного кода виджета на языке java. В таком виде шаблон не несет функциональной нагрузки, однако по комментариям кода можно понять, какой метод когда выполняется и за что отвечает:

Шаблон кода виджета

/**  * Implementation of App Widget functionality.  */ public class BTCWidget 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.btcwidget);         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     } }

Стоит сразу сказать, что ОС Android позволяет создавать большое количество экземпляров нашего виджета и их поведение будет целиком описываться приведенным выше кодом. Для более глубокого понимания поведения и жизненного цикла виджетов рекомендую ознакомиться со статьей Android Widget. Ниже в комментариях кода я объясню лишь некоторые моменты.
Столкнулся со следущей сложностью: система позволяет виджету обновляться (методом updateAppWidget) не чаще чем раз в 30 минут по соображениям экономии батареи. Но мне хотелось иметь возможность видеть данные в реальном времени и я нашел способ обойти это ограничение. Для этого виджет был запрограммирован к принудительному обновлению по клику на него. Реализовал такое действие следующим образом: по нажатию на виджет в систему отправляется интент (Intent), который ловится самим же виджетом и обрабатывается запуском обновления данных. Если кто-то знает способ проще — буду рад советам в комментариях:)

Исходный код виджета с добавленной функциональностью

 /**  * Implementation of App Widget functionality.  */ public class BTCwidget extends AppWidgetProvider {      private static final String SYNC_CLICKED    = "btcwidget_update_action";     private static final String WAITING_MESSAGE = "Wait for BTC price";     public static final int httpsDelayMs = 300;      //этот метод выполняется, когда пора обновлять виджет     static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,                                 int appWidgetId)  {          //Объект RemoteViews дает нам доступ к отображаемым в виджете элементам:         RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.btcwidget);         //в данном случае - к TextView         views.setTextViewText(R.id.appwidget_text, WAITING_MESSAGE);         appWidgetManager.updateAppWidget(appWidgetId, views);          String output;         //запускаем отдельный поток для получения данных с сайта биржи         //в основном потоке делать запрос нельзя - выбросит исключение         HTTPRequestThread thread = new HTTPRequestThread();         thread.start();         try {             while (true) {                 Thread.sleep(300);                 if(!thread.isAlive()) {                     output = thread.getInfoString();                     break;                 }             }          } catch (Exception e) {             output = e.toString();         }        //выводим в виджет результат         views.setTextViewText(R.id.appwidget_text, output);          // Instruct the widget manager to update the widget         appWidgetManager.updateAppWidget(appWidgetId, views);     }      @Override     public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {          RemoteViews remoteViews;         ComponentName watchWidget;          remoteViews = new RemoteViews(context.getPackageName(), R.layout.btcwidget);         watchWidget = new ComponentName(context, BTCwidget.class);          //при клике на виджет в систему отсылается вот такой интент, описание метода ниже         remoteViews.setOnClickPendingIntent(R.id.appwidget_text,   getPendingSelfIntent(context, SYNC_CLICKED));         appWidgetManager.updateAppWidget(watchWidget, remoteViews);          //обновление всех экземпляров виджета          for (int appWidgetId : appWidgetIds) {             updateAppWidget(context, appWidgetManager, appWidgetId);         }      }     //этот метод ловит интенты, срабатывает когда интент создан нажатием на виджет и   //запускает обновление виджета     @Override     public void onReceive(Context context, Intent intent) {          super.onReceive(context, intent);          if (SYNC_CLICKED.equals(intent.getAction())) {              AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);              RemoteViews remoteViews;             ComponentName watchWidget;              remoteViews = new RemoteViews(context.getPackageName(), R.layout.btcwidget);             watchWidget = new ComponentName(context, BTCwidget.class);              remoteViews.setTextViewText(R.id.appwidget_text, WAITING_MESSAGE);              //updating widget             appWidgetManager.updateAppWidget(watchWidget, remoteViews);              String output;             HTTPRequestThread thread = new HTTPRequestThread();             thread.start();             try {                 while (true) {                     Thread.sleep(httpsDelayMs);                     if(!thread.isAlive()) {                         output = thread.getInfoString();                         break;                     }                 }              } catch (Exception e) {                 output = e.toString();             }              remoteViews.setTextViewText(R.id.appwidget_text, output);              //widget manager to update the widget             appWidgetManager.updateAppWidget(watchWidget, remoteViews);          }     }         //создание интента     protected PendingIntent getPendingSelfIntent(Context context, String action) {         Intent intent = new Intent(context, getClass());         intent.setAction(action);         return PendingIntent.getBroadcast(context, 0, intent, 0);     } } 

Содержимое класса HTTPRequestThread.java:

Посмотреть:

package com.hakey.btcwidget;  import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.util.Calendar;  class HTTPRequestThread extends Thread{     private static final String urlString = "https://btc-e.nz/api/3/ticker/btc_usd";      String getInfoString() {         return output;     }      private String output = "";      private void requestPrice() {          try {             URL url = new URL(urlString);             HttpURLConnection con = (HttpURLConnection) url.openConnection();             con.setRequestMethod("GET");             BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));             String inputLine;             StringBuilder response = new StringBuilder();              while ((inputLine = in.readLine()) != null) {                 response.append(inputLine);             }             in.close();              output = "Price: " + JSONParser.getPrice(response.toString())                     + "\n" + getTimeStamp();          } catch (Exception e) {             output = e.toString();         }     }      @Override     public void run() {         requestPrice();     }      private String getTimeStamp() {         Calendar calendar = Calendar.getInstance();         if(calendar.get(Calendar.MINUTE)>9) {              return "Time: " + calendar.get(Calendar.HOUR_OF_DAY)                     + ":" + calendar.get(Calendar.MINUTE);         } else {             return "Time: " + calendar.get(Calendar.HOUR_OF_DAY)                     + ":0" + calendar.get(Calendar.MINUTE);         }      } } 

Парсер ответа с сервера — JSONParser.java:

Смотреть:

 package com.hakey.btcwidget;  import org.json.JSONException; import org.json.JSONObject;  class JSONParser {      static String getPrice(String s) throws JSONException {         String price;         JSONObject obj = new JSONObject(s);         JSONObject pairObj = obj.getJSONObject("btc_usd");         price = pairObj.getString("last");          return price;     } } 

Вот так выглядит описанный выше виджет:

Скриншот

image

Полный исходный код доступен здесь: github.com/hakeydotom/BTCPriceWidget
ссылка на оригинал статьи https://habrahabr.ru/post/318482/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *