Week 08

explicitný a implicitný Intent, intent filter, JSON, HttpURLConnection

Úvod

  • http://sli.do#ki-smart - anketa a otázky pre prednášajúceho
  • hackaton - (slide) - posledný novembrový víkend 25.-27.nov.2017 na tému Hack your life!
  • kultúra - (slide) - v kine úsmev bude budúci týždeň 31.okt.2017 Halloweenska párty spojená s premietaním kultového filmu Shining

Mr. Iľko

  • Dnes nás čaká nová aplikácia na predpoveď počasia. Pracovný názov aplikácie bude Mr. Iľko.
  • Aby sme sa na začiatku nezdržiavali, mám rovno pripravenú kostru projektu, ktorú je možné stiahnuť z tejto linky. Tento projekt zahrňuje:
    • dve aktivity - SearchActivity pre vyhľadávanie a DetailActivity pre zobrazenie výsledkov MrIlko SearchActivity (vľavo) a DetailActivity (vpravo)
    • niekoľko pripravených zdrojov (najmä obrázkových)
    • upravené konfiguračné súbory pre použitie knižnice ButterKnife

Intents

  • O zámeroch sme už hovorili pri baterke, kde sme ich používali na spustenie príslušnej služby. Podobným spôsobom ich použijeme aj tentokrát, keď budeme chcieť z jednej aktivity spustiť inú aktivitu. To však tiež nebude všetko, čo sa dnes o zámeroch naučíme.
  • Existujú však dva typy zámerov:
    • explicitný - špecifikuje komponent (obyčajne triedu), ktorý má byť spustený
    • implicitný - nešpecifikuje komponent, ktorý má byť spustený, ale deklaruje akciu, ktorá má byť vykonaná (komponentom inej aplikácie)
  • Ukážme si teda použitie najprv explicitného Intent-u, kedy po kliknutí na tlačidlo Search dôjde k prechodu z aktivity SearchActivity do aktivity DetailActivity.

Explicit Intent

  • Najprv teda vytvoríme metódu search(), po zavolaní ktorej dôjde k vytvoreniu explicitného intentu. Do tohto intentu pridáme aj extra údaj, ktorým bude zadané mesto pre vyhľadávanie. Následne vytvorený intent odovzdáme metóde startActivity(), ktorá zabezpečí zobrazenie aktivity DetailActivity a doručenie pripraveného intent-u priamo do nej.

  • Predtým však vytvoríme príslušnú členskú premennú, ktorá bude referenciou na objekt typu EditText. Samozrejme priamo použijeme knižnicu ButterKnife, ktorej binding inicializujeme v metóde onCreate() aktivity: ```java @BindView(R.id.edittext_search) EditText etInput;

    @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_search); ButterKnife.bind(this); } ```

  • Následne už vytvoríme samotnú metódu onSearchClicked(): java @OnClick(R.id.button_search) public void onSearchClicked(){ Intent intent = new Intent(this, DetailActivity.class); intent.putExtra("city", etInput.getText().toString()); startActivity(intent); }

Receiving Intent

  • Hneď po vytvorení akvity DetailActivity v metóde onCreate() potrebujeme pristúpiť k odovzdanému intentu. To zabezpečíme volaním metódy getIntent() priamo nad objektom aktivity. Z tohto intentu následne vytiahneme hodnotu parametra city, vytvoríme z neho členskú premennú triedy a toast, kde len overíme jeho prijatie:

    public class DetailActivity extends AppCompatActivity {
        private String city;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_detail);
            ButterKnife.bind(this);
    
            Intent intent = this.getIntent();
            this.city = intent.getStringExtra("city");
            Toast.makeText(this, this.city, Toast.LENGTH_SHORT).show();
        }
    }

Implicit Intent

  • V aktivite DetailActivity si ukážeme použitie implicitného intentu. V pravom dolnom rohu aktivity sa nachádza textový popisok, na ktorom je názov poskytovateľa údajov o počasí. Vytvoríme implicitný intent, pomocou ktorého zabezpečíme, že po kliknutí na tento popisok dôjde k otvoreniu prehliadača s webovou stránkou poskytovateľa.

  • Priamo môžeme otvoriť stránku s počasím pre mesto, ktoré sme dostali pomocou intentu z aktivity SearchActivity. Jej adresa bude v tvare (slide) http://openweathermap.org/find?q=city

  • Vytvoríme metódu onProviderClicked(), ktorá sa zavolá po kliknutí na uvedený popisok:

    @OnClick(R.id.textview_detail_provider)
    public void onProviderClicked(){
        Intent intent = new Intent(
                Intent.ACTION_VIEW,
                Uri.parse("http://openweathermap.org/find?q=" + this.city));
        startActivity(intent);
    }

Launcher Activity

  • Aktuálne máme k dispozícii dve aktivity - ktorá sa bude spúšťať ako prvá?
  • To je zadefinované v manifeste projektu pomocou tzv. (slide) intent filtru aktivitiy.

Intent Filter

  • An intent filter is an expression in an app’s manifest file that specifies the type of intents that the component would like to receive. For instance, by declaring an intent filter for an activity, you make it possible for other apps to directly start your activity with a certain kind of intent. A zasa naopak, ak žiadny intent filter pre aktivitu nezadeklarujete, potom túto aktivitu môžete spustiť len pomocou explicitného intentu.

Downloading Data

  • Aby aplikácia začala konečne prinášať hodnotu, potrebujeme stiahnuť živé údaje zo serveru poskytovateľa pomocou jeho REST API. Adresa pre požiadavku je v tvare (slide): http://api.openweathermap.org/data/2.5/weather?units=metric&q=CITY&APPID=KEY kde:

    • CITY predstavuje mesto, o ktorom chceme získať informácie o počasí, a
    • KEY predstavuje používateľský kľúč pre prístup k informáciám z tohto serveru. Kľúč je možé získať po zaregistrovaní sa na serveri služby OpenWeatherMap.
  • Jednoducho si môžeme otestovať toto spojenie aj pomocou príkazu curl z OS Linux (slide):

    curl "http://api.openweathermap.org/data/2.5/weather?units=metric&q=kosice&APPID=3718d7f90e7b081ca8f46aa4305c05ea" | json_reformat
  • Odpoveď zo servera dostaneme vo formáte JSON a našou úlohou bude získať tieto údaje v rámci našej Android aplikácie.

Class HttpURLConnection

  • Pre stiahnutie týchto údajov použijeme objekt triedy (slide) HttpURLConnection. Môžete sa však stretnúť aj s použitím Apache Http Client-a. Tento však už nie je od verzie Androidu 6.0 (API Level 23) súčasťou Android SDK a je dostupný vo forme externej knižnice.

  • Na to, aby sme mohli v našej aplikácii pracovať so sieťou, musíme mať príslušné povolenie. Toto nastavíme v súbore AndroidManifest.xml nasledovne:

    <uses-permission android:name="android.permission.INTERNET"/>
  • Takže po kliknutí na tlačidlo Search dôjde k stiahnutiu údajov priamo zo služby poskytovateľa údajov o počasí:

    @OnClick(R.id.button_search)
    public void onSearchClicked(){
        String city = this.etInput.getText().toString();
    
        try {
            URL url = new URL("http://api.openweathermap.org/data/2.5/weather?units=metric&APPID=3718d7f90e7b081ca8f46aa4305c05ea&q=" + city);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.connect();
    
            Log.i(TAG, "Connecting to " + url.toString());
            Log.i(TAG, "Status: " + connection.getResponseCode());
    
            Intent intent = new Intent(this, DetailActivity.class);
            intent.putExtra("city", city);
            startActivity(intent);
        } catch (IOException e) {
            Toast.makeText(this, "Connection error", Toast.LENGTH_SHORT).show();
            e.printStackTrace();
        }
    }
  • Miesto výsledku však dostaneme výnimku (slide) android.os.NetworkOnMainThreadException.

Exception android.os.NetworkOnMainThreadException

  • K tejto výnimke dôjde vtedy, ak sa aplikácia snaží vykonávať sieťové operácie v hlavnom vlákne.

  • Po spustení aplikácie totiž systém vytvorí vlákno, v ktorom je aplikácia vykonávaná. Je označovaná ako “main”, teda hlavné vlákno. Toto vlákno zodpovedá za úlohy súvisiace s používateľským rozhraním. Ak by sme v hlavnom vlákne spustili úlohu, ktorej vykonanie trvá príliš dlho, aplikácia sa bude javiť ako nefunkčná aj napriek tomu, že vlastne len čaká na dokončenie tejto dlhotrvajúcej úlohy.

  • Dlhotrvajúca úloha nemusí vždy trvať dlho, ale jej dĺžka môže byť neurčitá. Medzi takéto úlohy môžeme zaradiť napríklad sieťové operácie alebo databázové operácie.

  • Dlhotrvajúcu úlohu si môžeme vyskúšať veľmi jednoducho - stačí, ak po stlačení tlačidla Search spustíme toto:

    while(True){
        Log.i(TAG, "Thinking about the Answer to the Ultimate Question of Life, the Universe, and Everything...");
    }
  • Po kliknutí na tlačidlo sa aplikácia zahryzne a nereaguje na žiadne ďalšie podnety, resp. vstupy. Z tohto stavu nás vyseká už len Android sám tým, že sa ponúkne nereagujúcu aktivitu vypnúť.

  • V prípade, že potrebujeme spustiť nejakú dlhotrvajúcu operáciu, ktorá nemusí byť nutne sieťová, musíme to urobiť v samostatnom vlákne. Na možnosti, ktoré ponúka Android pre takéto úlohy sa následne pozrieme.

Threading in Android

  • Čo je to vlákno? Definícia z wikipédie. (slide)
  • Ilustrácia o spolubývajúcich (viaceré vlákna) z jednej izby (proces) čítajúcich jedny skriptá (zdieľaná pamäť) - v jednom čase môže ku skriptám (zdieľaným údajom) pristupovať len jeden z nich. (Stack Overflow)
  • V Android-e máme niekoľko možností, ako môžeme niektorú (dlhotrvajúcu) úlohu vykonávať v samostatnom vlákne, resp. na pozadí.
  • Jedno z nich je vytvoriť štandardné Javové vlákno (slide). Nie je to však odporúčaný postup, nakoľko tu vzniká niekoľko problémov, ako napr.: synchronizácia s hlavnou triedou, ak z vlákna budete chcieť vrátiť údaje späť UI.
  • Android poskytuje alternatívny prístup ku práci s vláknami cez android.os.Handler alebo AsyncTask.
  • Trieda Handler sa používa ako jednoduchý komunikačný kanál medzi vláknami. Ani ona nám však nepomôže, ak chceme v rámci vlákna spracovávať sieťovú komunikáciu. Tu musíme použiť triedu AsyncTask.

Class AsyncTask

  • AsyncTask enables proper and easy use of the UI thread. This class allows to perform background operations and publish results on the UI thread without having to manipulate threads and/or handlers.
  • Trieda AsyncTask je abstraktná. Ak ju chceme použiť, musíme z nej vytvoriť podtriedu (slide). Používa generiká a premenlivý počet parametrov.
  • Význam jednotlivých parametrov je nasledovný:
    • Params - typ parametrov odoslaných úlohe pri spustení.
    • Progress - the type of the progress units published during the background computation.
    • Result - the type of the result of the background computation. Not all types are always used by an asynchronous task. To mark a type as unused, simply use the type Void.
  • When an asynchronous task is executed, the task goes through 4 steps:
    1. onPreExecute() - invoked on the UI thread before the task is executed. This step is normally used to setup the task, for instance by showing a progress bar in the user interface.
    2. doInBackground(Params...) - invoked on the background thread immediately after onPreExecute() finishes executing. This step is used to perform background computation that can take a long time. The parameters of the asynchronous task are passed to this step. The result of the computation must be returned by this step and will be passed back to the last step. This step can also use publishProgress(Progress...) to publish one or more units of progress. These values are published on the UI thread, in the onProgressUpdate(Progress...) step.
    3. onProgressUpdate(Progress...) - invoked on the UI thread after a call to publishProgress(Progress...). The timing of the execution is undefined. This method is used to display any form of progress in the user interface while the background computation is still executing. For instance, it can be used to animate a progress bar or show logs in a text field.
    4. onPostExecute(Result) - invoked on the UI thread after the background computation finishes. The result of the background computation is passed to this step as a parameter.
  • Vzhľadom na uvedené teda vytvoríme vlastnú triedu s názvom ForecastTask, ktorá bude potomkom triedy AsyncTask

Class ForecastTask

  • Začneme identifikáciou generických typov:

    • Akého typu budú vstupné údaje úlohy? Čo bude vstupom tejto úlohy? (názov mesta, takže String)
    • Akého typu bude výstup z úlohy? Čo táto úloha vráti? (z webu providera príde JSON objekt serializovaný ako reťazec, ktorý v Jave vieme reprezentovať triedou JSONObject. Tento údajový typ však nie je vhodný, nakoľko ho nemôžeme zabaliť do intentu. Preto bude nakoniec lepšie použiť na prenos údajový typ String)
    • Budeme potrebovať nejakým spôsobom reprezentovať priebeh operácie? (max. tak točiacim sa kolieskom, takže tento typ zostane prázdny (Void))
  • Prázdna trieda bude vyzerať teda nasledovne:

    public class ForecastTask extends AsyncTask<String, Void, String> {
    }
  • Následne vytvoríme konštruktor tridy, ktorému predáme referenciu na aktivitu, v ktorej bola inštancia objektu AsyncTask vytvorená:

    public class ForecastTask extends AsyncTask<String, Void, String> {
        private final SearchActivity activity;
    
        public ForecastTask(SearchActivity activity) {
            this.activity = activity;
        }
    }
  • Nakoniec vytvoríme metódu doInBackground(), ktorá bude spustená v samostatnom vlákne a zabezpečí stiahnutie údajov o počasí zo služby OpenWeatherMap: ```java @Override protected String doInBackground(String… cities) { this.city = cities[0];

      try {
          URL url = new URL("http://api.openweathermap.org/data/2.5/weather?units=metric&APPID=3718d7f90e7b081ca8f46aa4305c05ea&q=" + city);
          HttpURLConnection connection = (HttpURLConnection) url.openConnection();
          connection.connect();
    
          Log.i(TAG, String.format("Connecting to %s", url.toString()));
          Log.i(TAG, String.format("HTTP Status Code: %d", connection.getResponseCode()));
    
          if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
              return null;
          }
    
          BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
          StringBuilder stringBuilder = new StringBuilder();
    
          String line;
          while ((line = bufferedReader.readLine()) != null) {
              stringBuilder.append(line + '\n');
          }
    
          Log.i(TAG, String.format("GET: %s", stringBuilder.toString()));
    
          return new stringBuilder.toString();
      } catch (MalformedURLException e) {
          e.printStackTrace();
      } catch (IOException e) {
          e.printStackTrace();
      }
    
      return null;

    } ```

Processing Downloaded Data

  • Keď sa tieto údaje úspešne stiahnu a vytvorí sa z došlého reťazca objekt typu JSONObject, bude tento objekt odovzdaný metóde onPostExecute(). Ak sa tak stalo, vytvoríme explicitný intent, pomocou ktorého necháme spustiť aktivitu DetailActivity a zabalíme do neho aj stiahnuté údaje v reťazcovej podobe s kľúčom json.

  • Samozrejme je potrebné overiť aj prípad, či sme príslušný objekt naozaj dostali a nedostali sme náhodou hodnotu null. Táto hodnote reprezentuje prípad, kedy sa údaje nepodarilo úspešne stiahnuť.

  • Výsledná podoba metódy onPostExecute() bude vyzerať nasledovne:

    @Override
    protected void onPostExecute(String json) {
        super.onPostExecute(json);
    
        // if no data are provided, stay with search activity
        if(json == null){
            Toast.makeText(this.activity, "Connection error", Toast.LENGTH_SHORT).show();
            return;
        }
    
        // prepare intent to start DetailActivity
        Intent intent = new Intent(this.activity, DetailActivity.class);
        intent.putExtra("json", json);
        this.activity.startActivity(intent);
    }
  • Následne už stačí upraviť metódu onSearchClicked(), kde vytvoríte inštanciu tejto triedy a zavoláte nad ňou metódu execute() s potrebným parametrom (mestom):

    @OnClick(R.id.button_search)
    public void onSearchClicked(){
        // get city from etInput field
        String city = etInput.getText().toString();
        Log.i(TAG, "Downloading forecast for " + city);
    
        // execute async task
        ForecastTask task = new ForecastTask(this);
        task.execute(city);
    }

Progress Bar

  • Samozrejme môžeme v aplikácii zobraziť aj priebeh operácie pomocou točiaceho sa kolieska reprezentujúceho všeobecný stav - Pracujem. Pridáme teda metódu onPreExecute():

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
    
        this.progress = new ProgressDialog(SearchActivity.this);
        this.progress.setMessage("Searching...");
        this.progress.show();
    }

    A v metóde onPostExecute() zasa tento priebeh zrušíme zavolaním: java this.progress.dismiss();

Complete Source code

Celkový kód bude vyzerať nasledovne:

#include "src/ForecastTask.java"

Presentation of data

  • Následne už treba iba rozbaliť prijatý intent v metóde onCreate() a pomocou získaných hodnôt naplniť aktivitu. Nižšie sa nachádza celkový výpis triedy DetailViewActivity:
#include "src/DetailActivity.java"
  • Ak ste postupovali správne, načítané údaje sa vám zobrazia: Zobrazenie načítaných údajov

Implicit Intent

  • Implicitný zámer môžeme vytvoriť ešte jeden - na základe získaných geo informácií (zemepisná šírka a výška) - po kliknutí na mesto sa zobrazí mapa s jeho lokáciou.

    @OnClick(R.id.textview_detail_city)
    public void onLocationClicked(){
        Intent intent = new Intent(
                Intent.ACTION_VIEW,
                Uri.parse(String.format("geo:%f,%f?z=10", this.latitude, this.longitude)));
        startActivity(intent);
    }
  • Tu však pozor - môže sa stať, že aktivita pre zobrazenie daného zámeru neexistuje, čo by mohlo viesť k zrúteniu aplikácie. Preto tento fragment kódu obaľte do odchytenia výnimky:

    @OnClick(R.id.textview_detail_city)
    public void onLocationClicked(){
        Intent intent = new Intent(
                Intent.ACTION_VIEW,
                Uri.parse(String.format("geo:%f,%f?z=10", this.latitude, this.longitude)));
    
        try {
            startActivity(intent);
        }catch (ActivityNotFoundException e){
            Toast.makeText(this, "Geolocation viewer is not installed.", Toast.LENGTH_SHORT).show();
        }
    }

Additional Resources

  • slides - prezentácia z tohto týždňa
  • Intents and Intent Filters
  • JSON - Homepage of JSON (JavaScript Object Notation) - the lightweight data-interchange format.
  • Temps - A simple but smart weather app. (odkukaný layout pre Mr Iľka)
  • OpenWeatherMap REST API - dokumentácia API služby openweathermap.org
  • Threads - Android SDK dokumentácia ku vláknam
  • AsyncTask - AsyncTask enables proper and easy use of the UI thread. This class allows you to perform background operations and publish results on the UI thread without having to manipulate threads and/or handlers.