Sensorki – widget – #5

This entry is part 6 of 10 in the series Sensorki

Przebijanie się przez kilka ekranów żeby znaleźć aplikacje, a potem jeszcze trzeba uruchomić, no i poczekać aż się załaduje, nie jest fajne, jak chce się na szybko sprawdzić jaka jest temperatura. Jest na to jedna rada – zrobię widget 🙂

Wykorzystałam Android Studio do tego, żeby wykonało za mnie część niezbędnej pracy i tutorial do tworzenia i obsługi widgetów z Udacity (link). Klikam prawym na projekcie -> New -> Widget -> App Widget. Android studio wygenerowało mi 5 plików i zmieniło 3. 

Praktycznie od tego momentu widget jest już gotowy do użycia. Wprawdzie nie wyświetla nic poza nazwą 'EXAMPLE’, ale już można go testować 😉

A co zmieniło się w plikach? Pomińmy dimens.xml i strings.xml i skupmy się na tym co naprawdę ma znaczenie.

AndroidManifest – zarejstrowanie widgetu: 

<receiver android:name=".SensorWidgetProvider">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/sensor_widget_provider_info" />
</receiver>
AndroidManifest.xml

sensor_widget_provider_info.xml – domyślne ustawienia dla widgetu takie jak minimalna wysokość i szerokość, domyślny layout i domyślny czas odświeżania widgetu (niestety nie można w ten sposób odświeżać częściej niż raz na 30 minut). Tak wygląda u mnie po modyfikacjach.

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialKeyguardLayout="@layout/sensor_widget_provider"
android:initialLayout="@layout/sensor_widget_provider"
android:minHeight="40dp"
android:minWidth="80dp"
android:previewImage="@drawable/ic_low_temperature"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="1800000"
android:widgetCategory="home_screen" />
sensor_widget_provider_info.xml

sensor_widget_provider.xml – zawiera layout dla widgetu. Chcę żeby wyświetlała się tam temperatura i godzina odczytu. Do tego chciałabym zachować kolorystykę z aplikacji. Dorzucę też ikonkę odświeżenia żebym mogła w dowolnym momencie pobrać dane. Ostatecznie ląduję z RelativeLayoutem zawierającym kilka textView i klikalny obrazek do odświeżenia temperatury.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/sensor_widget">
<TextView
android:id="@+id/appwidget_text"
android:text="@string/appwidget_text" (…)/>
<ImageView
android:id="@+id/refresh_ic"
android:src="@drawable/ic_rotate" (…)/>
<TextView android:text="@string/temp_outside" (…)/>
<TextView android:id="@+id/widget_timestamp_tv" (…)/>
</RelativeLayout>
widget layout

Jest jeszcze plik SensorWidgetProvider, który ma w sobie metody, do obsługi widgetu. Tu nie będzie tak prosto, bo do odświeżenia danych tutorial kazał mi użyć serwisu, więc tworzę SensorIntentService. No i teraz trzeba trochę pochodzić w tę i nazad pomiędzy servicem i providerem w celu odświeżenia i ustawienia danych na widgecie. Tak mniej więcej wygląda to w praktyce 😉

Interaction between provider and service

No i teraz można po kolei 😉

OnUpdate jest wywoływany w momencie dodania widgetu do ekranu, a także przy odświeżaniu widgetu. W moim kodzie to przekazanie sterowania do service’u:

@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
SensorIntentService.startActionUpdateWidgets(context);
}
onUpdate

Uruchamian service’u w tle z akcją 'UPDATE_WIDGET’. 

public static void startActionUpdateWidgets(Context context) {
Intent intent = new Intent(context, SensorIntentService.class);
intent.setAction(ACTION_UPDATE_WIDGET);
context.startService(intent);
}
startActionUpdateWidgets

Później w metodzie onHandleIntent odpalam odpowiednią akcję z service’u. U mnie w tym wypadku obie są takie same 😉

@Override
protected void onHandleIntent(@Nullable Intent intent) {
if (intent != null) {
final String action = intent.getAction();
if (ACTION_UPDATE_WIDGET.equals(action) || ACTION_REFRESH_WIDGET.equals(action)) {
refreshWidget();
}
}
}
onHandleIntent

No i najważniejsza akcja: refreshWidget. Metoda, która pobiera dane z serwera i przekazuje z powrotem do providera. Jest prawie, że identyczna z metodą, która wypełnia dane w kafelkach recyclerView. 

private void refreshWidget() {
final Context mContext = this;
WeatherService weatherService = WeatherService.retrofit.create(WeatherService.class);
final Call<JsonObject> call = weatherService.loadData();
call.enqueue(new Callback<JsonObject>() {
@Override
public void onResponse(@NonNull Call<JsonObject> call, @NonNull Response<JsonObject> response) {
JsonObject body = response.body();
if (body != null) {
Sensor sensor = createSensorsFromJsonObject(body);
if (sensor != null) {
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
int[] widgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(mContext, SensorWidgetProvider.class));
SensorWidgetProvider.updateSensorWidgets(mContext, appWidgetManager, sensor, widgetIds);
}
}
}
@Override
public void onFailure(@NonNull Call<JsonObject> call, @NonNull Throwable t) {
Log.e("LOAD_DATA", t.getLocalizedMessage());
Toast.makeText(mContext, R.string.connection_error, Toast.LENGTH_LONG).show();
}
});
}
refreshWidget

Prawie – tutaj przekazujemy dane z powrotem do providera, który trzyma informację o polach tekstowych. Zdecydowałam się na przekazywanie do metody całego obiektu sensora zamiast dwóch pól z niego. Wygląda to lepiej niż przekazywanie dwóch stringów, a w przyszłości może i większej ilości parametrów. 

public static void updateSensorWidgets(Context context, AppWidgetManager appWidgetManager, Sensor sensor, int[] appWidgetIds) {
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, sensor, appWidgetId);
}
}

Pętla po utworzonych widgetach, żeby wszystkie zostały odświeżone.

A teraz najważniejsza metoda, która jest odpowiedzialna za wszystko co dzieje się w widgecie updateAppWidget. Dzieją się tam 4 rzeczy:

Tworzę remoteView na podstawie layoutu zawierającego wygląd layoutu. Przypisuję do każego z textView wartości z obiektu sensora.

RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.sensor_widget_provider);
views.setTextViewText(R.id.appwidget_text, sensor.getTemperature());
views.setTextViewText(R.id.widget_timestamp_tv, sensor.getTimestamp());

Kolejne linijki odpowiadają za otworzenie aplikacji po kliknięciu na widget.

Intent intent = new Intent(context, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
views.setOnClickPendingIntent(R.id.sensor_widget, pendingIntent);

Podpinam też odpalenie odświeżania z service’u po kliknięciu na obrazek ze strzałką:

Intent refreshIntent = new Intent(context, SensorIntentService.class);
refreshIntent.setAction(SensorIntentService.ACTION_REFRESH_WIDGET);
PendingIntent refreshPendingIntent = PendingIntent.getService(
context,
0,
refreshIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
views.setOnClickPendingIntent(R.id.refresh_ic, refreshPendingIntent);

I na koniec powiadomienie widget managera, że trzeba odświeżyć widget.

appWidgetManager.updateAppWidget(appWidgetId, views);

A tak wygląda efekt końcowy:

emulator
Series Navigation<< Sensorki – wskaźnik baterii – #4Sensorki – groovy/spring boot demo serwer – #6 >>

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

This site uses Akismet to reduce spam. Learn how your comment data is processed.