V Androide nemáte len aplikácie pozostávajúce z aktivít, do ktorých môžete ďubať prstom. A nejde len o prípady, keď je aktivita pauznutá (lebo je prekrytá inou aktivitou) alebo zničená (lebo sa vyčerpali systémové zdroje). Popri nich si radostne na pozadí cvrliká množstvo služieb, ktoré nepotrebujú žiadne používateľské rozhranie: budíky, ktoré čakajú na moment, keď vás o 6:30 vyženú z postele; kontrola (G)mailu, a mnoho iných, ktoré nevnímate dovtedy, kým o sebe nedajú vedieť notifikáciou.
Služby (services) sú presne tým komponentom, ktorý môže bežať na pozadí, uskutočňovať dlhotrvajúce operácie a to bez toho, aby potrebovali kamaráta v podobe aktivity, teda bez toho, aby vyžadovali používateľské rozhranie.
Dnes si ukážeme najjednoduchšiu možnosť: službu, ktorá na pozadí pochrústa dáta a keď je hotová, tak upozorní používateľa pomocou systémovej notifikácie.
Budeme potrebovať:
- 1 ks triedy pre samotnú službu
- 1 ks triedy pre aktivitu (na spúšťanie služby)
- 1 objekt pre zobrazovanie notifikácií
Služba IntentService
Mohli by sme si vymyslieť kadejaké príklady: sťahovač súborov, uploader fotiek… alebo akúkoľvek inú službu, ktorá sa vykoná raz a potom skončí. Urobíme to najstupídnejšie ako môžeme. Naša služba bude predstierať sťahovanie: dvadsaťkrát vypíše do systémového logu hlášku s progresom a … to bude všetko. (Stupídnosť preto, aby sme nestrávili viac času písaním sťahovača než chápaním samotného princípu.)
Vyjdeme pri tom z triedy IntentService
, ktorá sa hodí presne na prípad, keď:
- úloha na pozadí má zbehnúť len raz, čiže nejde o dlhotrvajúcu operáciu v duchu chatu, kde sa dokola kontroluje prítomnosť nových správ na serveri
- úloha má zbehnúť v samostatnom vlákne, aby nezdržiavala hlavné vlákno aplikácie
- nepotrebuje priebežne aktualizovať GUI, samozrejme okrem jednorazovej notifikácie
Vytvorenie služby
- Vytvoríme triedu
DownloadService
, ktorá oddedí odIntentService
. - Vytvoríme implicitný konštruktor.
- Prekryjeme
onHandleIntent()
- Zavedieme ju do manifestu.
Vytvoriť službu môžeme klikaním:
File | New
New Android Object
Nastavenie vlastností
Dajme si rovno kód:
package sk.upjs.ics.novotnyr.downloadr;
import java.util.concurrent.TimeUnit;
import android.app.IntentService;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
public class DownloadService extends IntentService {
public static final String WORKER_THREAD_NAME = "DownloadServiceThread";
private static final String LOG_TAG = "DownloadService";
public DownloadService() {
super(WORKER_THREAD_NAME);
}
@Override
protected void onHandleIntent(Intent intent) {
Log.i(LOG_TAG, "Downloaded a file.");
}
}
Implicitný konštruktor
Implicitný konštruktor musí byť prítomný: bez neho sa nebude dať vytvoriť inštancia služby. V tomto prípade sme povinní zavolať rodičovský konštruktor s reťazcom popisujúcim názov vlákna, v ktorom služba pobeží. (Je to kvôli ladiacim dôvodom.)
Metóda onHandleIntent()
Jadrom je metóda onHandleIntent()
, v ktorej sa nachádza samotný kód služby, ktorý má bežať na pozadí. Zatiaľ sa toho veľa neudeje: jeden výpis do logu, ktorý časom vylepšíme.
Zavedenie do manifestu
Ak ste službu vytvorili pomocou sprievodcu, tento krok môžete preskočiť. V opačnom, prípade do elementu <application>
v manifeste zaveďme nový element <service>
s názvom triedy našej služby:
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
...
<service android:name=".DownloadService" />
</application>
Spustenie služby
Službu odpáľme z aktivity, kde vytvoríme jedno tlačidlo. SPUSTI SLUŽBU!
bude jeho popiskom.
Spustenie dosiahneme zaslaním správy, teda intentu, podobne ako keby sme chceli spustiť novú aktivitu. Vytvoríme teda nový intent s názvom triedy služby:
Intent downloadIntent = new Intent(this, DownloadService.class);
a pomocou startService()
ju naštartujeme.
V príklade máme metódu runServiceButtonClick()
, ktorá sa zavolá po kliknutí na tlačidlo (prepojenie je vďaka atribútu android:onClick="runServiceButtonClick"
v XML layoute aktivity):
public class MainActivity extends Activity {
...
public void runServiceButtonClick(View view) {
Intent downloadIntent = new Intent(this, DownloadService.class);
startService(downloadIntent);
}
}
Služba sa spustí na pozadí, vypíše do logu jeden riadok a dobehne.
Dlhotrvajúca služba
Jedna hláška do logu by naozaj nemusela mať samostatnú službu. Napravme to: vyrobme simuláciu sťahovania.
Namiesto skutočného sťahovania vypíše služba do logu hlášku o progrese a potom zaspí na sto milisekúnd.
@Override
protected void onHandleIntent(Intent intent) {
String url = "http://developer.android.com/images/brand/Android_Robot_100.png";
download(url);
}
private boolean download(String url) {
boolean isSuccessfullyCompleted = true;
for(long i = 1; i <= 100; i = i + 5) {
try {
TimeUnit.MILLISECONDS.sleep(100);
Log.i(LOG_TAG, "Download: " + i + "%");
} catch (InterruptedException e) {
isSuccessfullyCompleted = false;
break;
}
}
return isSuccessfullyCompleted;
}
Ako funguje IntentService
?
Po naštartovaní nášho IntentService
sa v systéme vytvorí jediná inštancia, ktorá spustí samostatné vlákno (tzv. worker thread), ktorý bude čakať na prichádzajúce intenty. Každý prichádzajúci intent (teda každý intent, ktorý aktivita poslala cez startService()
) sa zaradí do frontu a postupne sa spracováva v metóde onHandleIntent()
. Skúste zbesilo poklikať na tlačidlo: tri kliky spôsobia odoslanie troch intentov, ktoré sa spracujú postupne a teda uvidíte tri behy sťahovaní.
Ak náš intent service spracuje všetky intenty čakajúce vo fronte, automaticky sa ukončí.
Chcete to vidieť v akcii?
Prekrývanie metód životného cyklu
Ak prekryjeme onCreate()
, vieme reagovať na situáciu, keď sa služba vytvorí:
@Override
public void onCreate() {
super.onCreate();
Log.i(LOG_TAG, "Download service created");
}
Takisto vieme reagovať na moment, keď sa služba plánuje zničiť, či už preto, že dobehla, alebo preto, že sa v systéme minuli zdroje a treba ju odstreliť:
@Override
public void onDestroy() {
Log.i(LOG_TAG, "Download service destroyed");
super.onDestroy();
}
V oboch prípadoch nezabúdame volať rodičovské metódy!
Skúste si všakovaké kombinácie: napr. jeden klik na tlačidlo, päť sekúnd pauza a dva kliky. Služba sa vytvorí, vykoná sťahovanie a zničí sa a po druhom kliku sa opäť vytvorí, vykoná sťahovanie, potom druhé sťahovanie a následne sa zničí.
Notifikácie: indikácia, že služba beží!
Služba naozaj nepotrebuje používateľské rozhranie v podobe ťažkotonážnej aktivity. Čo chceme robiť v prípade sťahovania? Obdivovať progressbar? Napriek tomu je často vhodné indikovať, že sa niečo na pozadí naozaj deje: nie je nič horšie než naspúšťať stopäťdesiat skrytých služieb, ktoré zožerú všetku baterku a nechať používateľa sa veľmi diviť, čo sa to všetko s jeho mobilom deje.
Bokom: chcete zažiť chvíľku šoku? Otvorte si vo svojom Androide zoznam spustených služieb. Na Androide 2.3 v Settings | Applications | Running Services uvidíte zoznam bežiacich procesov a serviceov.
Ale späť k … notifikáciám.
Tie sú výbornou možnosťou, ako nemať GUI a zároveň indikovať progres.
Budeme potrebovať:
- Získať inštanciu
NotificationManager
a, čo je správca notifikácií zodpovedný za ich zobrazovanie, skrývanie a obsluhu udalostí. - Vytvorenie inštancie
NotificationCompat.Builder
, ktorý vybuduje notifikáciuNotification
. - Zobrazenie vybudovanej notifikácie pomocou
NotificationManager
a.
Celý kód
Do služby hoďme pomocnú metódu, ktorá zobrazí notifikáciu:
private void notifyCompleted(boolean isSuccessful) {
String text = isSuccessful ? "Download completed" : "Download failed";
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
Notification notification = new NotificationCompat.Builder(getApplicationContext())
.setContentTitle("Download is completed")
.setContentText(text)
.setContentIntent(getEmptyNotificationContentIntent())
.setTicker(text)
.setDefaults(Notification.DEFAULT_SOUND)
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_launcher)
.build();
String tag = Long.toString(System.currentTimeMillis());
notificationManager.notify(tag, DOWNLOAD_NOTIFICATION_COMPLETED_ID, notification);
}
Rozoberme si postupne túto metódu.
Získanie NotificationManager
a
Získanie manažéra notifikácií je jednoduché: ide o systémovú službu:
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
Vybudovanie notifikácie
Budovanie objektu notifikácie záleží od verzie Androida. Dnes je najlepšie využiť NotificationCompat.Builder
z knižnice Android Support Library, ktorý zabezpečí kompatibilitu na starých i novších verziách Androidu.
Táto trieda umožňuje nastaviť všakovaké vlastnosti notifikácie:
Notification notification = new NotificationCompat.Builder(getApplicationContext())
.setContentTitle("Download is completed")
.setContentText(text)
.setContentIntent(getEmptyNotificationContentIntent())
.setTicker(text)
.setDefaults(Notification.DEFAULT_SOUND)
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_launcher)
.build();
Postupne nastavujeme:
- veľký nadpis v notifikácii (content title)
- podnadpis v notifikácii (content text)
- intent, ktorý sa má vykonať po kliknutí na notifikáciu (content intent). O tomto porozprávame ešte nižšie.
- text, ktorý sa zjaví, ak sa notifikácia zjaví po prvý krát (ticker)
- ďalšie implicitné vlastnosti: v tomto prípade implicitný zvuk notifikácie
- auto cancel, teda zrušenie notifikácie po kliknutí na ňu
- a malú ikonku.
Nezabudnime po vybudovaní získať objekt notifikácie pomocou metódy build()
.
Content Intent — co zrobic po kliknutí?
Notifikácie nemusia slúžiť len na zobrazovanie informácií. Neraz sa čaká, že po kliknutí na ne sa spustí ďalšia aktivita: ak príde notifikácia o SMSke, asi si tú správu chcete prečítať; ak sa zjaví notifikácia o neprijatom hovore, chcete možno zavolať danému človeku.
Na to slúži content intent: jednoducho deklarujete intent, ktorý sa odošle po kliknutí na správu.
To však nie je náš prípad: u nás sa na content intent vykašleme. Ak používateľ klikne na notifikáciu, nestane sa nič špeciálne.
Content intent však nemôžeme vynechať: Android sa začne sťažovať výnimkou. Na vytvorenie prázdneho intentu poslúži pomocná metóda:
private PendingIntent getEmptyNotificationContentIntent() {
int REQUEST_CODE = 0;
int NO_FLASG = 0;
PendingIntent contentIntent = PendingIntent.getActivity(getApplicationContext(), REQUEST_CODE, new Intent(), NO_FLASG);
return contentIntent;
}
Pending intent predstavuje akéhosi prostredníka, ktorý umožní odoslať intent s takými právami a za rovnakých okolností, ako keby to robila samotná aplikácia, a to i vtedy, ak príslušná aktivita už/ešte nebeží. (Nezabudnite, pokojne sa môže stať, že download beží, i keď GUI aktivity neexistuje.).
Vytvoríme tak inštanciu pending intentu nad prázdnym intentom (new Intent()
), ktorého vlastníkom je aplikácia Downloadr
(získali sme ju cez getApplicationContext()
) s implicitnými nulovými hodnotami v ostatných parametroch.
Zobrazenie notifikácie
Toto je jednoduché:
notificationManager.notify(tag, DOWNLOAD_NOTIFICATION_COMPLETED_ID, notification);
Metóda notify()
potrebuje tri parametre: dvojicu identifikujúcu konkrétnu notifikáciu a samotný objekt notifikácie.
Každá notifikácia je totiž jednoznačne identifikovaná tagom (String) a IDčkom), ktoré je jednoznačné pre celú aplikáciu.
Inými slovami, ID identifikuje aplikáciu a tag rozpoznáva konkrétnu notifikáciu. Prvý deklarujeme ako konštantu (napr. nulu) a druhý získame napr. zo systémového času:
String tag = Long.toString(System.currentTimeMillis());
notificationManager.notify(tag, DOWNLOAD_NOTIFICATION_COMPLETED_ID, notification);
Len bokom: v metóde notify()
môžeme tag vynechať. V takom prípade každá notifikácia z aplikácie nahradí predošlú zobrazenú notifikáciu s rovnakým ID. To sa dá veľmi vhodne použiť pri aktualizácii notifikácie.
Celý kód, verzia 2.0
package sk.upjs.ics.novotnyr.downloadr;
import java.util.concurrent.TimeUnit;
import android.app.IntentService;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
public class DownloadService extends IntentService {
public static final String WORKER_THREAD_NAME = "DownloadServiceThread";
private static final int DOWNLOAD_NOTIFICATION_COMPLETED_ID = 0;
private static final String LOG_TAG = "DownloadService";
public DownloadService() {
super(WORKER_THREAD_NAME);
}
@Override
public void onCreate() {
super.onCreate();
Log.i(LOG_TAG, "Download service created");
}
@Override
public void onDestroy() {
Log.i(LOG_TAG, "Download service destroyed");
super.onDestroy();
}
@Override
protected void onHandleIntent(Intent intent) {
String url = "http://developer.android.com/images/brand/Android_Robot_100.png";
download(url);
}
private boolean download(String url) {
boolean isSuccessfullyCompleted = true;
for(long i = 1; i <= 100; i = i + 5) {
try {
TimeUnit.MILLISECONDS.sleep(100);
Log.i(LOG_TAG, "Download: " + i + "%");
} catch (InterruptedException e) {
isSuccessfullyCompleted = false;
break;
}
}
return isSuccessfullyCompleted;
}
private void notifyCompleted(boolean isSuccessful) {
String text = isSuccessful ? "Download completed" : "Download failed";
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
Notification notification = new NotificationCompat.Builder(getApplicationContext())
.setContentTitle("Download is completed")
.setContentText(text)
.setContentIntent(getEmptyNotificationContentIntent())
.setTicker(text)
.setDefaults(Notification.DEFAULT_SOUND)
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_launcher)
.build();
String tag = Long.toString(System.currentTimeMillis());
notificationManager.notify(tag, DOWNLOAD_NOTIFICATION_COMPLETED_ID, notification);
}
private PendingIntent getEmptyNotificationContentIntent() {
int REQUEST_CODE = 0;
int NO_FLASG = 0;
PendingIntent contentIntent = PendingIntent.getActivity(getApplicationContext(), REQUEST_CODE, new Intent(), NO_FLASG);
return contentIntent;
}
}
Odovzdávanie správ od aktivity k servicu
Aktivita a service medzi sebou môžu radostne švitoriť. Ukážme si najjednoduchší príklad: aktivita pošle servicu URL adresu, ktorá sa má stiahnuť.
Áno, hádate správne, použijeme na to extras v intente.
Intent downloadIntent = new Intent(this, DownloadService.class);
String url = "http://developer.android.com/images/brand/Android_Robot_100.png";
downloadIntent.putExtra(DownloadService.INTENT_EXTRA_DOWNLOAD_URL, url);
startService(downloadIntent);
V service si vyzdvihneme URL veľmi jednoducho:
@Override
protected void onHandleIntent(Intent intent) {
String downloadUrl = intent.getStringExtra(INTENT_EXTRA_DOWNLOAD_URL);;
notifyCompleted(download(downloadUrl));
}
Komunikácia môže byť omnoho zložitejšia (napr. ak chce service posielať správy naspäť do aktivity, ale o tom niekedy nabudúce).
Výsledný projekt
Stiahnite si výsledný projekt ako ZIP.
Zdroje
- Services API Guide @ developer.android.com
- Notification Design Patterns @ developer.android.com
- Notification API Guide @ developer.android.com
- Android Fundamentals: Downloading Data With Services @ mobile.tutsplus.com