Android: sťahovanie súborov pomocou services a notifikácií

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

  1. Vytvoríme triedu DownloadService, ktorá oddedí od IntentService.
  2. Vytvoríme implicitný konštruktor.
  3. Prekryjeme onHandleIntent()
  4. Zavedieme ju do manifestu.

Vytvoriť službu môžeme klikaním:

File | New

new-service-1

New Android Object

new-service-2

Nastavenie vlastností

new-service-3

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;
}

logcat-service

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.

notification

Budeme potrebovať:

  1. Získať inštanciu NotificationManagera, čo je správca notifikácií zodpovedný za ich zobrazovanie, skrývanie a obsluhu udalostí.
  2. Vytvorenie inštancie NotificationCompat.Builder, ktorý vybuduje notifikáciu Notification.
  3. Zobrazenie vybudovanej notifikácie pomocou NotificationManagera.

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 NotificationManagera

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

Pridaj komentár

Vaša e-mailová adresa nebude zverejnená. Vyžadované polia sú označené *