niedziela, 7 września 2014

Pojedynek z Androidem - odc.5 serwisy cz.2, broadcast receiver-y, notyfikacje

Kontynuacja poprzedniego odcinka.

Serwisy - co jeszcze o nich należy wiedzieć?  Możemy wyróżnić wśród nich serwisy na żądanie, które działają tylko wtedy, gdy ktoś zleca im coś do przetwarzania. Ostatnio narzekałem na skomplikowaną dość implementację serwisu, teraz mogę powiedzieć że dzięki klasie IntentService przynajmniej pisanie serwisów na żądanie może być proste. Serwisy długo wykonujące mogą z kolei działać praktycznie przez cały czas i służyć np. do monitorowania.  Jedynie ten przykład, gdzie dla zamknięcia serwisu wywoływana jest metoda onStartCommand z akcją zamknięcia wydaje się przekombinowany, ale chyba przy takim API nie dało się inaczej.  Korzystanie z jednego serwisu przez różne aplikacje może być sporym udogodnieniem dla twórców aplikacji. Świadomie musimy określić to w manifeście, więc może nie oznacza to zmniejszenia bezpieczeństwa. W Windows/Windows Phone nie ma takiej możliwości,  serwisy trzecie są związane tylko z daną aplikacją (każda aplikacja trzymana jest w izolowanym kontenerze) i nie wykonują się ciągle przez praktycznie nieokreślony czas (oszczędność zasobów).

Broadcast receivery - wspominałem o tym już wcześniej,  to jest częściowo odpowiednik triggerów i krótko wykonujących się tasków Windows/Windows Phone, przy czym mogą być używane w aktywnościach powiązanych z UI, nie tylko z serwisami. Mogą jawnie odwoływać się do metod aktywności wpływających na interfejs użytkownika.  W Windows/Windows do obsługi części zdarzeń systemowych w aplikacji mamy po prostu API z odpowiednimi zwykłymi zdarzeniami.  W Androidzie mogą podobać się spore możliwości emulacji obejmujące zmianę sposobu zasilania czy stanu naładowania baterii. Istnieje możliwość automatycznego uruchamiania broadcast receiver-ów na podstawie manifestu.  Często są używane do sterowania serwisami, które wykonują dłużej trwające prace.  Sterowanie taskami w zależności od zdarzeń systemowych  jest także bardzo popularne w Windows/Windows Phone.

Notyfikacje - notyfikacje w Android, przynajmniej na ile je poznałem, to ikona na pasku systemowym, która po dotknięciu może wykonać jakąś akcję, z reguły coś otworzyć (w Windows/Windows Phone notyfikacja toast/tile pochodząca od danej aplikacji zawsze w niej cos otwiera, notyfikacje raw mogą wykonać dowolnego taska w tle).  Pasek z notyfikacjami możemy rozwinąć w dół, otrzymując bardziej szczegółowy widok listy notyfikacji otrzymanych przez system z różnych aplikacji.  To jakby centrum akcji z Windows Phone 8.1.  Przy czym w Android toast messages nie są uważane za rodzaj notyfikacji. To osobny niezależny byt, jakby coś pośredniego między notyfikacją toast a tile z Windows/Windows Phone, a przez dowolność akcji mogący także przypominać notyfikację raw.

 

Serwisy cz.2

Serwisy na żądanie

Prostsza implementacja

  • Android uruchamia serwis przy pierwszym żądaniu
  • Serwis pozostaje uruchomiony tak długo jak przetwarzane są zgłoszenia
  • Serwis jest zamykany jak tylko zakończy się przetwarzanie

Android dostarcza identyfikator dla każdego żądania

  • onStartCommand otrzymuje unikalne startId z każdym requestem
  • o zakończeniu requestu informujemy wołając stopSelfResult
    • przekazujemy startId żądania, które chcemy zakończyć
    • jeśli pojawią się dodatkowe żądania, serwis pozostanie uruchomiony
    • jeśli startId pochodzi z ostatniego żądania, serwis zostanie zamknięty

Android może zabić przedwcześnie serwis, jeśli będzie potrzebował zasobów

  • serwis może później zostać ponownie uruchomiony, jeśli zasoby będą dostępne
  • onStartCommand zwraca wartości wskazujące na potrzeby restartu
  • START_REDELIVER_INTENT - restartuj serwis i dostarcz ponownie ostatni Intent
  • START_STICKY - restartuj serwis bez Intentu
  • START_NOT_STICKY – nie restartuj serwisu

Serwis na żądanie - implementacja

public class MyService extends Service  {

        …

        @Override

        public void onCreate()  {

                mHandlerThread = new HandlerThread(“MyService”);

                mHandlerThread.start();

                mHandler  =  new Handler(mHandlerThread.getLooper())   {

                        @Override

                        public void  handleMessage(Message msg)  {

                                Intent intent  = (Intent) msg.obj;

                                int startId = msg.arg1;

                                doWork(intent);

                                stopSelfResult(startId);

                        }

                };

        } 

        …

        @Override

        public int onStartCommand(Intent intent, int flags,  int startId)  {

               Message msg = mHandler.obtainMessage();

               msg.obj  = intent;

               msg.arg1 = startId;

               msg.sendToTarget();

               return 0;

        }

}

IntentService  enkapsuluje serwisy na żądanie

  • ustawia HandlerThread i przekazywanie pracy do tego wątku
  • obsługuje cykl życiowy
    • dostarcza implementację onCreate,  onDestroy i onStartCommand
    • do kontroli zachowania podczas restartu używa metody setIntentRedelivery
      • true - powoduje, że onStartCommand zwraca START_REDELIVER_INTENT
      • false - powoduje, że onStartCommand zwraca START_NOT_STICKY
    • sygnalizuje zakończenie każdego żądania
  • aby wykonywać pracę nadpisujemy onHandleIntent
    • otrzymuje instancję Intent, która została przekazana do onStartCommand
    • wykonuje się w wątku w tle
  • dostarczamy domyślny konstruktor, który wywołuje konstruktor klasy bazowej i przekazuje napis do nadania nazwy wątku

public class MyIntentService extends IntentService  {

      public IBinder onBind(Intent intent)  {

             return null;

      }

      @Override

      protected void onHandleIntent(Intent intent)  {

              String messageText  = intent.getStringExtra(“MessageText”);

              FileOutputStream outStream = FileHelper.openOutStream(this, “xxx.dat”);

              for (int i = 0;  i < 10, i++)  {

                    FileHelper.slowWrite(outStream, messageText);                                     

              }

              FileHelper.closeOutStream(outStream);

       }

       public MyIntentService()  {

              super(“MyIntentService”);

       }

}

Serwisy długo wykonujące

Czas życia serwisu nie musi być powiązany z zadaniami

  • raz uruchomiony może wykonywać się przez czas nieokreślony (wykonuje się dopóki pozwolą na to zasoby lub przyjdzie żądanie zatrzymania - na skutek zaistniałych potrzeb lub jawne)
  • użyteczne dla monitoringu,  wstępnych pobierań itp.
  • zatrzymywanie długo wykonującego się serwisu
    • sam może natychmiast się zatrzymać przez wywołanie metody stopSelf bez parametrów
    • może być zatrzymany z zewnątrz przez wywołanie Context.stopService z odpowiednim Intent-em

public class MonitoringService extends Service  {

        public  static  String  START_ACTION = “com.xx.yyy.START_MONITORING”;

        public  static  String  STOP_ACTION = “com.xx.yyy.STOP_MONITORING”;

 

        HandlerThread mHandlerThread;

        LocationListener mLocationListener = null;

        public IBinder onBind(Intent intent)  {

                return null;

        }

        @Override

        public void onCreate()  {

                mHandlerThread = new HandlerThread(“MyService”);

                mHandlerThread.start();

        } 

        @Override

        public void onDestroy()  {

                mHandlerThread.quit();

                mHandlerThread = null;

                mHandler = null;

        }

        @Override

        public int onStartCommand(Intent intent,  int flags,  int startId)  {

                String action = intent.getAction();

                if  (START_ACTION.equalsIgnoreCase(action))  {

                       startMonitoring();

                } else if  (STOP_ACTION.equalsIgnoreCase(action))  {

                        stopMonitoring();

                        stopSelf();

                }           

                return START_REDELIVER_INTENT;

        }

        private void startMonitoring() {

                if  (mLocationListener == null)  {

                       LocationManager lm = (LocationManager)  getSystemService(LOCATION_SERVICE);

                       mLocationListener = new MyLocationListener();

                       lm.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,  10000, 0,

                       mLocationListener,  mHandlerThread.getLooper());

                }

        }

        private void stopMonitoring() {

               if  (mLocationListener != null)  {

                       LocationManager lm = (LocationManager)  getSystemService(LOCATION_SERVICE);

                       lm.removeUpdates(mLocationListener);

                       mLocationListener  = null;

               }

        }   

}

 

//start

Intent intent = new Intent(this,  MonitoringService.class);

intent.setAction(MonitoringService.START_ACTION);

startService(intent);

 

//stop

Intent intent = new Intent(this,  MonitoringService.class);

intent.setAction(MonitoringService.STOP_ACTION);

startService(intent);

 

Serwisy dostępne dla innych procesów

Serwisy mogą być dostępne dla innych procesów na urządzeniu

  • w tym celu używamy niejawny Intent (Action, Data, Category)
  • Service zawiera w manifeście Intent Filter (takie same zasady jak przy dostępie do aktywności)
  • Service wykonuje się w procesie aplikacji, w której został zdefiniowany

      <service android:name=”.MonitoringService”>

        <intent-filter>

                <action android:name=”com.xx.yyy.START_MONITORING”/>

                <action android:name=”com.xx.yyy.STOP_MONITORING”/>

        </intent-filter>

</service>

      //inna aplikacja, w innym procesie

Intent intent = new Intent();

intent.setAction(”com.xx.yyy.START_MONITORING”);

startService(intent);

 

Przechwytywanie systemowych notyfikacji za pomocą broadcast receiver-ów

Android dostarcza systemowe notyfikacje z rozgłaszaniem

  • Użyteczne do monitorowania zmian w stanie systemu
    • przełączanie z baterii na zewnętrzne zasilanie
    • wyłączenie / włączenie trybu airplane
    • zmiany w statusie sieci
  • Notyfikacje są wysyłane jako instancje Intent
    • Akcja Intent  identyfikuje typ rozgłaszania
    • Dodatkowe informacje często zawarte są w extras
  • Muszą być przechwytywane przez specjalne komponenty zwane broadcast receiver-ami

Broadcast receivers

  • jedyny sposób na odbieranie broadcast-ów
  • muszą dziedziczyć po BroadcastReceiver
  • nadpisujemy metodę onReceive (otrzymujemy Context i Intent)
  • musi nastąpić wyjście z onReceive do 10 sekund

public class MyReceiver extends BroadcastReceiver  {

       public void onReceive(Context c, Intent i)  {

              …

       }

}

Przechwytywanie broadcastów

Używamy Context.registerReceiver do wiązania receivera z broadcastem

  • dostarczamy referencję do instancji receivera
  • dostarczamy IntentFilter z odpowiednią akcją
    • Intent.ACTION_BATTERY_CHANGED
    • Intent.ACTION_AIRPLANE_MODE_CHANGED
  • Używamy Context.unregisterReceiver, gdy nie chcemy już dłużej słuchać

public class BatteryStatus  {

       public final int level;

       …

       public BatteryStatus(Intent intent) {

              level  = intent.getIntExtra(“level”, -1);

              …

       }

}

Android Studio

  • New Android Studio –> Broadcast Receiver

public class BatteryLogReceiver extends BroadcastReceiver {

        public void onReceive(Context context,  Intent intent) {

                  BatteryStatus batteryStatus = new BatteryStatus(intent);

                  …

        }

}

 

public class MainActivity extends Activity {

         BatteryLogReceiver mBatteryLogReceiver = null;

         …

         private void handleActionStartBatteryLog(MenuItem item) {

                if (mBatteryLogReceiver == null)  {

                        mBatteryLogReceiver = new BatteryLogReceiver();

                        IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);

                        registerReceiver(mBatteryLogReceiver,  intentFilter);   

                }                          

         }

         private void handleActionStopBatteryLog(MenuItem item) {

                 if (mBatteryReceiver != null) {

                         unregisterReceiver(mBatteryLogReceiver);

                         mBatteryLogReceiver = null;

                 }                

         }

}

Sticky broadcasts – zawsze wysyłają broadcast do nowo zarejestrowanych odbiorców

Standardowe nie wysyłają nic po rejestracji, tylko jeśli nastąpi zmiana stanu.

Konsola Telnet  - symulacja odłączenia od sieci, zmiany poziomu naładowania baterii

open localhost port_emulatora

power ac off

power capacity 75

Broadcasty i aktywności

Aktywności często potrzebują odpowiedzieć na informacje w broadcastach

  • potrzeba utworzenia asocjacji pomiędzy broadcast receiverem a aktywnością
    • można przekazać referencję aktywności do broadcast receivera
    • często klasy zagnieżdżone są prostszym rozwiązaniem (trzymają referencję do instancji otaczającej klasy)
  • Rejestrujemy odbiorcę w onResume aktywności
  • Odpinamy odbiorcę w onPause aktywności

public class MainActivity extends Activity {

        …

        BatteryDisplayReceiver mDisplayReceiver = null;

        …

        @Override

        protected void onResume() {

               super.onResume();

               mDisplayReceiver = new BatteryDisplayReceiver();

               IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);

               registerReceiver(mDisplayReceiver,  intentFilter);

        }

        @Override

        protected void onPause() {

               super.onPause();

               unregisterReceiver(mDisplayReceiver);

        }

        …

        private void setPowerDisplay(boolean onAC, …) {

              …

        }

        …

        private class BatteryDisplayReceiver extends BroadcastReceiver {

                @Override

                public void onReceive(Context context,  Intent intent) {

                      int level = intent.getIntExtra(“level”, –1);

                      int plugged = intent.getIntExtra(“plugged”, –1);

                      boolean onAC = plugged == BatteryManager.BATTERY_PLUGGED_AC;

                      …

                      setPowerDisplay(onAC, …);

                }

        }

}

Automatyczne wykonywanie broadcast receiver-ów

Mogą być statycznie rejestrowane

  • Umieszczamy Intent Filter dla broadcast receivera w manifeście
    • Android utworzy instancję broadcast receivera
    • uruchomi powiązany proces, jeśli aktualnie nie jest wykonywany
  • Android wymusza bardzo surowe czasowe restrykcje
    • metoda onReceive musi zakończyć się do 10 sekund
    • Component jest uważany za niepoprawny, gdy tylko onReceive powraca
  • Często powiązane z serwisami
    • Informują serwisy o zmianach
    • Używają serwisów do wykonywania każdych powiązanych dłużej trwających operacji

<receiver android:name=”.MyReceiver”>

       <intent-filter>

              <action … />

        </intent-filter>

</receiver>

 

public class MonitoringService extends Service  {

       …

}

 

public class ManageLocationListenerReceiver  extends BroadcastReceiver {

       public void onReceive(Context context, Intent intent)  {

               String action = intent.getAction();

               if (Intent.ACTION_AIRPLANE_MODE_CHANGED.equals(action))  {

                       boolean isOn = intent.getBooleanExtra(“state”, false);

                       String serviceAction = isOn ? MonitoringService.STOP_ACTION  : MonitoringService.START_ACTION;

                       Intent serviceIntent = new Intent(serviceAction);

                       context.startService(serviceIntent);

               }

       }

}

 

<receiver android:name=”.ManageLocationListenerReceiver”>

       <intent-filter>

               <action android:name=”android.intent.action.AIRPLANE_MODE”  />

       </intent-filter>

</receiver>

 

Wyświetlanie notyfikacji

Wady i zalety dialogów i powiadomień toast

  • message dialog:  widoczny, ale jest zbyt nachalny
  • toast message:  nie narzuca się, ale użytkownik może przegapić

Notyfikacje łączą wymagania

  • nie są nachalne - na górze ekranu (na pasku systemowym) pokazuje się ikona
  • użytkownik zobaczy - możliwość rozwinięcia w dół szczegółów
  • mogą być wyświetlane przez aplikacje w tle

Budowanie notyfikacji

Używamy klasy Notification.Builder

  • dla API Level < 11 używamy NotificationCompat.Builder
  • mała ikona:  setSmallIcon
  • tytuł:  setContentTitle
  • tekst:  setContentText
  • pobieranie zbudowanej instancji Notification
    • Notification.Builder < API 16:  getNotification
    • Notification.Builder  >= API 16:  build
    • NotificationCompat.Builder:  build

Wyświetlanie notyfikacji

NotificationManager dostarcza dostęp do systemu notyfikacji

  • NotificationManager jest systemowym serwisem
    • używamy Context.getSystemService z Context.NOTIFICATION_SERVICE
  • do wyświetlenia notyfikacji używamy metody notify
    • na instancji Notification
    • dostarczamy zdefiniowane przez aplikację całkowite id
    • opcjonalnie dołączamy tag dla zwiększenia pewności id
  • możliwość aktualizacji notyfikacji z wywołaniem notify z istniejącym id/tagiem
  • usuwamy notyfikację przez cancel

public class MyLocationListener implements LocationListener  {

      …

      @Override

      public void onLocationChanged(Location location)  {

             …

             MyNotificationHelper.displayNotification(mContext, location);

      }

}

public class MyNotificationHelper  {

       public static final int LOCATION_NOTIFICATION_ID = 1;

       public static void displayNotification(Context context,  Location location)  {

              …

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

              builder.setContentTitle(“XXX”)

                     .setContentText(“message”)

                     .setSmallIcon(R.drawable.icon24x24);

              Notification notification = builder.build();

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

              //String tag = this.getClass().getName();

              mgr.notify(LOCATION_NOTIFICATION_ID,  notification);                   

       }

       public static void removeNotification(Context context)  {

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

       mgr.cancel(LOCATION_NOTIFICATION_ID);

       }

}

Akcja dla notyfikacji

Akcja dla notyfikacji po naciśnięciu

  • używamy setContentIntent
    • wszystko, co powiązane z Intent
    • najczęściej otwiera Activity
  • używamy specjalnej klasy PendingIntent
    • opakowuje Intent w przyszłą akcję
    • używamy PendingIntent.getActivity do opakowania Activity Intent w PendingIntent

Wyświetlenie zawartości adresu po naciśnięciu

Uri uri = Uri.parse(mapUri);

Intent mapIntent = new Intent(Intent.ACTION_VIEW, uri);

PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, mapIntent,  0);

builder. …

      .setContentIntent(pendingIntent);

Brak komentarzy: