Vyvíjíme pro Android: Notifikace, broadcast receivery a Internet

V dnešním nabitém dílu seriálu Vyvíjíme pro Android se naučíme pracovat s notifikacemi (včetně Jelly Bean novinek), broadcast receivery a Internetem, a to všechno při vytváření aplikace hlídající články na Zdrojáku.

Seriál: Vyvíjíme pro Android (14 dílů)

  1. Vyvíjíme pro Android: Začínáme 15.6.2012
  2. Vyvíjíme pro Android: První krůčky 22.6.2012
  3. Vyvíjíme pro Android: Suroviny, Intenty a jednotky 29.6.2012
  4. Vyvíjíme pro Android: Bližší pohled na pohledy – 1. díl 13.7.2012
  5. Vyvíjíme pro Android: Bližší pohled na pohledy – 2. díl 13.7.2012
  6. Vyvíjíme pro Android: Fragmenty a SQLite databáze 20.7.2012
  7. Vyvíjíme pro Android: Preference, menu a vlastní Adapter 27.7.2012
  8. Vyvíjíme pro Android: Intenty, intent filtry a permissions 3.8.2012
  9. Vyvíjíme pro Android: Content providery 10.8.2012
  10. Vyvíjíme pro Android: Dialogy a activity 17.8.2012
  11. Vyvíjíme pro Android: Stylování a design 24.8.2012
  12. Vyvíjíme pro Android: Notifikace, broadcast receivery a Internet 31.8.2012
  13. Vyvíjíme pro Android: Nahraváme aplikaci na Google Play Store 7.9.2012
  14. Vyvíjíme pro Android: Epilog 14.9.2012

minulém díle jsme se věnovali design guidelines a stylování. Dnes se naučíme pracovat s notifikacemi (a to včetně novinek z Jelly Bean, ale přitom zůstaneme zpětně kompatibilní), broadcast receivery a Internetem. Naučíme se spouštět nějaký kód každý den ve stejnou dobu, procvičíme si content providery a vytvoříme si vlastní Adapter.

To všechno na jedné ukázkové aplikaci, jíž jsem nazval Nové články na Zdrojáku. Ta každý den po půlnoci stáhne RSS článků Zdrojáku, zjistí, jsou-li nějaké nové, a pokud ano, zobrazí notifikaci. Kromě toho ještě bude obsahovat ListActivity se seznamem článků, po kliknutí na některý z nich se článek otevře v prohlížeči.

Dnes je článek opravdu hodně nabitý. Přemýšlel jsem, jestli vynechat části kódu, ale pak jsem si řekl, že jednak už jste na Androidu dost zkušení, jednak může být docela zajímavé vyvíjet aplikaci, která sleduje nějaký praktický účel, a zobrazení jen nových věcí by tomu uškodilo. Takže nové věci samozřejmě vysvětlím, ale staré a známé (anebo jednoduše nalezitelné v dokumentaci) nechám víceméně bez komentáře.

Broadcast receivery

Než ale začneme pracovat, bylo by dobré seznámit se s nástroji. Začneme broadcast receivery.

Díky broadcast receiverům můžeme provést nějakou akci třeba když se změní stav připojení k internetu. Nebo když se zapne displej. Nebo si můžeme vytvořit nějakou vlastní událost, tu předat frameworku a on upozorní všechny broadcast receivery, které deklarovaly, že se o danou událost zajímají. Pokud jste pracovali s nějakým event-driven jazykem (třeba JavaScript), je vám to určitě povědomé.

Samotný broadcast receiver dědí od třídy BroadcastReceiver (na odkazované adrese je i lidsky čitelná dokumentace, nejen API reference) a implementuje metodu onReceive(Context context, Intent intent). Ta se spustí vždy, když se broadcast receiver aktivuje.

Broadcast receiver se aktivuje tak, že někdo vytvoří obyčejný Intent s obyčejnou action (a čímkoli dalším), který předá metodě Context.sendBroadcast() (nebo sendOrderedBroadcast() či sendStickyBroadcast(), kterým se věnovat nebudeme). O zbytek se postará framework.

A jak se o to postará? Jak pozná, které broadcast receivery upozornit? Broadcast receiver musíte zapsat v manifestu, kde stejně jako <activity> může obsahovat elementy <intent-filter>. Pokud Intent filtrem projde, broadcast se spustí (a ten Intent dostane jako parametr).

Ve skutečnosti v manifestu zapsán být nemusí. Celkem běžné využití broadcast receiverů je i v rámci Activity, kde sledují například změny nějakých vnějších faktorů a podle toho se Activity pak chová. Tehdy je možné vytvořit objekt IntentFilter a společně s instancí BroadcastReceiver-u ho předat metodě registerReceiver(). Při zničení Activity se musí receiver zase odregistrovat ( unregisterReceiver()). I to dnes použijeme.

Notifikace

Stránka dokumentace věnující se notifikacím je zoufale zastaralá, takže na ni ani nebudu odkazovat. Odkážu ale na design guidelines a hlavně na icon design guidelines, část věnující se statusbarovým ikonám, kde je výborně vysvětleno, které všechny ikony musíte pro notifikaci vytvořit, aby to vypadalo dobře na všech verzích Androidu (to my dnes zanedbáme).

Nikdy byste neměli z nějakého procesu běžícího na pozadí (ať už broadcast receiveru, service nebo čehokoli jiného) přímo spouštět nějakou Activity. To by uživatele pěkně naštvalo. Místo toho vyrobte notifikaci, on se o všem dozví a Activity spustí, až bude sám chtít (nebo nespustí, když chtít nebude).

Notifikace se postupně s Androidem měnily. Měnil se vzhled ikon, trochu se měnil i vzhled notifikací, hodně se změnil způsob jejich vytváření (o čemž článek v dokumentaci neví), který adoptovala i support library. Díky ní můžeme jednoduše vytvořit notifikaci, která používá novinky z Jelly Bean (což je verze, která přinesla zdaleka největší změnu v notifikacích), ale přitom bude fungovat i na Androidu 1.6.

Dejte si pozor na to, abyste měli nejnovější verzi support library. Některé věci, které budeme používat, se do ní totiž dostaly opravdu nedávno. S tím ale bohužel přichází i to, že v ní jsou bugy. Nezkoušel jsem, jestli je to v mé verzi už opravené, vzhledem ke stáří ticketu bych řekl, že ne. Ale přesto vám to chci ukázat, takže se smíříme s tím, že na Androidu 3.2 aplikace nejspíš nebude fungovat. A budeme čekat, než vyjde nová verze support library.

Notifikaci vytvoříme pomocí třídy NotificationCompat.Builder, metodou build() získáme instanci Notification. Tu potom zobrazíme pomocí  NotificationManager-u:

Notification n;
NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
n = builder
    .setContentTitle(R.string.title)
    .setContentText(R.string.text)
    .setSmallIcon(R.drawable.notification)
    .build();

NotificationManager notificationManager =
    (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

notificationManager.notify(NOTIFICATION_ID, n);

Práce s Internetem

Pro práci s Internetem potřebujeme permission android.permission.INTERNET.

Nikdy nesmíme pracovat na UI vlákně. V našem případě nám bude stačit obyčejný Thread, ale často se hodí androidí AsyncTask. AsyncTask (v dokumentaci na stránce Processes and threads) usnadňuje komunikaci mezi UI a worker vláknem, posílání informací o progressu a formalizuje „životní cyklus” pracovního vlákna – před spuštěním, pak běží samotné vlákno, které může zveřejňovat svůj pokrok, po doběhnutí se pracuje s daty zase na UI vlákně. Bohužel dnes na AsyncTask není místo, snad jindy.

Pro samotné vytváření a volání nějakých požadavků (requestůdoporučují třídu HttpURLConnection, která má však různé problémy, takže bychom zbytečně plýtvali energií na workaroundy. Proto dnes použijeme starý dobrý  DefaultHttpClient:

String responseStr = null;
try {
    DefaultHttpClient httpClient = new DefaultHttpClient();
    HttpGet get = new HttpGet(url);

    HttpResponse httpResponse = httpClient.execute(get);
    HttpEntity httpEntity = httpResponse.getEntity();
    responseStr = EntityUtils.toString(httpEntity);

} catch (UnsupportedEncodingException e) {
} catch (ClientProtocolException e) {
} catch (IOException e) {
}

AlarmManager

Potřebujete spouštět nějakou akci pravidelně třeba každý den v určitou hodinu? Na to slouží AlarmManager. Její metoda setRepeating(int type, long triggerAtMillis, long intervalMillis, PendingIntent operation) přebírá následující argumenty:

type je jedna ze čtyř konstant AlarmManager.ELAPSED_REALTIME, AlarmManager.ELAPSED_REALTIME_WAKEUP, AlarmManager.RTC a AlarmManager.RTC_WAKEUP. Rozdíl mezi ELAPSED_REALTIME a RTC je v tom, že u ELAPSED_REALTIME  se čas předaný triggerAtMillis počítá od nabootování zařízení, RTC přijímá Unixovou časovou známku (počet milisekund od půlnoci 1. 1. 1970). Rozdíl mezi WAKEUP a ne- WAKEUP variantami je ten, že s WAKEUP  proběhne akce přibližně přesně v předanou dobu, a pokud je potřeba, zařízení je probuzeno, zatímco varianta bez WAKEUP se v případě, že telefon spinká, odloží a provede se až tehdy, když se telefon probudí.

triggerAtMillis je timestamp, kdy se akce spustí poprvé.

intervalMillis je časové rozmezí jednotlivých opakování akce (v milisekundách). AlarmManager nabízí některé předdefinované konstanty.

operation  reprezentuje samotnou akci a povíme si o tom později.

AlarmManager získáte jako systémovou službu pomocí  ALARM_SERVICE.

// Spustí se půl hodiny po nejbližší další půlnoci a pak už každý den ve stejnou dobu.
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MINUTE, 30);
calendar.set(Calendar.HOUR, 0);
calendar.set(Calendar.AM_PM, Calendar.AM);
calendar.add(Calendar.DAY_OF_MONTH, 1);

((AlarmManager) getSystemService(Context.ALARM_SERVICE)).setRepeating(
        AlarmManager.RTC, calendar.getTimeInMillis(), AlarmManager.INTERVAL_DAY, pi);

PendingIntent

Asi jste si všimli proměnné pi v předchozí ukázce. Není to π, jde o zkratku slova pending intent.

O PendingIntent-ech se dá přemýšlet v podstatě jako o Intentech, které mají navíc informaci o tom, jakou metodou se mají spustit (jestli startActivity(), sendBroadcast() nebo třeba  startService().

Intent intent = new Intent(this, UpdateReceiver.class);
intent.putExtra("someExtra", true);
PendingIntent pi = PendingIntent.getBroadcast(this, 0, intent, 0);

Tahle ukážka vytvoří PendingIntent pro nějaký BroadcastReceiver. Třídu UpdateReceiver si za chvíli implementujeme, není vestavěná, v dokumentaci ji nehledejte.

A jdeme programovat!

Nejdřív ale asi stojí za to si promyslet, co vlastně chceme dělat (to už víceméně máme) a jak to chceme dělat.

Určitě bude potřeba třída, která stáhne RSS Zdrojáku z Internetu. Potom nějaká třída, která ho rozparsuje a vytáhne z něj potřebné věci. Abychom neztráceli čas pro náš účel nedůležitými věcmi, možná obětujeme čistotu kódu, testovatelnost a efektivitu, ale parsovací třída vrátí pole objektů ContentValues, pro každý článek jeden objekt, všechno připravené na vložení do content provideru. Třída, která stahuje data, na ně (opět asi ne úplně nejčistší návrh) rovnou zavolá parser a pole vrácené parserem předá content provideru. Pro parser použijeme DOM ( org.w3c.dom) – nějaký proudový parser by byl určitě efektivnější, ale kód s DOMem se nám minimálně bude lépe číst. A vzhledem k tomu, že Zdroják má v RSS deset posledních článků, výkonnostní nebo paměťové problémy nám DOM určitě působit nebude.

V souladu s principem DRY by asi bylo, abychom vytvořili obecný RSS parser a potom třídy, které by s jeho výsledky nějak pracovaly. To by ale zabralo spoustu času a vůbec bychom se dnes nedostali k zajímavým věcem, takže parser je přímo na míru Zdrojákovému RSS a i na jiných místech jsme si práci usnadnili.

Ukládání dat vyřešíme pomocí content provideru. U každého článku si kromě dat z RSS budeme pamatovat, jestli ho uživatel přečetl a také, kdy byl stažen do telefonu (kvůli notifikacím). Content provider od stahovače může dostat i články, které už má uložené, a musí si s tím poradit. Vyřešíme to tak, že uložíme pouze články novější než nejnovější uložený. Abychom si to zjednodušili, toto filtrování uděláme jen v metodě bulkInsert(). Protože ale těžko bude zapisovat někdo jiný než my (číst by ale klidně nějaká aplikace mohla. Třeba taková, která dá na plochu widget s počtem nepřečtených článků), můžeme si to dovolit.

Naprostá většina content provideru je ale zkopírována z provideru, který jsme vytvořili v díle Content providery.

Po jakékoli změně dat vypustí content provider vysílání (broadcast) informující o změně dat.

Tomu bude naslouchat mimo jiné i ListActivity, které potom obnoví svá data (jde to i jinak, ale na to není místo, takhle alespoň ukážu broadcast receiver registrovaný v kódu). Ta bude zobrazovat seznam článků na Zdrojáku, a to ne jen titulek, jako třeba Google Reader, ale i perex. V podstatě se bude snažit „vypadat jako” mobilní stránky Zdrojáku. Na to budeme potřebovat vlastní Adapter.

Kromě toho nabídne Activity položku menu na nastavení všech článků jako přečtené, zařídí updatování vždy o půlnoci (pokud je v AlarmManageru stejný PendingIntent, jeho alarm se zruší a nahradí se nově nastaveným. Takže žádný problém s mnohonásobným spouštěním broadcast receiverů) a do SharedPreferences uloží informaci o tom, kdy byla Activity naposledy zobrazena (také info pro notifikaci). Při uložení této informace také vyšle broadcast.

Nakonec zbývají ještě dva broadcast receivery. UpdateReceiver se bude starat o stáhnutí RSS, a předání získaných dat content provideru. Bude volán vždy o půlnoci, ale také vždy, když se změní stav připojení k Internetu (to proto, kdyby o půlnoci připojení nebylo). Pokud však UpdateReceiver už ten den RSS stáhl, nic znovu stahovat nebude.

Úplně nakonec nám zbyl NotificationReceiver. Ten bude naslouchat jednak změnám dat v content provideru a jednak broadcastům od Activity o jejím zobrazení. Vždy při spuštění notifikaci odstraní, ale pokud existuje alespoň jeden článek, který se objevil (stáhl) poté, co byla Activity naposledy spuštěna, zobrazí (jinou) notifikaci znovu. Pokud je takový článek jen jeden, bude v notifikaci jeho titulek, jméno autora a na Jelly Bean se při roztažení notifikace zobrazí perex. Pokud je takových článků víc, zobrazí se v titulku informace, že jsou dostupné nové články, v „nerozvinutém” těle bude věta typu „Máte 7 nepřečtených článků.” a v „rozvinutém” budou titulky maximálně pěti nepřečtených článků. Po kliknutí na notifikaci se zobrazí Activity.

Máme rozmyšleno, jdeme opravdu programovat!

A teď bude hodně Javy, trochu XML a málo češtiny. Všechno už v podstatě víte :).

RSSParser

Třída RSSParser parsuje RSS zdrojáku a vrací ContentValues[]. Kromě samotných parsovacích funkcí budu muset převést datum z elementu <pubdate> na timestamp a také zkrátím jméno autora (z redakce@zdrojak.cz (Zdroják: John Doe)  na John Doe). Protože je metoda getTextContent() až od API 8, vytvořím si vlastní:

Kontrolu, jestli mě aktuální element zajímá a zároveň převedení jeho jména na klíč content provideru vyřeším pomocí  HashMap.

public class RSSParser {

    private String rss;
    private Map<String, String> keys;
    private SimpleDateFormat format;

    public RSSParser(String rss) {
        this.rss = rss;

        format = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss z", Locale.US);

        keys = new HashMap<String, String>();

        keys.put("title", ZdrojakContract.TITLE);
        keys.put("link", ZdrojakContract.LINK);
        keys.put("description", ZdrojakContract.DESCRIPTION);
        keys.put("author", ZdrojakContract.AUTHOR);
        keys.put("pubDate", ZdrojakContract.PUBDATE);
    }

Metoda parse() se stará o samotné parsování a používá některé pomocné metody, jež ukážu za chvilku:

public ContentValues[] parse() throws RSSParserException {
    Document doc = getDOM(rss);

    List list = new ArrayList();

    NodeList items = doc.getElementsByTagName("item");

    Node item;
    ContentValues values;
    NodeList children;
    Node child;
    for (int i = 0; (item = items.item(i)) != null; i++) {
        values = new ContentValues();
        children = item.getChildNodes();

        for (int j = 0; (child = children.item(j)) != null; j++) {
            if (child.getNodeType() == Node.ELEMENT_NODE)
                processNode(values, child);
        }

        list.add(values);
    }

    return list.toArray(new ContentValues[0]);
}

ProcessNode() se volá na každém elementovém uzlu:

private void processNode(ContentValues values, Node node) {
    String key = getKeyForTagName(node.getNodeName());

    if (key == null)
        return;

    String nodeValue = getNodeTextContent(node);
    if (key.equals(ZdrojakContract.PUBDATE)) {
        long pubDate = 0;
        try {
            pubDate = format.parse(nodeValue).getTime();
        } catch (ParseException e) {
            e.printStackTrace();
        }
        values.put(key, pubDate);
    } else if (key.equals(ZdrojakContract.AUTHOR)) {
        String authorName = nodeValue.replaceFirst(
                "redakce@zdrojak\.cz \(Zdroják: (.*)\).*", "$1");
        values.put(key, authorName);
    } else {
        values.put(key, nodeValue);
    }
}

A nakonec tři pomocné funkce:

private Document getDOM(String rss) throws RSSParserException {
    Document doc = null;
    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    try {

        DocumentBuilder db = dbf.newDocumentBuilder();

        InputSource is = new InputSource();
        is.setCharacterStream(new StringReader(rss));
        doc = db.parse(is);
        return doc;

    } catch (ParserConfigurationException e) {
    } catch (SAXException e) {
    } catch (IOException e) {
    }

    throw new RSSParserException();
}

private String getKeyForTagName(String tagName) {
    if (keys.containsKey(tagName))
        return keys.get(tagName);
    else
        return null;
}

private String getNodeTextContent(Node node) {
    String ret = "";

    NodeList children = node.getChildNodes();
    Node child;
    for (int i = 0; (child = children.item(i)) != null; i++) {
        if (child.getNodeType() == Node.TEXT_NODE) {
            if (!ret.equals(""))
                ret += " ";
            ret += child.getNodeValue();
        }
    }

    return ret;
}

Třídu RSSParser jsem vytvářel s ohledem na rychlost (mou, ne její), stručnost a co nejjednodušší srozumitelnost. Doufám, že v opravdu reálném neukázkovém projektu byste si s ní poradili lépe než já zde.

ZdrojakRSSHandler

Přestože třída končí na Handler, není potomkem Handler-u. Místo toho se stará o stažení RSS, jeho předání parseru a pak předání rozparsovaných dat content provideru:

public class ZdrojakRSSHandler {

    private static final String url = "https://www.zdrojak.cz/rss/clanky/";
    private Context ctx;

    public ZdrojakRSSHandler(Context ctx) {
        this.ctx = ctx;
    }

    public int updateData() throws ZdrojakRSSHandlerException, RSSParserException{
        String rss = getStringData();

        RSSParser parser = new RSSParser(rss);

        ContentValues[] values = parser.parse();

        return ctx.getContentResolver().bulkInsert(ZdrojakContract.CONTENT_URI_ARTICLES, values);
    }

    private String getStringData() throws ZdrojakRSSHandlerException {
        String xml = null;
        try {
            DefaultHttpClient httpClient = new DefaultHttpClient();
            HttpGet get = new HttpGet(url);

            HttpResponse httpResponse = httpClient.execute(get);
            HttpEntity httpEntity = httpResponse.getEntity();
            xml = EntityUtils.toString(httpEntity);

            return xml;

        } catch (UnsupportedEncodingException e) {
        } catch (ClientProtocolException e) {
        } catch (IOException e) {
        }

        throw new ZdrojakRSSHandlerException();
    }
}

RSSParserException i ZdrojakRSSHandlerException  jsou potomky Exception, ukazovat je nebudu.

ZdrojakContract

Abychom mohli implementovat content provider, musíme znát contract class. Ta kromě běžných položek nabízí i statickou metodu setViewed(), neboť to může být docela častý úkon a taková pomocná metoda práci jistě usnadní. Navíc jsem do ZdrojakContract přidal i konstanty pro naše broadcast actions.

public class ZdrojakContract {

    public static final String _ID = "_id";
    public static final String TITLE = "title";
    public static final String LINK = "link";
    public static final String DESCRIPTION = "desc";
    public static final String AUTHOR = "author";
    public static final String PUBDATE = "pubdate";
    public static final String VIEWED = "viewed";
    public static final String ADDED = "added";


    public static final String AUTHORITY = "com.example.zdrojaknotifier.provider";
    public static final String ARTICLES = "articles";

    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY);
    public static final Uri CONTENT_URI_ARTICLES = Uri.withAppendedPath(CONTENT_URI, ARTICLES);


    public static final String[] PROJECTION = { _ID, TITLE, LINK, DESCRIPTION, AUTHOR, PUBDATE, VIEWED };

    public static final String DEFAULT_SORT_ORDER = _ID + " DESC";
    public static final String SORT_ORDER_PUBDATE = PUBDATE + " DESC";


    public static final String MIME_TYPE_ITEM = "vnd.android.cursor.item/vnd.com.example.zdrojaknotifier.article";
    public static final String MIME_TYPE_DIR = "vnd.android.cursor.dir/vnd.com.example.zdrojaknotifier.article";

    public static void setViewed(Context ctx, long id, boolean viewed){
        Uri uri = ContentUris.withAppendedId(CONTENT_URI_ARTICLES, id);
        ContentValues values = new ContentValues();
        values.put(VIEWED, viewed);
        ctx.getContentResolver().update(uri, values, null, null);
    }



    // Netýká se content providerů, tohle je Action našeho broadcastu.
    public static final String ACTION_ZDROJAK_UPDATED = "com.example.zdrojaknotifier.action.ZDROJAK_UPDATED";
    public static final String ACTION_LASTVIEW_CHANGED = "com.example.zdrojaknotifier.action.LASTVIEW_CHANGED";
}

ZdrojakProvider

Pokud si pamatujete NotesProvider za článku Content providery, ZdrojakProvider vás rozhodně nepřekvapí. Většinu kódu jsem dokonce z NotesProvider-u zkopíroval, u nových věcí se pozastavím:

Na konstantách, open helperu ani metodě delete() (kterou potřebujeme v podstatě jen pro testovací účely) není nic nového:

public class ZdrojakProvider extends ContentProvider {

    private static final int ARTICLES = 0;
    private static final int ARTICLE_ID = 1;

    private static final String[] MANDATORY_COLUMNS = { ZdrojakContract.TITLE,
            ZdrojakContract.LINK, ZdrojakContract.DESCRIPTION,
            ZdrojakContract.AUTHOR, ZdrojakContract.PUBDATE };

    private static final UriMatcher uriMatcher = new UriMatcher(
            UriMatcher.NO_MATCH);

    static {
        uriMatcher.addURI(ZdrojakContract.AUTHORITY, ZdrojakContract.ARTICLES,
                ARTICLES);
        uriMatcher.addURI(ZdrojakContract.AUTHORITY, ZdrojakContract.ARTICLES
                + "/#", ARTICLE_ID);
    }

    protected static final String DATABASE_NAME = "zdrojak";
    protected static final int DATABASE_VERSION = 2;

    protected static final String TB_NAME = "articles";

    private SQLiteOpenHelper openHelper;

    static class DatabaseHelper extends SQLiteOpenHelper {

        DatabaseHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL("CREATE TABLE " + TB_NAME + " (" + ZdrojakContract._ID
                    + " INTEGER PRIMARY KEY," + ZdrojakContract.TITLE
                    + " TEXT NOT NULL," + ZdrojakContract.LINK
                    + " TEXT NOT NULL," + ZdrojakContract.DESCRIPTION
                    + " TEXT NOT NULL," + ZdrojakContract.AUTHOR
                    + " TEXT NOT NULL," + ZdrojakContract.PUBDATE
                    + " INTEGER NOT NULL," + ZdrojakContract.VIEWED
                    + " BOOLEAN NOT NULL DEFAULT 0, " + ZdrojakContract.ADDED
                    + " INTEGER NOT NULL" + ");");
        }

        /*
         * Bylo by lepší vytvořit pro každou změnu struktury databáze nějaký
         * upgradovací nedestruktivní SQL příkaz, ačkoli tady ztráta dat tolik
         * nebolí.
         */
        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            db.execSQL("DROP TABLE IF EXISTS " + TB_NAME);
            onCreate(db);
        }
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        SQLiteDatabase db = openHelper.getWritableDatabase();

        int count;

        switch (uriMatcher.match(uri)) {
        case ARTICLES:
            count = db.delete(TB_NAME, selection, selectionArgs);
            break;

        case ARTICLE_ID:
            // Musíme vytvořít podmínku WHERE pro dané ID, ale zároveň musíme
            // zachovat případné where předané uživatelem.
            String[] newArgs = createSelectionArgsWithId(selectionArgs, uri
                    .getPathSegments().get(1));
            String where = createSelectionWithId(selection);

            count = db.delete(TB_NAME, where, newArgs);
            break;

        default:
            throw new IllegalArgumentException("Unknown URI " + uri);
        }

        // Vrátíme počet smazaných řádků
        return count;
    }

Metoda insert() volá pomocnou funkci checkMandatoryColumns(), která zjistí přítomnost všech povinných sloupců a případně vyhodí výjimku:

@Override
public Uri insert(Uri uri, ContentValues values) {
    // Neměl by se používat sám o sobě - nekontroluje, jestli tam článek už
    // není a také neobnovuje notifikaci.

    if (uriMatcher.match(uri) != ARTICLES) {
        throw new IllegalArgumentException("Unknown URI " + uri);
    }

    checkMandatoryColumns(values);

    if (!values.containsKey(ZdrojakContract.VIEWED))
        values.put(ZdrojakContract.VIEWED, false);

    values.put(ZdrojakContract.ADDED, new Date().getTime());

    SQLiteDatabase db = openHelper.getWritableDatabase();

    long id = db.insert(TB_NAME, null, values);

    if (id > 0) {
        Uri noteUri = ContentUris.withAppendedId(
                ZdrojakContract.CONTENT_URI_ARTICLES, id);
        return noteUri;
    } else {
        return null;
    }
}

private void checkMandatoryColumns(ContentValues values) {
    for (String key : MANDATORY_COLUMNS) {
        if (!values.containsKey(key)) {
            throw new NullPointerException(
                    "ContentValues must contain key " + key);
        }
    }
}

Update() ani getType() nic nového.

@Override
public int update(Uri uri, ContentValues values, String selection,
        String[] selectionArgs) {
    SQLiteDatabase db = openHelper.getWritableDatabase();

    int count;

    switch (uriMatcher.match(uri)) {
    case ARTICLES:
        count = db.update(TB_NAME, values, selection, selectionArgs);
        break;

    case ARTICLE_ID:
        String[] newArgs = createSelectionArgsWithId(selectionArgs, uri
                .getPathSegments().get(1));
        String where = createSelectionWithId(selection);

        count = db.update(TB_NAME, values, where, newArgs);
        break;

    default:
        throw new IllegalArgumentException("Unknown URI " + uri);
    }

    sendUpdateBroadcast();

    return count;
}

@Override
public String getType(Uri uri) {

    switch (uriMatcher.match(uri)) {
    case ARTICLES:
        return ZdrojakContract.MIME_TYPE_DIR;

    case ARTICLE_ID:
        return ZdrojakContract.MIME_TYPE_ITEM;

    default:
        throw new IllegalArgumentException("Unknown URI " + uri);
    }
}

BulkInsert() je zajímavější.

Metoda bulkInsert() slouží ke vložení více dat najednou. Ve výchozí implmenentaci jen zavolá insert() nkrát za sebou, ale my ještě odfiltrujeme staré články.

@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
    if (uriMatcher.match(uri) != ARTICLES) {
        throw new IllegalArgumentException("Unknown URI " + uri);
    }

    int insertCount = 0;
    long maxDate = getMaxDate();

    for (ContentValues v : values) {
        checkMandatoryColumns(v);
        long date = v.getAsLong(ZdrojakContract.PUBDATE);

        if (date > maxDate) {
            if (insert(uri, v) != null)
                insertCount++;
        }
    }

    sendUpdateBroadcast();

    return insertCount;
}

private long getMaxDate() {
    SQLiteDatabase db = openHelper.getReadableDatabase();

    String[] cols = { ZdrojakContract.PUBDATE };

    Cursor c = db.query(TB_NAME, cols, null, null, null, null,
            ZdrojakContract.SORT_ORDER_PUBDATE, "1");

    if (!c.moveToFirst())
        return -1;

    else
        return c.getLong(0);
}

private void sendUpdateBroadcast() {
    Intent i = new Intent(ZdrojakContract.ACTION_ZDROJAK_UPDATED);
    getContext().sendBroadcast(i);
}

A nakonec onCreate() a query(). Tam také nic nového. (Pro metody createSelectionArgsWithId() a createSelectionWithId viz Vyvíjíme pro Android: Content providery

@Override
public boolean onCreate() {
    openHelper = new DatabaseHelper(getContext());
    return true;
}

@Override
public Cursor query(Uri uri, String[] projection, String selection,
        String[] selectionArgs, String sortOrder) {
    SQLiteDatabase db = openHelper.getReadableDatabase();

    if (sortOrder == null || sortOrder.equals(""))
        sortOrder = ZdrojakContract.DEFAULT_SORT_ORDER;

    switch (uriMatcher.match(uri)) {
    case ARTICLES:
        break;

    case ARTICLE_ID:
        selectionArgs = createSelectionArgsWithId(selectionArgs, uri
                .getPathSegments().get(1));
        selection = createSelectionWithId(selection);
        break;

    default:
        throw new IllegalArgumentException("Unknown URI " + uri);
    }

    Cursor c = db.query(TB_NAME, projection, selection, selectionArgs,
            null, null, sortOrder);

    return c;
}

V manifestu je ZdrojakProvider zapsaný jednoduše. Možná by se slušelo přidat mu ještě writePermission  – číst může kdokoli, jsou to veřejně dostupná data, ale zapisovat by měli jen ti povolanější (a to pokud možno jen  viewed):

<provider
    android:name=".ZdrojakProvider"
    android:authorities="com.example.zdrojaknotifier.provider" />

ZdrojakActivity

Třídu ZdrojakActivity byste měli bez problému pochopit. Upozorním na šikovnou metodu  CursorAdapter.swapCursor().

public class ZdrojakActivity extends ListActivity {

    private BroadcastReceiver receiver;
    private ArticlesAdapter adapter = null;
    public static final String SHARED_PREFS_NAME = "sharedPrefs";
    public static final String LAST_VIEWED = "lastViewed";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Pro testování - Pozor na updatedToday = true, data se nemusí stáhnout!
        /*
        getContentResolver().delete(ZdrojakContract.
        CONTENT_URI_ARTICLES, "1", null);
        */

        updateDataIfNone();
        updateList();
        setupDataUpdating();

        IntentFilter filter = new IntentFilter();
        filter.addAction(ZdrojakContract.ACTION_ZDROJAK_UPDATED);

        receiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                updateList();
            }
        };

        registerReceiver(receiver, filter);
    }

    @Override
    public void onResume(){
        super.onResume();
        setLastViewedNow();
    }

    @Override
    public void onDestroy() {
        unregisterReceiver(receiver);
        super.onDestroy();
    }

    private void updateList() {
        Cursor c = getContentResolver().query(
                ZdrojakContract.CONTENT_URI_ARTICLES,
                ZdrojakContract.PROJECTION, null, null,
                ZdrojakContract.SORT_ORDER_PUBDATE);

        if (adapter == null) {
            adapter = new ArticlesAdapter(this, c, 0);
            setListAdapter(adapter);
        } else {
            adapter.swapCursor(c);
        }

    }

    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        String uriString = (String) v.getTag(R.id.tag_link);
        ZdrojakContract.setViewed(this, id, true);
        Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(uriString));
        startActivity(i);
    }

    private void setLastViewedNow(){
        getSharedPreferences(SHARED_PREFS_NAME, 0).edit()
                .putLong(LAST_VIEWED, new Date().getTime()).commit();
        sendBroadcast(new Intent(ZdrojakContract.ACTION_LASTVIEW_CHANGED));
    }

Alarm nastavíme půl hodiny po nejbližší půlnoci.

    private void setupDataUpdating() {
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(System.currentTimeMillis());
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MINUTE, 30);
        calendar.set(Calendar.HOUR, 0);
        calendar.set(Calendar.AM_PM, Calendar.AM);
        calendar.add(Calendar.DAY_OF_MONTH, 1);

        Intent intent = new Intent(this, UpdateReceiver.class);
        intent.putExtra(UpdateReceiver.ALARM_BROADCAST, true);
        PendingIntent pi = PendingIntent.getBroadcast(this, 0, intent, 0);

        ((AlarmManager) getSystemService(Context.ALARM_SERVICE)).setRepeating(
                AlarmManager.RTC, calendar.getTimeInMillis(), AlarmManager.INTERVAL_DAY, pi);
    }

A ostatní metody už jsou zase jednoduché:

    private void updateDataIfNone(){
        Cursor c = getContentResolver().query(
                ZdrojakContract.CONTENT_URI_ARTICLES,
                ZdrojakContract.PROJECTION, null, null,
                ZdrojakContract.DEFAULT_SORT_ORDER);

        if(c.getCount() == 0){
            Intent intent = new Intent(this, UpdateReceiver.class);
            sendBroadcast(intent);
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case R.id.menu_mark_all_read:
            ContentValues values = new ContentValues();
            values.put(ZdrojakContract.VIEWED, true);
            getContentResolver().update(ZdrojakContract.CONTENT_URI_ARTICLES, values, "1", null);
            return true;
        default:
            return super.onOptionsItemSelected(item);
        }
    }
}

V manifestu je ZdrojakActivity zapsána klasicky.

ArticlesAdapter

ArticlesAdapter zobrazí seznam článků včetně perexů. Přečtené budou mít černý titulek, nepřečtené zelený. Jak jste si určitě všimli ve ZdrojakActivity.onListItemClick() odkaz na článek získávám z tagu příslušného View. (Mimochodem, id R.id.tag_link jsem vytvořil v /res/values/ids.xml.) Asi by bylo čistší poprosit Adapter o Cursor, ten přesunout na příslušnou pozici a link zjistit z něj, ale to je práce navíc.

Začátek ArticlesAdapter-u je obyčejný:

public class ArticlesAdapter extends CursorAdapter {


    public ArticlesAdapter(Context context, Cursor c, int flags) {
        super(context, c, flags);
    }

    @Override
    public View newView(Context ctx, Cursor c, ViewGroup root) {
        LayoutInflater inflater = LayoutInflater.from(ctx);
        View view = inflater.inflate(R.layout.article, root, false);
        bindView(view, ctx, c);
        return view;
    }

    @Override
    public void bindView(View oldView, Context ctx, Cursor c) {
        int titleIndex = c.getColumnIndex(ZdrojakContract.TITLE);
        int descriptionIndex = c.getColumnIndex(ZdrojakContract.DESCRIPTION);
        int authorIndex = c.getColumnIndex(ZdrojakContract.AUTHOR);
        int pubdateIndex = c.getColumnIndex(ZdrojakContract.PUBDATE);
        int linkIndex = c.getColumnIndex(ZdrojakContract.LINK);
        int viewedIndex = c.getColumnIndex(ZdrojakContract.VIEWED);

Potom nastavím kořenovému View dané položky tag:

oldView.setTag(R.id.tag_link, c.getString(linkIndex));

A pak se postarám o titulek. Metodu Theme.resolveAttribute() nebudete používat dvakrát často, takže případně vizte dokumentaci.

TextView title = (TextView) oldView.findViewById(R.id.title);
title.setText(c.getString(titleIndex));

boolean viewed = c.getInt(viewedIndex) == 1;

TypedValue tv = new TypedValue();
ctx.getTheme().resolveAttribute(android.R.attr.textColorPrimary, tv, true);
title.setTextColor(ctx.getResources().getColor(viewed ? tv.resourceId : R.color.zdrojak_green));

Pak už se zase neděje nic zvláštního:

    ((TextView) oldView.findViewById(R.id.author)).setText(c
            .getString(authorIndex));

    ((TextView) oldView.findViewById(R.id.description)).setText(c
            .getString(descriptionIndex));


    long date = c.getLong(pubdateIndex);
    SimpleDateFormat f = new SimpleDateFormat("d. M.");
    ((TextView) oldView.findViewById(R.id.pubdate)).setText(f.format(new Date(date)));
}

Soubor /res/layout/article.xml vypadá takto:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".ZdrojakActivity" >

    <TextView
        android:id="@+id/title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:textColor="?android:attr/textColorPrimary"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/pubdate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="right"
        android:textAppearance="?android:attr/textAppearanceSmall"
        android:textColor="?android:attr/textColorSecondary" />

    <TextView
        android:id="@+id/author"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="left"
        android:textAppearance="?android:attr/textAppearanceSmall"
        android:textColor="?android:attr/textColorSecondary" />

    <TextView
        android:id="@+id/description"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="left"
        android:layout_marginTop="8dp"
        android:textAppearance="?android:attr/textAppearanceMedium"
        android:textColor="?android:attr/textColorPrimary" />

</LinearLayout>

Není to nic hezkého, v příštím díle to možná napravíme.

Pokud vás zaujala namespace tools, vězte, že to jsou pomocné atributy pro nový ADT plugin.

A zbývají nám už jen broadcast receivery.

UpdateReceiver

Začneme UpdateReceiver-em. Ten je spouštěn jednak půl hodiny po půlnoci (s extra ALARM_BROADCAST = true), jednak při každé změně stavu připojení a jednak občas ručně. Aby nevytvářel moc požadavků na Zdroják, do shared preferences uložíme informaci, zda byla data dnes už aktualizována ( UPDATED_TODAY), jíž s každým spuštěním receiveru o půlnoci nastavíme na  false:

public class UpdateReceiver extends BroadcastReceiver {

    public static final String ALARM_BROADCAST = "alarmBroadcast";
    public static final String UPDATED_TODAY = "updatedToday";

    @Override
    public void onReceive(Context ctx, Intent intent) {
        SharedPreferences prefs = ctx.getSharedPreferences(ZdrojakActivity.SHARED_PREFS_NAME, 0);

        if(intent.getBooleanExtra(ALARM_BROADCAST, false)){
            prefs.edit().putBoolean(UPDATED_TODAY, false).commit();
        }

        if(!prefs.getBoolean(UPDATED_TODAY, false))
            updateData(ctx, prefs);
    }

Když není k dispozici připojení, vyhodí se výjimka ZdrojakRSSHandlerException. Toho využijeme a vůbec nemusíme kontrolovat, je-li telefon na internetu:

private void updateData(final Context ctx, final SharedPreferences prefs){
    (new Thread(new Runnable() {
        public void run() {
            ZdrojakRSSHandler handler = new ZdrojakRSSHandler(ctx);
            try {
                handler.updateData();
                prefs.edit().putBoolean(UPDATED_TODAY, true).commit();
            } catch (ZdrojakRSSHandlerException e) {
                e.printStackTrace();
            } catch (RSSParserException e) {
                e.printStackTrace();
            }
        }
    })).start();
}

Broadcast receivery musejí být zapsané v manifestu:

<receiver android:name=".UpdateReceiver" >
    <intent-filter>
     <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
    </intent-filter>
</receiver>

Abychom mohli naslouchat Intentům s android.net.conn.CONNECTIVITY_CHANGE, musíme mít permission ACCESS_NETWORK_STATE.Zároveň s ním přidáme i permission  INTERNET:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

NotificationReceiver

Pokud jste si všimli, ZdrojakActivity rozesílá broadcast, že se změnil čas jejího posledního zobrazení. Tomu (a k tomu ještě i broadcastu rozesílanému ZdrojakProvider-em o změně dat) naslouchá  NotificationReceiver.

Ten vždycky na začátku odstraní případnou existující notifikaci (každá notifikace má své id), protože je opruz, když vidíte, že je nový článek, spustíte aplikaci pomocí seznamu aplikací, článek přečtete, ale notifikace zůstane.

public class NotificationReceiver extends BroadcastReceiver {

    private static final int NOTIFICATION_ID = 1;

    @Override
    public void onReceive(Context context, Intent incoming) {
        NotificationManager notificationManager = (NotificationManager) context
                .getSystemService(Context.NOTIFICATION_SERVICE);

        notificationManager.cancel(NOTIFICATION_ID);

Potom zjistí, kdy byla ZdrojakActivity naposledy zobrazena a na základě toho vytvoří dva Cursory. V jednom budou články, které se stáhly až po posledním zobrazení Activity, ve druhém budou nepřečtené články:

long lastViewed = context.getSharedPreferences(
        ZdrojakActivity.SHARED_PREFS_NAME, 0).getLong(
        ZdrojakActivity.LAST_VIEWED, -1);

ContentResolver cr = context.getContentResolver();

Cursor totalyNew = cr.query(ZdrojakContract.CONTENT_URI_ARTICLES,
        ZdrojakContract.PROJECTION, ZdrojakContract.ADDED + " > ?",
        new String[] { String.valueOf(lastViewed) },
        ZdrojakContract.SORT_ORDER_PUBDATE);

Cursor unread = cr.query(ZdrojakContract.CONTENT_URI_ARTICLES,
        new String[] { ZdrojakContract.TITLE }, ZdrojakContract.VIEWED
                + " = 0", null, ZdrojakContract.SORT_ORDER_PUBDATE);

A nakonec se podle počtu úplně nových rozhodne, jakou notifikaci zobrazí.

int unreadCount = unread.getCount();

Notification n;
NotificationCompat.Builder builder = new NotificationCompat.Builder(
        context);

Intent intent = new Intent(context, ZdrojakActivity.class);

PendingIntent pi = PendingIntent.getActivity(context, 0, intent, 0);

Pokud je úplně nový jen jeden, bude notifikace obsahovat jeho titulek, jeho autora, na API >= 11 informaci o celkovém počtu nepřečtených článků (jako GMail) a na Jelly Bean roztahovací perex nového článku pomocí NotificationCompat.BigTextStyle. Po kliknutí na notifikaci se spustí ZdrojakActivity.

if (totalyNew.getCount() == 1) {
    int titleIndex = totalyNew.getColumnIndex(ZdrojakContract.TITLE);
    int authorIndex = totalyNew.getColumnIndex(ZdrojakContract.AUTHOR);
    int descIndex = totalyNew
            .getColumnIndex(ZdrojakContract.DESCRIPTION);

    totalyNew.moveToFirst();

    n = builder
            .setContentTitle(totalyNew.getString(titleIndex))
            .setContentText(totalyNew.getString(authorIndex))
            .setSmallIcon(R.drawable.notification)
            .setContentIntent(pi)
            .setNumber(unreadCount)
            .setStyle(
                    new NotificationCompat.BigTextStyle()
                            .bigText(totalyNew.getString(descIndex)))
            .build();
}

Pokud je úplně nových článků více, bude v titulku notifikace, že jsou dostupné nové články, v malém textu i jejich počet, ten bude také vpravo dole. A na Jelly Bean po vzoru GMailu nabídneme seznam (pěti, více nejde) titulků pomocí  NotificationCompat.InboxStyle.html:

else if (totalyNew.getCount() > 1) {
    Resources res = context.getResources();
    int titleIndex = unread.getColumnIndex(ZdrojakContract.TITLE);

    NotificationCompat.InboxStyle st = new NotificationCompat.InboxStyle();

    if(unread.getCount() > 5)
        st.setSummaryText(res.getString(R.string.and_x_more, unread.getCount() - 5));

    for(int i = 0;i < 5 && unread.moveToNext();i++){
        st.addLine(unread.getString(titleIndex));
    }

    n = builder
            .setContentTitle(res.getString(R.string.new_articles))
            .setContentText(
                    res.getString(R.string.new_articles_count,
                            unreadCount))
            .setSmallIcon(R.drawable.notification)
            .setContentIntent(pi)
            .setNumber(unreadCount)
            .setStyle(st)
            .build();
}

Pokud nejsou žádné úplně nové články, notifikace nebude:

else{
    return;
}

No a nakonec vyrobenou notifikaci zobrazíme:

notificationManager.notify(NOTIFICATION_ID, n);

Nové články na Zdrojáku v akci!

Veliká notifikace na Jelly Bean

Zmenšená notifikace na Jelly Bean (zaboha se mi ji nepodařilo zmenšit na emulátoru, takže je screnshot z mého Nexusu S)

Notifikace na Androidu 1.6, vidíte (resp. spíš nevidíte) problém s bílou ikonkou?

Seznam článků (druhý je přečtený)

 

Notifikační ikony

Protože mám ještě nějaké místo k dobru, zmíním se o notifikačních ikonách.

Od Androidu 3.0 mají být notifikační ikony čtvercové a mají se skládat jen z bílé a průhlednosti. Podle icon guidelines mají mít na ldpi displejích rozměry 18 × 18 px, na mdpi 24 × 24 px, na hdpi 36 × 36 px a na xhdpi  48 × 48 px.

Na Androidu 2.3 sice už mají být víceméně průhledné, ale mají být v odstínech šedi a jinak velké.

A aby toho nebylo málo, na Androidu < 2.3 mají být ikony zase čtverečkové, ačkoli s trochu jinými rozměry a nemají mít průhledné pozadí (jen malinko v rozích).

Pokud chcete mít aplikaci, jejíž notifikace vypadají všude použitelně, musíte vytvořit složky drawable-xhdpi-v11, drawable-hdpi-v11, drawable-mdpi-v11, a drawable-ldpi-v11 s ikonou pro Android >= 3.0; složky drawable-xhdpi-v9, drawable-hdpi-v9, drawable-mdpi-v9, a drawable-ldpi-v9 s ikonou pro Android 2.3 a složky drawable-xhdpi, drawable-hdpi, drawable-mdpi, a drawable-ldpi pro Android < 2.3. Soubor ikony se samozřejmě musí jmenovat vždy stejně.

Pro dnešní ukázku jsem opravdu tolik ikon nevytvářel, ale pokud bude čas, pokusím se to příště napravit.

Závěr

Dnes jsme si na aplikaci, která má s přimhouřením oka opravdu praktické využití, vyzkoušeli práci s broadcast receivery, notifikacemi, naučili jsme se pracovat s Internetem, AlarmManagerem a procvičili jsme si plno dalších věcí. Dovolím si konstatovat, že pokud umíte všechno, co jsme používali v tomto a předchozích dílech, umíte toho mnohem víc než já.

Zdrojové kódy dnešní aplikace si můžete stáhnout na této adrese.

V příštím díle zkusíme dnešní aplikaci trochu zkultivovat a nahrát ji na Play Store.

Tip na konec

Stiskem klávesy F8 na emulátoru vypnete/zapnete připojení k internetu. Jde tak docela pěkně testovat broadcast receiver.

Naopak jsem nikde nenašel, jak testovat AlarmManager, takže to dělám tak, že zavolání nastavím na blízkou budoucnost. Ale ani to není tak jednoduché, Calendar se občas chová zvláštně. Pro vypsání data z kalendáře můžete použít následující útržek kódu. Sice používá deprecated metodu, ale to nám nevadí.

Log.d("calendar", calendar.getTime().toLocaleString());

Matěj začal programovat ve třinácti v PHP, pak v JavaScriptu a Lispu. Nakonec si koupil Androida, a tak programuje hlavně pro něj.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 5

Přehled komentářů

BlackRider Nikdy nerikej nikdy ;)
Jakub Hejhal Stahování (a parsování?) RSS
Martin Hassman Re: Stahování (a parsování?) RSS
Venda Zvuk
Lenochod Padá
Zdroj: https://www.zdrojak.cz/?p=3702