Week 04

Service, Intent, Activity Manager, ButterKnife, SDK Version

Úvod

Torch Application - The Final Part

  • Baterka síce funguje, ale aj tak má zatiaľ svoje nedostatky. Jedným z nich je, že ak sa napr. vypne obrazovka alebo aplikáciu minimalizujete, dôjde k uvoľneniu zdrojov, čo nemusí byť vždy žiadúce. A rovnako tak to nepríjemné blikanie, keď aplikáciu otočíte. Zatiaľ to vyzerá tak, že s tým, čo poznáme, sa už ďalej nepohneme. A preto si predstavíme ďalší stavebný komponent Android aplikácií. Dnes sa pozrieme na služby (slide).

Activity Lifecycle

  • S našou aplikáciou však máme stále jeden veľký problém - po zmene orientácie dôjde k resetnutiu stavu aktivity (spustí sa nanovo). To nie je veľmi dobré, nakoľko môžeme stratiť veľmi dôležité údaje (napr. výsledok športovej činnosti po 30min, stav rozohratej hry, …).

  • Problém by bolo možné vyriešiť zakázaním zmeny orientácie aplikácie. Toto je možné zabezpečiť pridaním nasledujúceho atribútu pre potrebnú aktivitu v súbore s manifestom aplikácie:

    <activity android:name=".MainActivity"
              android:screenOrientation="portrait">
  • Aby sme lepšie porozumeli tomu, prečo je to vlastne tak, pozrime sa na životný cyklus aktivity (slide). Aktivita sa teda môže nachádzať v jednom z týchto stavov:

    • starting - V tomto momente dochádza k spusteniu aktivity a teda jej vytvoreniu ako objektu. Volajú sa postupne metódy onCreate(), onStart() a onResume()
    • running - Stav, v ktorom aplikácia vykonáva svoju činnosť v tzv. UI vlákne - je viditeľná a aktívna.
    • paused - Aplikácia stratila fokus, napr. je prekrytá inou nie plneobrazovkovou priesvitnou aktivitou. Aplikácia je teda čiastočne viditeľná a drží svoj stav. Pri prechode do tohto stavu volá metódu onPause().
    • stopped - Ak je aktivita kompletne prekrytá inou aktivitou, je v tomto stave. Stále síce obsahuje svoje členské premenné, ale nie je používateľovi viditeľná.
    • destroyed - Stav, kedy sa aktivita ukončuje (buď s povolením používateľa alebo automaticky). Ak sa znova zobrazí používateľovi, aktivita je vytvorená nanovo.
  • Basically, whenever Android destroys and recreates your Activity for orientation change, it calls onSaveInstanceState() before destroying and calls onCreate() after recreating. Whatever you save in the bundle in onSaveInstanceState, you can get back from the onCreate() parameter.

  • Vytvoríme teda metódu onSaveInstanceState() a uložíme v nej stav baterky:

    
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putBoolean("state", this.isOn);
    }
    
  • A následne upravíme aj metódu onCreate(), v ktorej v prípade, že bol stav aktivity uložený, tak si ho odpamätáme:

    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    
        if(savedInstanceState != null){
            this.isOn = savedInstanceState.getBoolean("state");
            if(this.isOn == true){
                ImageView image = (ImageView) findViewById(R.id.image);
                Button button = (Button) findViewById(R.id.button);
    
                image.setImageResource(R.drawable.bulb_on);
                button.setText(R.string.turn_off);
            }
        }else{
            this.isOn = false;
        }
    }
    
  • Tento fragment kódu sa nám čiastočne prekrýva s metódou toggle(), takže môžeme urobiť menší refaktoring:

    • vytvoríme súkromnú metódu render(), ktorá len vyrenderuje aktivitu na základe aktuálneho stavu (na základe hodnoty členskej premennej isOn, a
    • aktualizujeme metódu toggle(), z ktorej presunieme všetok fragment kódu týkajúci sa renderovania do metódy render().
  • Metóda toggle() teda bude vyzerať nasledovne:

    
    public void toggle(View view){
        player.start();
        this.isOn = !this.isOn;
        render();
    }
    
  • Metóda render() bude po refaktoringu vyzerať nasledovne:

    
    private void render(){
        Button button = (Button) findViewById(R.id.button);
        ImageView image = (ImageView) findViewById(R.id.image);
        Camera.Parameters params = this.camera.getParameters();
    
        if(this.isOn == true) {
            image.setImageResource(R.drawable.bulb_on);
            button.setText(R.string.turn_off);
    
            params.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
            this.camera.setParameters(params);
            this.camera.startPreview();
        }else{
            image.setImageResource(R.drawable.bulb_off);
            button.setText(R.string.turn_on);
    
            params.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
            this.camera.setParameters(params);
            this.camera.stopPreview();
        }
    }
    
  • Následne metóda onCreate() bude vyzerať takto:

Services Introduction

  • Služba beží na pozadí bez priameho zásahu používateľa. Služba nemá žiadne používateľské rozhranie, nie je nijako zviazaná s UI. (alebo nemusí byť?)

  • Služby sú však spúšťané s vyššou prioritou ako neaktívne aktivity, takže Android ich v prípade nedostatočných systémových prostriedkov neukončí.

  • Tu však treba ešte povedať aj to, čo služba nie je. Takže:

    • (slide) Služba nie je samostatným procesom! Služba beží v rovnakom procese, ako aplikácia, ktorej je súčasťou.
    • (slide) Služba nie je vláknom! Nepracuje mimo hlavné vlákno aplikácie.
  • Android poskytuje aj niektoré systémové služby, ktoré je možné používať v rámci aplikácie, pokiaľ tá má potrebné prístupové práva. Prístup k týmto službám je možný prostredníctvom metódy getSystemService(). Trieda Context poskytuje niekoľko konštánt pre prístup k týmto službám, ako napríklad:

    • ALARM_SERVICE - umožňuje získať prístup ku službe AlarmService.
    • BATTERY_SERVICE - umožňuje získať prístup ku BatteryManager pre správu stavu batérie.

Service Types

  • V Androide máme k dispozícii dva typy služieb (slide):
    1. started - A service is “started” when an application component (such as an activity) starts it by calling startService(). Once started, a service can run in the background indefinitely, even if the component that started it is destroyed. Usually, a started service performs a single operation and does not return a result to the caller. For example, it might download or upload a file over the network. When the operation is done, the service should stop itself.
    2. bound - A service is “bound” when an application component binds to it by calling bindService(). A bound service offers a client-server interface that allows components to interact with the service, send requests, get results, and even do so across processes with interprocess communication (IPC). A bound service runs only as long as another application component is bound to it. Multiple components can bind to the service at once, but when all of them unbind, the service is destroyed.

Service Lifecycle

  • Samotná služba má však podobne ako aktivita svoj vlastný životný cyklus. Pozrime sa naň (slide).

  • Životný cyklus služby je výrazne jednoduchší, ako v prípade aktivity. Počas neho služba prechádza len troma metódami:

    1. onCreate() - význam tejto metódy je rovnaký ako v prípade aktivity - metóda sa spustí pri vytvorení služby,
    2. onStart() - metóda sa spustí po spustení služby (zavolaní metódy startService()), a
    3. onDestroy() - metóda sa spustí pri ukončovaní služby (po zavolaní metódy stopService()).
  • Okrem uvedených metód je možné so službou komunikovať prostredníctvom metódy onStartCommand(). Túto metódu zavolá systém vždy, keď klient spustí službu explicitne volaním metódy startService().

Service Example

  • Vytvoríme si jednoduchú službu s názvom ServiceExample, na ktorej si ukážeme základy fungovania služby. Na vytvorenie použijeme Android Studio, ktoré nám s tvorbou pomôže.

  • Pri vytvorení služby je potrebné službu opísať, resp. zaregistrovať aj v manifeste projektu, aby o nej vedel aj systém sám. To za nás vyrieši Android Studio pridaním nasledovného XML elementu:

    <service
        android:name=".ExampleService"
        android:enabled="true"
        android:exported="true"></service>
  • Význam jednotlivých atribútov je nasledovný:

    • android:name - názov služby, povinný atribút
    • android:enabled - ak má hodnotu true, služba môže byť spúšťaná, v opačnom prípade ju nebude možné vôbec spustiť
    • andoird:exported - ak má hodnotu true, službu môžu volať aj iné komponenty iných aplikácií
  • Na nasledujúcom fragmente kódu si pozrieme, ako služba pracuje. Jedná sa o prázdnu službu len s jej základnými metódami, ktoré budeme volať pomocou nástroja am:

    public class ExampleService extends Service {
        private static final String TAG = "ExampleService";
    
        @Override
        public void onCreate() {
            super.onCreate();
            Log.i(TAG, "Service has been created");
        }
    
        @Override
        public void onDestroy() {
            super.onCreate();
            Log.i(TAG, "Service has been destroyed");
        }
    
        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            Log.i(TAG, "Handling intent with message " + intent.getStringExtra("message"));
            return super.onStartCommand(intent, flags, startId);
        }
    
        @Nullable
        @Override
        public IBinder onBind(Intent intent) {
            Log.i(TAG, "onBind()");
            return null;
        }
    }
  • Službu spustíme volaním

    am startservice -n sk.tuke.smart.torch/.ExampleService

    a vypneme ju volaním

    am stopservice -n sk.tuke.smart.torch/.ExampleService
  • Pokiaľ je služba zapnutá, môžeme s ňou komunikovať posielaním intentov volaním nástroja am s parametrom startservice. Tentokrát však nedôjde k opätovnému spusteniu služby, ale len k zavolaniu metódy onStartCommand(), ktorému je odovzdaný intent:

    am startservice -n sk.tuke.smart.torch/.ExampleService --es message "hello world"

TorchService

  • Pre naše potreby budeme teda používať prvý typ služby - Started Service. Vytvoríme teda novú triedu s názvom TorchService, ktorá bude potomkom triedy Service reprezentujúcej typ služby Started Service.

  • Po vytvorení bude kostra služby vyzerať nasledovne:

    public class TorchService extends Service {
        public TorchService() {
        }
    
        @Override
        public IBinder onBind(Intent intent) {
            // TODO: Return the communication channel to the service.
            throw new UnsupportedOperationException("Not yet implemented");
        }
    }
  • My teda budeme postupovať nasledovne:

    • po vytvorení služby získame referenciu na kameru a otvoríme ju
    • po spustení služby zapneme na kamere blesk
    • pri ukončení služby blesk zasa vypneme a kameru uvoľníme
  • Kód riešenia bude vyzerať nasledovne:

    public class TorchService extends Service {
        private static final String TAG = "TorchService";
        private Camera camera;
        private Camera.Parameters params;
    
        public TorchService() {
        }
    
        @Override
        public IBinder onBind(Intent intent) {
            // TODO: Return the communication channel to the service.
            throw new UnsupportedOperationException("Not yet implemented");
        }
    
        @Override
        public void onCreate() {
            super.onCreate();
    
            this.camera = Camera.open();
            this.params = this.camera.getParameters();
        }
    
        @Override
        public void onStart(Intent intent, int startId) {
            super.onStart(intent, startId);
    
            this.params.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
            this.camera.setParameters(this.params);
            this.camera.startPreview();
        }
    
        @Override
        public void onDestroy() {
            super.onDestroy();
    
            this.params.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
            this.camera.setParameters(this.params);
            this.camera.stopPreview();
    
            this.camera.release();
        }
    }

Spustenie služby pomocou Activity Manager-a

  • Vytvorenú službu môžeme otestovať zatiaľ pomocou nástroja am priamo na Android zariadení: shell adb shell am startservice -n sk.tuke.smart.torch/.TorchService
  • Následne ju môžeme vypnúť pomocou volania am s parametrom stopservice: shell adb shell am stopservice -n sk.tuke.smart.torch/.TorchService

Intent

  • Teraz sa však vráťme späť do samotnej aktivity, z ktorej budeme službu pri zapnutí tlačidla spúšťať a pri vypnutí zasa vypínať. Za tým účelom budeme volať metódy startService() a stopService(). Parametrom tejto metódy však bude objekt typu Intent (slide).

  • Intent v Android-e predstavuje objekt reprezentujúci správu, pomocou ktorej je možné požiadať o vykonanie príslušnej akcie od inej časti aplikácie (komponentu). Rovnako je možné pomocou intentu medzi rozličnými komponentmi komunikovať a odovzdávať si informácie.

  • V našom prípade použijeme intent len na vytvorenie správy, pomocou ktorej zabezpečíme spustenie služby z našej aktivity. To vykonáme aktualizovaním metódy toggle():

    public void toggle(View view){
        MediaPlayer player = MediaPlayer.create(this, R.raw.flashlight_on);
        player.start();
    
        this.state = !this.state;
        Intent intent = new Intent(this, TorchService.class);
        if(this.state == true){
            startService(intent);
        }else{
            stopService(intent);
        }
    
        render();
    }

Conclusion

  • Zhrňme však ešte postup, ktorý sme použili pri práci s hw súčasťami zariadenia. (slide)

  • Až teraz môžeme povedať, že je aplikácia hotová. Bolo by ešte možné vytvoriť niekoľko úprav (napr. zrušiť orientáciu na šírku, aby aplikácia zbytočne neblikala), ale tu sa už dá hovoriť o profite. (slide)

Refactoring with ButterKnife

  • Aj pre Android existuje množstvo knižníc, ktoré nejakým spôsobom rozširujú alebo uľahčujú vývoj aplikácií. Pokiaľ budete hľadať na internete, nájdete rozličné rebríčky typu (slide) “Must have libraries” alebo (slide) “X best Android libraries”] alebo (slide) “Top 5 libraries in 2015” a podobne. V rámci tohto kurzu sa s niektorými z nich tiež zoznámime. A dnes to bude rovno knižnica s názvom ButterKnife.

  • Butterknife is a popular View “injection” library for Android. This means that the library writes common boilerplate view code for you based on annotations to save you time and significantly reduce the lines of boilerplate code written.

ButterKnife Setup

  • Je potrebné upraviť konfiguráciu Gradle pre aplikáciu (app/build.gradle) pridaním nasledovných riadkov do časti dependencies:

    compile 'com.jakewharton:butterknife:8.8.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'

ButterKnife Usage

  • There are three major features of ButterKnife:
    1. Improved View Lookups
    2. Improved Listener Attachments
    3. Improved Resource Lookups

Improved View Lookups

  • Pokiaľ chcete v štandardnom Android SDK získať referenciu na niektorý pohľad, použijete za týmto účelom metódu findViewById(). Ak teda napr. v našej baterke osamostatníme jednotlivé pohľady a vytvoríme z nich členské premenné, bude tento fragment kódu vyzerať nasledovne:

    public class MainActivity extends AppCompatActivity {
        private Button button;
        private ImageView imageView;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            this.button = (Button)findViewById(R.id.button);
            this.imageView = (ImageView)findViewById(R.id.image);
        }
        ...
    }
  • Knižnica ButterKnife umožňuje anotovať členské premenné triedy pomocou @BindView() a identifikátora príslušného pohľadu, čím automaticky vyhľadá príslušný pohľad v rozložení aktivity. Uvedený kód je teda možné prepísať s využitím tejto knižnice nasledovne:

    public class MainActivity extends AppCompatActivity {
        @BindView(R.id.button) Button button;
        @BindView(R.id.image) ImageView imageView;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            ButterKnife.bind(this);
        }
        ...
    }

Improved Listener Attachments

  • Ak chceme ošetriť kliknutie na tlačidlo alebo vo všeobecnosti na pohľad, máme dve možnosti:

    1. buď v XML súbore definujúcom rozloženie aktivity pridáme príslušnému elementu atribút android:onClick, ktorého hodnotou bude názov metódy ošetrujúceho kliknutie, alebo
    2. vytvoríme anonymnú vnútornú triedu (inner-class) v metóde setOnClickListener() objektu príslušného pohľadu, napríklad takto:
    this.button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            toggle(null);
        }
    });
  • Knižnica ButterKnife však ponúka riešenie pomocou anotácie metódy ošetrujúcej kliknutie @OnClick(), kde parametrom je identifikátor príslušného pohľadu. Je však rovnako možné príslušnej metóde pridať v rámci anotácie naraz niekoľko pohľadov, čo je aj náš prípad - metóda toggle() bude ošetrovať kliknutie na tlačidlo aj na obrázok. Treba však samozrejme odstrániť predchádzajúcu používanú metódu.

  • Anotovaná metóda toggle() bude vyzerať nasledovne:

    @OnClick({R.id.button, R.id.imageView})
    public void toggle(View view){
        ...
    }

Min/Target/Compile SDK Version

  • Význam jednotlivých položiek je nasledovný (slide):
    • min sdk version - Is the earliest release of the Android SDK that your application can run on. Usually this is because of a problem with the earlier APIs, lacking functionality, or some other behavioral issue.

    • target sdk version - The version your application was targeted to run on. Ideally this is because of some sort of optimal run conditions. If you were to “make your app for version 19” this is where that would be specified. It may run on earlier or later releases, but this is what you were aiming for. This is mostly to indicate how current your application is for use in the marketplace, etc.

    • compile sdk version - The version of android your IDE (or other means of compiling I suppose) uses to make your app when you publish a .apk file. This is useful for testing your application as it is a common need to compile your app as you develop it. As this will be the version to compile to an APK, it will naturally be the version of your release. Likewise it is advisable to have this match you target sdk version.

  • V našom prípade teda potrebujeme nastaviť hodnotu min sdk a target sdk na 19. V ideálnom prípade aj hodnotu verzie compile sdk. Tá však závisí od nainštalovanej verzie Android SDK build tools.

Additional Resources