Vyvíjíme pro Android: Intenty, intent filtry a permissions

Spolupráce aplikací na Androidu přímo závisí na intentech a intent filtrech. Bezpečnost je zase poměrně vysoká díky permissions. A to bude náplní dnešního dílu seriálu Vyvíjíme pro Android.

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

Minule jsme vytvořili editovatelný a personifikovatelný seznam přečtené literatury s možností hodnocení a řazení, přičemž jsme použili preference. Také jsme potřebovali databázi, o níž jsem psal před dvěma týdny. Dneškem jsme původně měli zakončit trojblok, v němž pracujeme hlavně s daty, ale o intentech je toho zajímavého tolik, že to vydá na vlastní článek (a bez dnešních informací by bylo vysvětlování content providerů složitější). Takže slíbené content providery budou až příště.

Oprávnění

Oprávnění (anglicky permissions) jsou velmi mocným nástrojem, díky němuž má uživatel jasný přehled o tom, co která aplikace může dělat. Už jste se s nimi mnohokrát setkali, je to to, co otravuje při instalaci aplikací, a my to musíme odklepávat.

Každá aplikace musí v manifestu vypsat seznam všech oprávnění, která potřebuje. Na to slouží emenent <uses-permission> s jediným atributem, android:name, jehož hodnotou je název daného oprávnění. Názvy vestavěných androidích oprávnění můžete najít v dokumentaci třídy Manifest.permission. Potřebuje-li aplikace například využívat internet, jako přímého potomka elementu <manifest>  přidá

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

, neboť android.permission.INTERNET je hodnota konstanty Manifest.permission.INTERNET, jíž jsme našli ve výše odkázané stránce dokumentace.

Pokud aplikace internet chce využívat, ale o oprávnění INTERNET si v manifestu nepožádala, utře a dostane jen  SecurityException.

Aplikace ale mohou oprávnění samy vytvářet. K tomu slouží element <permission>, přímý potomek <manifest>-u. Důležitými atributy jsou:

Důležité atributy <permission>
Název Popis
android:name Řetězec, který identifikuje potřebné povolení. Vestavěná povolení mají všechna předponu android.permission, ostatní by měla před samotným jménem mít jako předponu namespace vašeho projektu, někdo přidává ještě navíc permission. Vlastní název oprávnění se řídí stejnými pravidly jako pojmenovávání konstant – velká písmena, místo mezer podrtžítka. Kdybychom tedy třeba chtěli, aby aplikaci z minulého týdne mohly spustit jen aplikace s nějakým oprávněním, jako jméno bychom zvolili com.example.bookshelf.START_APP potažmo  com.example.bookshelf.permission.START_APP.
android:label Nějaký lidsky čitelný řetězec (surovina), název oprávnění. Třeba „Spustit Knihovnu”.
android:description Lidsky čitelný delší popis oprávnění.
<permission
    android:name="com.example.bookshelf.START_APP"
    android:label="@string/perm_label"
    android:description="@string/perm_description" />

Pokud bychom opravdu chtěli omezit spouštění knihovny, ještě bychom v manifestu příslušnému elementu <activity> museli přidat atribut android:permission, jehož hodnotou je android:name daného oprávnění.

Element <uses-permission> použijete snad v každé aplikaci, zatímco vytváření nových oprávnění zdaleka tak časté není.

Více se o oprávněních dočtete ve článku Permissions a trochu ještě přidáme, až si budeme povídat o content providerech.

Intenty

Intent-y jsou ohromně důležitým (a mocným) androidím můstkem mezi jednotlivými stavebními prvky (activity, ale i broadcast receivery či services), a my jim přitom věnovali hodně málo, vlastně jen odstaveček ve článku Suroviny, Intenty a jednotky. Dnes to napravíme.

Intent je opravdu jednoduchý objekt, který v podstatě umí jen obsáhnout nějaká primitivní data (a procpat je mezi procesy). V širším kontextu můžeme rozlišit dva typy intentů – explicitní a implicitní.

Explicitní intenty

S těmi jsme už pracovali. Vědí přímo, kterou třídu chtějí spustit. Takový explicitní intent se vytvoří třeba takto:

Intent i = new Intent(context, BerserkActivity.class);

S explicitními intenty ví systém přesně, co chcete spustit.

Implicitní intenty

Implicitní intenty jsou mnohem zajímavější. Umožňují vám totiž popsat pouze váš záměr (intent = záměr) a nikoli přesný způsob, jak ho provést. Tím jednak v mnoha případech značně zjednodušují práci a jednak umožňují vzájemnou interoperabilitu aplikací v rámci Androidu.

Například řekněme, že chceme zobrazit stránku https://www.zdrojak.cz. Kdybychom mohli používat pouze explicitní intenty, byla by to dost komplikovaná věc – museli bychom postupně vyzkoušet přítomnost všech možných prohlížečů a spustit ten, který by se jako první ukázal být dostupný. To samozřejmě zní dost nepohodlně (a to už nemluvím o tom, že bychom vůbec nehleděli na nějaké přání uživatele), takže bychom nakonec možná skončili se speciální activity, do níž bychom umístili WebView, a web zobrazili v ní.

Když známe implicitní intenty, je zobrazení webové stránky jednoduché:

Uri uri = Uri.parse("https://www.zdrojak.cz");
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);

Použili jsme konstruktor Intent(String action, Uri uri), který nám umožní předat název implicitní akce a potom nějaké URI.

Na základě dat v intentu a toho, že jsme použili metodu startActivity, Android zjistí, které activity mají nastavený takový intent filter, že je slučitelný s daty v intentu. Pokud je jen jedna, spustí ji, pokud není žádná, obdržíte ActivityNotFoundException, pokud jich je víc, uživateli se zobrazí dialog s výběrem té, která daný intent splní (pokud už předtím nějakou vybral, že bude plnit všechny podobné intenty, spustí se samozřejmě rovnou ta).

Intent filtry

Takový intent filter je způsob, jak může nějaká activity (service, broadcast receiver) dát najevo, že je schopna nějaký typ implicitních intentů splnit. Element <intent-filter> je potomkem elementu <activity> (či <service>, <receiver> nebo <activity-alias>). Může jich tam být 0, 1 i více. Activity se považuje za plnící daný intent tehdy, když alespoň jeden z jejích intent filtrů projde. Pokud nemá activity žádný intent filter, může být spuštěna pouze explicitně.

Naopak pokud je activity spuštěna explicitně, nebere se na intent filtry vůbec žádný ohled. Takže nemůžete intent filter brát jako nějakou bezpečnostní překážku. U špatně napsané activity vám může útočník podstrčit i data, s nimiž si activity neporadí. Nebo hůře, nějaká data, která způsobí bezpečnostní problém.

A podle čeho se může filtrovat?

„Přihrádky” Intentu

Jak jsme si řekli, objekt Intent je vlastně jen kontejnerem pro nějaká primitivní data.

Component name

Předáme-li intentu přímo třídu, již má spustit, vznikne explicitní intent. U něj nemá category ani action smysl. (To není úplně pravda, interně může activity podle action rozlišovat, jak se vlastně bude chovat.)

Nadpis component name jsem použil z důvodu vysvětleného v Intents and Intent Filters.

Action

Můžeme také vyrobit intent konstruktorem Intent(String action). Tehdy neurčíme, která třída nebo dokonce aplikace má náš požadavek splnit. Tím jsme vytvořili implicitní intent. Action je nějaký řetězec, jehož samotná hodnota podléhá stejným konvencím jako permission. Například často používaná action, která znamená, že chceme něco zobrazit má hodnotu android.intent.action.VIEW a chceme-li s ní zacházet v kódu, slouží k tomu konstanta  Intent.ACTION_VIEW.

Teď trochu odbočíme. Na Androidu existují tři komponenty aplikace, které mají co do činění s intenty. Activity, s nimiž jsme se už setkali, broadcast receivery, což jsou jakési posluchače globálních událostí (například můžete vyrobit broadcast receiver, který něco provede tehdy, když se změní stav připojení k internetu) a nejspíš si o nich něco povíme, a services, služby, které běží na pozadí a bez nichž se pravděpodobně v našem seriálu obejdeme. (Společně s content providery, které jsou ovšem z hlediska intentů odlišné, jsou tyto komponenty popsány v Application Components.)

Všechny tyto komponenty nějak pracují s intenty. Jak už jsme se naučili, activity spustíme buď metodou Context.startActivity() nebo Activity.startActivityForResult(). Service můžeme spustit například pomocíContext.startService() (a pokud vás services zajímají, odkážu vás do manuálu na kapitolu Services – pokud vás zajímají enormně, ozvěte se v komentářích). Broadcast receiver přímo nespustíme, ale můžeme vyslat hlášení pomocí metodyContext.sendBroadcast() (nebo podobných), která přijímá intent, v němž je broadcast popsán.

A protože každá z těchto komponent přímo závisí na intentech a přitom každá dělá něco úplně jiného, mají své vlastní vestavěné actions. Některé z nich můžeme najít v dokumentaci třídy Intent (skoro hned nahoře, nadpis Standard Activity Actions, bohužel tam není žádná kotva), další (hlavně právě pro broadcast receivery) jsou roztroušené jinde – třeba android.net.conn.CONNECTIVITY_CHANGE, které se zavolá právě při změně stavu připojení, má konstantu android.net.ConnectivityManager.CONNECTIVITY_ACTION. Smyslem těchto tří odstavců bylo vysvětlit, že nemá smysl volat startActivity s intentem, který má action třeba android.intent.action.ACTION_SHUTDOWN . Technicky to sice možné je, ale nejspíše vám nikdo neodpoví.

Ve zbytku článku poněkud nepřesně zapomenu, že intenty mohou spouštět nejen activity.

Kromě konstruktoru samozřejmě existuje metoda Intent.setAction(String action). Ve spuštěné activity můžete potom action získat metodou  Intent.getAction().

Každý Intent může mít maximálně jednu action.

Category

Kromě action může mít Intent i category (jednu, ale i více). Zatímco action určuje, co se má provést, category by měla specifikovat, jaký typ activity je potřeba. Každý Intent má implicitně android.intent.category.DEFAULT (až na jednu výjimku – později). Mezi další categories patří například android.intent.category.BROWSABLE, jež značí, že daná activity může být bezpečně spuštěna z prohlížeče, že se nestane bez uživatelova vědomí nic, co by nechtěl, co by nečekal, android.intent.category.HOME, jež společně s ACTION_MAIN  žádají o domovskou obrazovku, či android.intent.category.LAUNCHER, s níž jsme se už setkali kolikrát – je to (společně s ACTION_MAIN) součást automaticky generovaného intent filtru pro generovanou activity při vytváření projektu. A tato kombinace znamená, že se daná activity zobrazí v seznamu aplikací.

Pro přidávání kategorií existuje metoda Intent.addCategory(), pro odebírání Intent.removeCategory(), chcete-li zkontrolovat, zda intent obsahuje nějakou category, použijte metodu Intent.hasCategory(). Ještě existuje Intent.getCategories(), ale nenapadá mě případ, kdy by její použití bylo odůvodnitelné – za mnohem lepší považuju právě  hasCategory.

S component name je intent explicitní, s action a category je implicitní. Ale občas se hodí poslat i nějaká data. To lze dvěma způsoby – pomocí uri, anebo pomocí tzv. intent extras.

Intent extras

Začneme tím jednodušším. S intent extras jsme se už seznámili. Je to způsob, jak do Intentu přidat key-value páry s řetezcovými klíči a takovými hodnotami, které Android umí serializovat. Tedy primitivní typy, objekty implementující rozhraní Parcelable a pole obého. Na přidání extra dat existuje metoda putExtra(), na odebírání removeExtra(), na získání getTypeExtra(), kde za Type doplňte typ dat, a na zjištění, zda daný klíč existuje, máme metodu  hasExtra().

Podle extra dat se v intent filtrech filtrovat nedá.

URI a MIME type

Intent může přijmout i URI a (nebo) MIME type. URI podá informaci, kde daná data získat, nebo (např. v případě protokolů mailto: resp. tel:) je přímo obsahovat. MIME type umí Android v některých případech zjistit z URI, jindy ho musíme předat explicitně.

Samotné URI se (jako každé URI) skládá ze schématu (scheme), názvu hostitele (host), portu (port) a cesty (path), asi takto:

scheme://host:port/path

URI může ukazovat na nějaký obsah z content provideru, může ukazovat na soubor na webu, může ukazovat na soubor na telefonu, může značit e-mailovou adresu nebo třeba telefonní číslo.

MIME type je buď klasický MIME type, třeba image/png, ale může to být i MIME type definovaný v Androidu (třeba vnd.android.cursor.dir/phone_v2 pro telefonní čísla) či nějakou aplikací. Pro samotnou spouštěnou activity valný smysl nemá, ale díky němu lze v intent filtrech jednoduše říci, že activity umí například zobrazit obrázky.

Pro nastavení URI slouží konstruktor Intent(String action, Uri uri) nebo metoda setData(Uri uri). Pro získání URI máme metodu getData(). MIME type nastavíme metodou setType()  a získáme pomocí getType(). Pro nastavení URI a MIME najednou existuje setDataAndType(). Pozor! Pokud potřebujete, aby měl Intent nastavenou zároveň URI a zároveň MIME type, musíte použít metodu setDataAndType(). Zavolání nejdříve setData() (příp. použití takového konstruktoru) a pak setType() nebo opačně nefunguje.

Flags

Úplně mimo stojí flags, které určují, jakým způsobem activity spustit (třeba jestli se smí použít její už spuštěná instance). Bez nich se dá docela dobře fungovat, takže vás odkážu na dokumentaci třídy Intent , kde jsou všechny možné flags hezky popsány (obecně, Intent má velmi dobře odvedenou dokumentaci).

Zpátky k intent filtrům

Teď, když už máme popsány všechny přihrádky, můžeme si konečně objasnit intent filtry.

Jak už jsem se zmínil, filtry jsou definovány v manifestu jako elementy <intent-filter> a jejich potomci. Activity je schopna splnit daný intent právě tehdy, když alespoň jeden element <intent-filter> projde testem. A jak se pozná, že projde testem? A jaké může mít vlastně potomky?

<action>

Jako první si představíme element <action>. Ten má jeden atribut, android:name, jehož hodnotou je název action, kterou umí splnit. Každý <intent-filter> může mít neomezeně potomků <action>, přičemž stačí, aby prošel jeden z nich (zjevně). Pokud ale neprojde žádný, anebo <intent-filter> nemá žádné potomky <action>, test pro <intent-filter>  selže.

<category>

Element <category> je prakticky stejný s elementem <action>  – také má atribut android:name. Na Intentu může být categories nastaveno více, takže aby <intent-filter> prošel, musí obsahovat alespoň všechny categories nastavené v Intentu. Protože je android.intent.category.DEFAULT na všech Intentech, které spouštějí activity, <intent-filter>, který neobsahuje android.intent.category.DEFAULT, nikdy neprojde. S jednou výjimkou, a to shodou okolností tou, s níž jsme se zatím jako s jedinou setkali, tedy kombinace android.intent.category.LAUNCHER a android.intent.action.MAIN  – activity, které se mají zobrazit v launcheru. Jejich intent filtry CATEGORY_DEFAULT obsahovat mohou, ale nemusí.

<data>

Když mluvíme o datech, máme na mysli URI a MIME type. A pro ty existuje element <data>. Ten má atributů hned několik, všechny nepovinné.

Android:mimeType, jak už název napovídá, testuje, zda MIME type dat na předané URI odpovídá naším požadavkům. Místo každé části MIME type ( type/subtype) můžeme použít hvězdičku – tedy říci, že chceme například všechny obrázky ( image/*) nebo všechny textové soubory ( text/*).

Atribut android:scheme označuje požadované schéma, protokol. Nepatří tam dvojtečka na konec. Narozdíl od příslušného RFC je case-sensitive, pište ho vždy malými písmeny.

Chtěli-li bychom pracovat se všemi xml dokumenty z internetu, element <data> by vypadal asi takto (ve skutečnosti bychom asi měli udělat ještě jednou to samé pro  https):

<data scheme="http" mimeType="text/xml" />

Chceme-li obsloužit jen některé porty, slouží pro to atribut android:port. To asi moc často nepoužijete.

Díky android:host můžeme určit, kteří hostitelé nás zajímají. Vězte, že narozdíl od standardu je Android case-sensitive. Atribut android:host nemá smysl, není-li nastaveno  android:scheme.

A konečně testování samotné cesty. To je trochu složitější, neboť existují hned tři atributy. Android:path hloupě porovná svou hodnotu s cestou z URI z Intentu. Android:pathPrefix je malinko šikovnější, stačí, když se začátek cesty přesně shoduje s jeho hodnotou.

Atribut android:pathPattern je proti nim o dost chytřejší. Opět se porovnává s celým řetězcem, stejně jako android:path, ale narozdíl od něj může obsahovat následující wildcards: *, která značí 0–nekonečno výskytů předcházejícího znaku, a .* pro 0–nekonečno výskytů libovolného znaku.

Všechny tři cestové atributy musejí začínat lomítkem ( /), neboť všechny cesty začínají lomítkem. Chcete-li použít hvězdičku jako znak, musíte ji escapovat – takto: \*. Pro zpětné lomítko potřebujete dokonce toto: \\. Stručně řečeno, platí stejná escapovací pravidla jako pro javovské řetězce.

Například go*gle vyhovuje jak řetězci ggle, tak řetězci google, tak třeba řetězci goooooooogle, ale nikoli řetězci g0gle. g.*gle oproti tomu vyhoví všem řetězcům, které začínají na g a končí na gle (a není to přímo  gle).

Následující řádky jsou si ekvivalentní (první se třetím a čtvrtým dohromady), jinak řečeno je jedno, jestli na jednom elementu <data> nastavíte více atributů, anebo jestli použijete více elementů  <data>.

<data android:scheme="http" android:host="www.zdrojak.cz" />
<!-- je ekvivalentní s -->
<data android:scheme="http" />
<data android:host="www.zdrojak.cz" />

Určit, zda daný Intent odpovídá elementům <data> je trochu složitější (seznam volně převzat z Intents and Intent Filters (podnadpis Data test)):

  1. Pokud Intent nespecifikuje ani URI, ani MIME type, projde jen tím filtrem, který neobsahuje žádné elementy  <data>.
  2. Pokud Intent obsahuje URI, ale nikoli MIME type, a ten se z ní navíc nedá ani odvodit, projdou jen ty filtry, které nepožadují nějaký MIME type. V praxi jde pouze o protokoly mailtotel.
  3. Pokud objekt Intent nabízí MIME type, ale žádnou URI, projde tehdy, když se ve filtru nachází i specifikovaný MIME, ale přitom žádné testování URI.
  4. Pokud Intentový objekt nabízí MIME i URI, projde tehdy, pokud filter obsahuje daný MIME type a zároveň projde URI. Pokud filter vůbec URI nespecifikuje, Intent projde jen tehdy, pokud má scheme content nebo  file.

Možná ještě obecněji a jednodušeji můžu říci, že v případě action, categories a protokolů (schemat) s výjimkou content a file  projde intent pouze tehdy, pokud jsou všechny v intent filtru explicitně zmíněny. U MIME type a ostatních částí URI kromě scheme platí následující: pokud není v intent filtru ani jednou zmíněno mime type (nebo host, path atd.), přijímá intent filter všechny. Jakmile se tam objeví i třeba jen jeden atribut android:mimeType, projdou jen Intenty s MIME type nastaveným v intent filtru.

Mějme například adresu https://www.zdrojak.cz. Pokud mám v intent filteru vedle potřebné action a category ještě <data scheme="http" />, je všechno v pořádku. Jakmile přidám <data host="www.google.com" />, najednou už náš intent filter Intent s URI https://www.zdrojak.cz nepropustí. Pokud přidám ještě řádek <data host="www.zdrojak.cz" />, adresa https://www.zdrojak.cz (a samozřejmě všechny její podadresy) znovu projdou.

Nezapomeňte, že ačkoli to tak na webu často není, adresy zdrojak.cz a www.zdrojak.cz jsou z pohledu intent filtru odlišné.

Na další čtení o intentech a intent filtrech doporučuju už odkazovaný článek Intents and Intent Filters a dokumentaci třídy Intent.

Programujeme

Ve dnešní ukázkové aplikaci, ačkoliv tentokrát bych to spíše než aplikací nazval demonstrací schopností intent filtrů, si pohrajeme s intenty, intent filtry a permissions.

Vytvořte nový projekt s hlavní třídou SenderActivity, která bude dědit od ListActivity. Bude obsahovat seznam pro nás srozumitelných názvů jednotlivých intentů a po kliknutí na některý z nich se zavolá startActivity() s příslušným Intentem. Kostra SenderActivity bude vypadat takto:

public class SenderActivity extends ListActivity {

    static String[] names = { "Web", "Contact", "File", "Zdroják" };

    Intent[] intents;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setListAdapter(new ArrayAdapter<String>(this,
                android.R.layout.simple_list_item_1, names));

        intents = new Intent[names.length];
        Intent i;
    }

    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        startActivity(intents[position]);
    }

V onCreate()  budeme postupně vytvářet Intenty pro jednotlivá jména a umisťovat je na odpovídající místo pole intents. Intent i je jen inicializace proměnné, kterou budeme používat při vytváření Intentů.

Kromě SenderActivity.java se nám objevil i element <activity> v AndroidManifest.xml. Dnes nás zajímá enormně, neboť konečně rozumíme tomu, co znamená vygenerovaný <intent-filter>. Díky kombinaci jeho potomků se SenderActivity zobrazí v seznamu aplikací.

<activity
    android:name=".SenderActivity"
    android:label="@string/title_activity_sender" >
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

Permission INTERNET

Budeme pracovat s internetem, na což je potřeba permission android.permission.INTERNET. To je pro nás ale hračka, ne? Do manifestu jako přímého potomka elementu <manifest> přidáme následující řádek:

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

Webové adresy

Zkusme do SenderActivity přidat první Intent – třeba s URI http://www.google.com/webhp. ACTION_VIEW znamená, že chceme data nacházející se na URI zobrazit.

i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse("http://www.google.com/webhp"));
intents[0] = i;

Pro tuto URI vytvoříme hned několik různých Activit s různými intent filtry:

OnlyGoogleComActivity

Samotný soubor OnlyGoogleComActivity.java vlastně nic nedělá:

public class OnlyGoogleComActivity extends Activity {

    @Override
    public void onCreate(Bundle icicle){
        super.onCreate(icicle);
        setContentView(android.R.layout.simple_list_item_1);
    }
}

Jako content view nastavíme nějaký existující layout, jen aby Activity při spuštění neházela výjimky a my zároveň nemuseli vytvářet nějaký zbytečný layoutový XML soubor. Ve skutečnosti nás ale zajímá jen, jestli se Activity zbrazí v nabídce těch, které umějí daný Intent obsloužit, takže samotný její kód je v dané situaci nedůležitý.

Důležitý je ovšem intent filter. Musíme nastavit ACTION_VIEW, CATEGORY_DEFAULT, aby to vůbec fungovalo. A potom ještě budeme filtrovat podle URI. Pokud bude protokolhttp  a hostgoogle.com  anebo www.google.com , Intent projde. Jen podotknu, že to, že je scheme a host specifikovaný na jednom elementu <data>, není vůbec nijak důležité.

<activity
    android:name=".OnlyGoogleComActivity"
    android:label="OnlyGoogleComActivity" >
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />

        <category android:name="android.intent.category.DEFAULT" />

        <data
            android:host="google.com"
            android:scheme="http" />

        <data android:host="www.google.com" />
    </intent-filter>
</activity>

NonexistentUriActivity

NonexistentUriActivity má kód totožný s OnlyGoogleComActivity. Ani intent filtry se nijak neliší – ve skutečnosti jen v jednom řádku:

<activity
    android:name=".NonexistentUriActivity"
    android:label="NonexistentUriActivity" >
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />

        <category android:name="android.intent.category.DEFAULT" />

        <data
            android:host="google.com"
            android:scheme="http" />
        <data android:host="www.google.com" />
        <data android:path="/asdasd" /> <!-- Navíc -->
    </intent-filter>
</activity>

Přidali jsme řádek s neexistující path. Tím se ovšem intent filter dost rapidně změnil – zatímco OnlyGoogleComActivity přijímala URI s jakoukoli cestou, NonexistentUriActivity chce pouze cestu přesně se rovnající /asdasd. Tím pádem náš intent nesplní a v nabídce se nezobrazí.

NonexistentAndExistentUriActivity

To ale můžeme napravit přidáním dalšího elementu <data> se správnou path. Activity je opět totožná (až na svůj název, samozřejmě).

<activity
    android:name=".NonexistentAndExistentUriActivity"
    android:label="NonexistentAndExistentUriActivity"
    android:permission="com.example.intenty.permission.START_ACTIVITY_PERMISSION" >
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />

        <category android:name="android.intent.category.DEFAULT" />

        <data
            android:host="google.com"
            android:scheme="http" />
        <data android:host="www.google.com" />
        <data android:path="/asdasd" />
        <data android:path="/webhp" /> <!-- Navíc -->
    </intent-filter>
</activity>

Asi jste si všimli, že jsme u <activity>  použili atribut android:permission. Ten určuje permission, která je potřeba pro spuštění dané Activity. Není samozřejmě potřeba v rámci aplikace, do níž Activity přísluší.

Tu permission musíme ještě definovat, což ale už umíme. Jako potomka manifestu přidejte:

<permission
    android:name="com.example.intenty.permission.START_ACTIVITY_PERMISSION"
    android:label="@string/perm_label"
    android:description="@string/perm_description" />

Později i vytvoříme aplikaci, kde si ukážeme, že bez permission se Activity nezobrazí v seznamu plnitelů implicitního intentu.

NoDefaultCategoryActivity

Trochu mimo stojí NoDefaultCategoryActivity. Javový kód má zase nicnedělající, intent filter velmi velkorysý (bere ACTION_VIEW, ACTION_EDIT i ACTION_PICK  a mnoho různých protokolů), ovšem chybí CATEGORY_DEFAULT. Protože launcherový Intent neprojde, neumí NoDefaultCategoryActivity splnit žádný implicitní Intent.

<activity
    android:name=".NoDefaultCategoryActivity"
    android:label="NoDefaultCategoryActivity" >
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <action android:name="android.intent.action.EDIT" />
        <action android:name="android.intent.action.PICK" />

        <data android:scheme="http" />
        <data android:scheme="https" />
        <data android:scheme="content" />
        <data android:scheme="file" />
    </intent-filter>
</activity>

WebActivity

Nakonec zkusíme trochu zapracovat i na kódu samotné Activity a zkusíme si vyrobit „prohlížeč”. Ve skutečnosti půjde vlastně jen o Activity s WebView, která přijímá všechny webové adresy a ve WebView je zobrazí.

public class WebActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.web_activity);

        Intent i = getIntent();
        WebView wv = (WebView)findViewById(R.id.web_view);

        wv.loadUrl(i.getDataString());
    }
}

Web_activity.xml je opravdu triviální:

<?xml version="1.0" encoding="utf-8"?>
<WebView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/web_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

Intent filter přijme všechny webové URL. CATEGORY_BROWSABLE je tam proto, aby byla WebActivity schopna přijmout i Intenty z prohlížeče, WebView a podobných.

<activity
    android:name=".WebActivity"
    android:label="WebActivity" >
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />

        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />

        <data android:scheme="http" />
        <data android:scheme="https" />
    </intent-filter>
</activity>

WebView i vestavěný prohlížeč (jak je to u ostatních, nevím) mají totiž jednu vlastnost (již jsem odpozoroval, nikde jsem o tom nečetl, takže to možná řeknu trochu nepřesně – pokud někdo víte víc nebo máte odkaz, rád se vše v komentářích dozvím): Jak při kliknutí na odkaz, tak při přesměrování vysílají nový implicitní intent. Na jednu stranu díky tomu můžeme zobrazit třeba odkaz na Wikipedii ve wikipedijní aplikaci, na druhou stranu to může trochu obtěžovat (třeba zrovna v případě, že spustíte náš googlovský Intent v WebActivity: Protože jsme na telefonu, Google nás přesměruje na mobilní verzi, a pokud jste neřekli, že WebView bude spouštět takové Intenty vždy, znovu vám vyskočí dialog s výběrem plnící Activity.)

VyvijimeProAndroidViewerActivity

Abychom si vyzkoušeli android:pathPattern, přidáme do SenderActivity nový Intent:

intents[1] = new Intent(Intent.ACTION_VIEW,
        Uri.parse("http://m.zdrojak.cz/clanky/vyvijime-pro-android-zaciname/"));

My chceme vytvořit Activity, která zobrazí WebView a v něm článek z tohoto seriálu, ale chceme, aby se nabídla pouze pro články seriálu Vyvíjíme pro Android a žádné jiné. Když se podíváme, zjistíme, že mají společnou část url vyvijime-pro-android. Z toho budeme vycházet.

Nejprve layout. Potřebujeme WebView, ale dáme naší Activity ještě nějaký hezký nadpis, abychom se trochu odlišili od WebActivity.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#f00"
        android:gravity="center_horizontal"
        android:text="@string/zdrojak_title"
        android:textAppearance="?android:attr/textAppearanceLarge" />

    <WebView
        android:id="@+id/web_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

</LinearLayout>

VyvijimeProAndroidViewerActivity je prakticky totožná s WebActivity.

public class VyvijimeProAndroidViewerActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.zdrojak_web_view);

        Intent i = getIntent();
        WebView wv = (WebView)findViewById(R.id.web_view);

        wv.loadUrl(i.getDataString());
    }
}

Intent filtery v manifestu jsou už poněkud komplikovanější. Vedle nastavení ACTION_VIEW a CATEGORY_DEFAULT musíme nastavit i scheme (bez něj by koneckonců neměl host smysl), host (jak na desktopový, tak na mobilní Zdroják) a potom samotný partPattern. Ten není nijak složitý. Protože před vyvijime-pro-android máme /clanky/, dáme před něj .*  tečka-hvězdička). Za ním je potom samotný název článku, takže další tečka-hvězdička.

<activity
    android:name=".VyvijimeProAndroidViewerActivity"
    android:label="VyvijimeProAndroidViewerActivity" >
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />

        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="http" />
        <data android:scheme="https" /> <!-- Co kdyby někdy... -->
        <data android:host="www.zdrojak.cz" />
        <data android:host="m.zdrojak.cz" />
        <data android:pathPattern=".*vyvijime-pro-android.*" />
    </intent-filter>
</activity>

V tomhle případě by asi stejnou funkci zastal atribut android:pathPrefix="/clanky/vyvijime-pro-android", ale jednak mi přijde méně obecný (je mnohem pravděpodobnější, že se změní url okolo samotného identifikátoru článku, než identifikátor článku) a jednak jsme si takhle vyzkoušeli pathPattern, který je složitější.

Asi jste si už všimli teček před názvem Activity v android:name. Znamená to, že je Activity z balíčku definovaného atributem package na <manifest>, stejně tak jsme mohli napsat com.example.intenty.VyvijimeProAndroidViewerActivity. A dokonce to funguje i bez tečky!

Takhle vybíráme Activity na Androidu 4.1

Krásná VyvijimeProAndroidViewerActivity .

A co jiná scheme?

Dám vám dva příklady.

Content provider

Zkusíme otevřít URI z nějakého content provideru (to si všechno vysvětlíme příště). Aby to bylo co nejjednodušší, bude to dost možná neexistující URI. Jde jen o to, že se spustí (a možná zase hned vypne) Activity upravující předaný kontakt.

URI kontaktu je definována jako konstanta ContactsContract.Contacts.CONTENT_URI. Metodou ContentUris.withAppendedId() přidáme id kontaktu (zde 5).

i = new Intent(Intent.ACTION_EDIT, ContentUris.withAppendedId(
        ContactsContract.Contacts.CONTENT_URI, 5));
intents[2] = i;

Text viewer

A co takhle zobrazit text? Tady použijeme scheme file a také budeme testovat mimeType. Metoda copyFileToSD() zkopíruje obsah souboru /raw/text.txt do souboru na SD kartě s předaným jménem. Environment.getExternalStorageDirectory().getAbsolutePath() vrátí absolutní cestu ke kořenovému adresáři externího úložiště.

Abychom ale mohli pracovat s external storage, musíme do manifestu přidat ještě  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> .

String path = Environment.getExternalStorageDirectory()
        .getAbsolutePath();
path += File.separator;
path += "zdrojak_intent_app_text_file.txt";
copyFileToSD(path);
i = new Intent(Intent.ACTION_VIEW);
// URI a MIME musíme nastavit najednou!
i.setDataAndType(Uri.fromFile(new File(path)), "text/plain");
intents[3] = i;

Layoutem třídy TextViewerActivity je TextView ve ScrollView. Samotná TextViewerActivity vypadá takhle:

public class TextViewerActivity extends Activity {

    @Override
    public void onCreate(Bundle icicle){
        super.onCreate(icicle);
        setContentView(R.layout.text_viewer);
        FileInputStream in = null;
        try {
            in = new FileInputStream(getIntent().getData().getPath());
            byte[] buffer = new byte[in.available()];
            in.read(buffer);

            String text = new String(buffer);

Pomocí getIntent().getData().getPath() získáme absolutní cestu k souboru, ale bez scheme file://. Potom soubor přečteme a jeho obsah dáme do řetězce text. Vím, že to není zrovna optimální řešení, kdybychom neměli garantováno, že soubor bude malý, chtělo by to číst ho po částech.

            ((TextView)findViewById(R.id.text_view)).setText(text);
        } catch (FileNotFoundException e) {
        } catch (IOException e) {
        } finally{
            if(in != null){
                try {
                    in.close();
                } catch (IOException e) {
                }
            }
        }
    }
}

Zbytek třídy je naplnění TextView daty, ošetření chyb a zavření streamu.

A nakonec to pro nás nejdůležitější – intent filtry:

<activity
    android:name=".TextViewerActivity"
    android:label="TextViewerActivity" >
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />

        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />

        <data
            android:mimeType="text/*"
            android:scheme="file" />
    </intent-filter>
</activity>

Ještě otestujeme, že permissions fungují.

Vytvořme nový projekt, se jménem třeba Permission Test. Hlavní Activity pojmenujeme MainActivity a její kód bude takový:

public class MainActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Intent i = new Intent(Intent.ACTION_VIEW);
        i.setData(Uri.parse("http://www.google.com/webhp"));
        startActivity(i);
    }
}

Když ji spustíte, bude NonexistentAndExistentUriActivity ve výpisu chybět.

 

Novinky v Androidu

Určitě jste už zaznamenali, že se objevila nová verze Androidu, 4.1 Jelly Bean. Jsa hrdým vlastníkem Nexusu S mám tu čest s ní už asi dva týdny koexistovat a musím říci, že se opravdu povedla. S ICS byl můj Nexus občas poněkud dýchavičný, což teď zmizelo. Všechno běží hezky plynule a ohromně se zrychlily reakce na tlačítko zapínající/vypínající displej. Google Now vypadá zajímavě, rozpoznávání řeči funguje v angličtině většinou opravdu spolehlivě.

Ale opravdu zajímavou novinkou jsou vylepšené notifikace – dají se roztáhnout a je možno k nim přidat i ovládací prvky (nějak to bylo možné asi i dříve, třeba Poweramp to uměl). V souvislosti s tím jsem začal vážně uvažovat o začlenení notifikací do seriálu, protože díky tomu začaly být poměrně zajímavé a kromě toho ovládací prvky zatím nejsou zrovna běžné, takže aplikace, jež je nabídne, bude vypadat víc cool.

Jelly Bean přinesl také novou verzi API, a to 16. Co je nového, si můžete přečíst třeba v Android 4.1 APIs (jednou z těch novinek jsou právě notification actions).

Jinak ovšem, do té doby, než bychom se případně věnovali novým možnostem, budu dále používat API 15, abych zachoval konzistenci v průběhu seriálu.

ADT Plugin

Změnil se (už je to delší dobu, ale až teď jsem si toho všiml a updatoval jsem ho) ovšem ADT Plugin. Z verze 18 se stala verze 20 (v tuto chvíli je nejaktuálnější 20.0.2), jež přináší mnoho změn. V příštím díle se pokusím změny popsat a vyrobit aktualizovaný návod, jak vytvořit nový projekt. Jak si všimnete v ukázkových zdrojácích, některé z novinek jsem už použil (tvorba vlastní ikonky).

Závěr

Dnes jsme se naučili s intenty a intent filtry, velmi důležitými, šikovnými, ale přitom jednoduchými androidími řídícími pokyny. Také jsme se seznámili s tím, jak používat a vytvářet vlastní permissions. Příště se už opravdu budeme věnovat slíbeným content providerům.

Zdrojové kódy dnešní aplikace si můžete stáhnou tady

Tip na konec

Pokud to má smysl, snažte se místo explicitního spouštění activit vytvořit vlastní intentový (a případně content providerový) protokol. Je to mnohem šikovnější, příjemnější pro uživatele a navíc v souladu s androidí filozofií.

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é.)

Zdroj: https://www.zdrojak.cz/?p=3691