Dobrze zaplanowana i niezawodna komunikacja między aktywnościami i usługą to podstawowy element każdej rozbudowanej aplikacji na platformę Android. W tym celu warto zapoznać się z zastosowanym w darmowym komunikatorze Gandu modelu wymiany danych wewnątrz aplikacji.
O aplikacji
Gandu to komunikator internetowy na platformę Android, który umożliwia komunikację użytkowników społeczności GG. Za pomocą programu można nie tylko prowadzić rozmowy z kontaktami zaimportowanymi z serwerów Gadu-Gadu, ale także korzystać z dodatkowych funkcji, które można wykorzystać bazując na protokole GG. W porównaniu z płatnym odpowiednikiem stworzonym przez GG Network, Gandu oferuje znacznie większą funkcjonalność. Użytkownik ma możliwość:
- importu oraz eksportu listy kontaktów,
- zmiany stanu oraz opisu,
- rejestracji nowego konta GG,
- edycji listy kontaktów (dodawanie, usuwanie, ignorowanie kontaktów, obsługa grup),
- przesyłania plików (w tym zdjęć zrobionych wbudowanym aparatem),
- prowadzenia rozmów konferencyjnych1,
- wyświetlania awatarów na liście kontaktów,
- przechodzenia do zasobów URL, dzięki obsłudze linków w opisach i rozmowach.
Oprócz tego Gandu umożliwia geolokalizację użytkowników na podstawie wbudowanego modułu AGPS i danych udostępnianych przez operatora sieci komórkowej.
Architektura aplikacji
W komunikatorze został zastosowany rozbudowany model komunikacji między aktywnościami a usługą. Za komunikację z serwerami Gadu-Gadu odpowiada usługa (ang. service) GanduService.java, która odbiera i wysyła komunikaty do serwerów Gadu-Gadu. Odebrany komunikat jest interpretowany w usłudze, a niezbędne dane wykorzystywane dalej przez aktywności (ang. activities) są pakowane do obiektu Bundle oraz wysyłane do aktywności. Model aplikacji został przedstawiony na rysunku nr 1.
Implementacja
Usługa na platformie Android może zostać uruchomiona na dwa sposoby. Poprzez wywołanie metody startService lub bindService wewnątrz aktywności. Metody różnią się sposobem oddziaływania na cykl życia usługi. Usługa uruchomiona poprzez wywołanie metody startService działa do momentu bezpośredniego zakończenia jej przez aktywność, metodą stopService, lub przez samą siebie, metodą stopSelf. W przypadku kiedy usługa została wcześniej uruchomiona, wywołanie metody startService nie będzie miało wpływu na jej cykl życia – usługa pozostanie uruchomiona. Wywołanie metody bindService spowoduje uruchomienie usługi pod warunkiem, że nie została uruchomiona wcześniej, np. przez inną aktywność. W takim przypadku usługa będzie działać do momentu istnienia powiązania z aktywnością. Jeśli np. aktywność, która uruchomiła usługę poprzez wywołanie metody bindService, zakończy swoje działanie, to spowoduje to także zakończenie działania usługi. Zatem w takim przypadku cykl życia usługi jest ściśle powiązany z cyklem życia wywołującej ją aktywności. Dodatkowym efektem wywołania metody bindService jest powiązanie usługi z aktywnością, umożliwiające interakcje pomiędzy nimi.
Do metody startService jako argument przekazywana jest intencja wskazująca klasę uruchamianej usługi. Metoda bindService jako argumenty przyjmuje:
- intencję wskazującą na klasę uruchamianej usługi,
- obiekt typu ServiceConnection, wykorzystywany podczas zestawiania połączenia oraz rozłączania aktywności z usługą,
- flagę połączenia, do wyboru jest kilka zdefiniowanych flag, m.in.:
- BIND_AUTO_CREATE – automatycznie tworzy (uruchamia) usługę, jeśli nie została wcześniej uruchomiona,
- BIND_DEBUG_UBIND – umożliwia debugowanie błędów związanych z wywołaniem metody unbind.
Aby uruchomić i połączyć się z usługą, która ma działać w tle, niezależnie od cyklu życia aktywności, należy najpierw uruchomić usługę poprzez wywołanie metody startService, a następnie powiązać ją z aktywnością poprzez wywołanie metody bindService.
Uruchomienie i zestawienie połączenia pomiędzy aktywnością i usługą obrazuje poniższy przykład z aplikacji Gandu. Aktywność GanduClient (okno logowania) uruchamia usługę GanduService, której cykl życia jest niezależny od cyklu życia aktywności GanduClient.
//Stworzenie intencji wskazującej na klasę usługi GanduService
Intent intent = new Intent(getApplicationContext(), GanduService.class); //uruchomienie usługi GanduService
startService(intent);
//zestawienie połączenia aktywności GanduClient z usługą GanduService bindService(intent,mConnection,BIND_AUTO_CREATE);
Powyższe instrukcje wywoływane są w metodzie onCreate klasy GanduClient. mConnection jest obiektem implementującym metody interfejsu ServiceConnection. Interfejs ServiceConnection posiada dwie metody:
- onServiceConnected – wywoływana w momencie zestawienia połączenia z usługą,
- onServiceDisconnected – wywoływana w momencie zerwania połączenia z usługą.
Metoda onServiceConnected jako drugi argument przyjmuje obiekt typu IBinder (IBinder to interfejs umożliwiający interakcję ze zdalnym obiektem. Jest częścią mechanizmu zdalnego wywoływania procedur (ang. Remote Procedure Call), służącego do komunikacji wewnątrz i pomiędzy procesami). Obiekt ten jest przekazywany do konstruktora klasy Messenger, omówionej w dalszej części.
Wywołanie metody bindService skutkuje wywoływaniem w usłudze GanduService metody onBind.
@Override
public IBinder onBind(Intent intent) {
//metoda zwraca obiekt IBinder powiązany z Messengerem usługi
return mMessenger.getBinder();
}
Właśnie w tym momencie powyższa metoda zwraca obiekt typu IBinder, który odbierany jest przez aktywność GanduClient i przekazywany jako argument do poprzednio opisywanej metody onServiceConnected. Obiekt IBinder pobierany jest z obiektu mMessenger, należącego do usługi GanduService. mMessenger jest obiektem klasy Messenger.
Poniższy kod przedstawia budowę metody onServiceConnected, implementowanej przez obiekt mConnection, wywoływanej w momencie zestawienia połączenia aktywności z usługą.
public void onServiceConnected(ComponentName className, IBinder service) {
//Przekazanie obiektu service typu IBinder do konstruktora klasy //Messenger.
mService = new Messenger(service);
//Odesłanie obiektu mMessenger (należącego do klasy GanduClient) //typu Messenger, do usługi GanduService.
try {
Message msg = Message.obtain(null, Common.CLIENT_REGISTER);
msg.replyTo = mMessenger;
mService.send(msg);
} catch (RemoteException e) {
}
}
Podsumowując, zestawienie połączenia aktywności z usługą, której cykl życia nie jest bezpośrednio zależny od cyklu życia aktywności, na przykładzie aplikacji Gandu został zrealizowany w następujący sposób:
- aktywność uruchamia usługę poprzez wywołanie metody startService, a następnie łączy się z nią poprzez wywołanie metody bindService, do której przekazywany jest obiekt mConnection implementujący interfejs ServiceConnection,
- po wykryciu próby połączenia, wewnątrz usługi wywoływana jest metoda onBind, która w wyniku swojego działania zwraca obiekt typu IBinder, umożliwiający komunikację z usługą,
- w wyniku zestawienia połączenia pomiędzy aktywnością i usługą, wewnątrz aktywności, w obiekcie mConnection wywoływana jest metoda onServiceConnected, do której przekazane zostaje obiekt typu IBinder usługi. Po utworzeniu obiektu typu Messenger aktywność wysyła do usługi swój obiekt typu Messenger.
Obiekt klasy Messenger jest powiązany z obiektem klasy Handler. Handler przyjmuje przychodzące do niego wiadomości i odpowiednio je interpretuje. Przy pomocy Messengera możliwa jest komunikacja pomiędzy procesami. W tym celu należy stworzyć obiekt klasy Messenger, powiązany z obiektem klasy Handler, w jednym procesie i przekazać go do innego procesu.
Tak też dzieje się w przypadku aplikacji Gandu. Najpierw, po zestawieniu połączenia aktywności z usługą, aktywność GanduClient odbiera od GanduService obiekt typu IBinder. Przy pomocy tego obiektu tworzy obiekt mService klasy Messenger (patrz wyżej, metoda onServiceConnected). mService jest powiązany z Handlerem należącym do usługi GanduService. GanduClient od tej pory, poprzez obiekt mService, może wysyłać komunikaty do usługi GanduService.
Klasa Messenger posiada metodę send. Służy ona do wysłania komunikatu do obiektu Handler, związanego z danym Messengerem. Metoda send jako argument przyjmuje obiekt typu Message, który zostanie omówiony w dalszej części. Metoda send może wyrzucić wyjątek RemoteException w przypadku, kiedy docelowy Handler nie istnieje. Taka sytuacja nie jest wykluczona, ponieważ docelowy Handler może istnieć w osobnym procesie.
Pierwszym komunikatem wysyłanym przez aktywność GanduClient do usługi jest komunikat, do którego załączony zostaje obiekt Messenger należący do tej aktywności (patrz wyżej, metoda onServiceConnected). Usługa GanduService po odebraniu tego Messengera może wysyłać komunikaty do aktywności GanduClient. Od tej pory możliwa jest komunikacja w obie strony pomiędzy usługą i aktywnością.
Implementacja Handlera polega na stworzeniu klasy dziedziczącej po klasie Handler. Klasa potomna musi implementować metodę handleMessage, aby móc odbierać przychodzące komunikaty. Metoda handleMessage jako argument przyjmuje obiekt typu Message, który został wysłany do Handlera.
Poniżej przedstawiony jest uproszczony przykład, z aplikacji Gandu, implementacji klasy IncomingHandler dziedziczącej po klasie Handler. Dla przejrzystości przykładu pozostawiona została tylko interpretacja komunikatu CLIENT_REGISTER, oraz szkielet komunikatu CLIENT_LOGIN, który zostanie opisany w dalszej części.
class IncomingHandler extends Handler {
//metoda odbierająca i interpretująca przychodzące komunikaty
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case Common.CLIENT_REGISTER:
//mClients jest obiektem typu ArrayList,
//przechowującym obiekty typu Messenger.
mClients.add(msg.replyTo);
break;
case Common.CLIENT_LOGIN:
(?)
break;
}}}
IncomingHandler należy do usługi GanduService, zatem wszystkie komunikaty przysyłane do usługi są interpretowane właśnie wewnątrz tej klasy, w metodzie handleMessage. Na powyższym przykładzie widać, że po odebraniu komunikatu typu CLIENT_REGISTER, do listy mClients dodawany jest Messenger nadawcy komunikatu. Komunikat CLIENT_REGISTER wysyłany jest przez aktywność po nawiązaniu połączenia z usługą (patrz wyżej, metoda onServiceConnected). Usługa w aplikacji Gandu zapisuje odebrany Messenger na liście mClients, ponieważ z usługą łączy się kilka aktywności, m.in. GanduClient (okno logowania), ContactBook (okna z kontaktami) czy Chat (okno z zakładkami prowadzonych rozmów).
Powiązanie Handlera z Messengerem jest realizowane w momencie tworzenia obiektu Messenger. Handler przekazywany jest do konstruktora klasy Messenger, jak przedstawiono na poniższym przykładzie.
final Messenger mMessenger = new Messenger(new IncomingHandler());
Obiekt klasy Message służy do transportu informacji. Przy pomocy obiektu tej klasy można przenosić proste, pojedyncze informacje typu int. Służą do tego m.in. pola what, arg1 oraz arg2 klasy Message. Za pośrednictwem tej klasy, wewnątrz pola replyTo, można także przekazać Messengera, poprzez którego odbiorca będzie mógł wysyłać komunikaty zwrotne (patrz wyżej, metoda onServiceConnected). Aby przesłać więcej danych, klasa Message udostępnia metody setData oraz getData. Metoda setData przyjmuje jeden argument, obiekt klasy Bundle. Aby uzyskać obiekt klasy Message można skorzystać ze statycznej metody obtain klasy Message.
Obiekt klasy Bundle można uzupełniać danymi różnych typów, np. int, boolean, char, byte, String, short, long itp. Wszystkie dane powiązane są z odpowiednim kluczem typu String. Aby pobrać odpowiednią daną z Bundle’a należy znać jej klucz.
Poniżej przedstawiony jest przykład użycia klas Message oraz Bundle. Jest to operacja przesłania informacji z GanduClient do GanduService potrzebnych do zalogowania.
//Metoda wywoływana po naciśnięciu Zaloguj... w aktywności GanduClient
private OnClickListener connectListener = new OnClickListener() {
public void onClick(View v) {
//Pobranie numeru GG wprowadzonego przez użytkownika
mojNumer = ggNumberEdit.getText().toString();
//Pobranie hasła wprowadzonego przez użytkownika
haslo = ggPasswordEdit.getText().toString();
//Utworzenie obiektu Message. Przekazywane argumenty:
//1. Handler odbiorcy ? null, ponieważ obiekt msg zostanie
//przesłany przy pomocy Messangera mService. Należałoby podać
//ten argument w przypadku wysyłania komunikatu przy pomocy
//metody sendToTarget obiektu msg
//2. Int What ? typ komunikatu
//3. Int arg1 ? niewykorzystywane po stronie odbiorczej
//4. Int arg2 - niewykorzystywane po stronie odbiorczej
Message msg = Message.obtain(null,Common.CLIENT_LOGIN, 0, 0);
//Messenger należący do aktywności GanduClient, na który usługa
//GanduService będzie mogła odsyłać wiadomości
msg.replyTo = mMessenger;
//Stworzenie obiektu klasy Bundle
Bundle wysylany = new Bundle();
//Umieszczenie w wysylany pod kluczem ?numerGG? numeru GG
wysylany.putString("numerGG", mojNumer);
//Umieszczenie w wysylany pod kluczem ?hasloGG? hasła
wysylany.putString("hasloGG", haslo);
//Umieszczenie Bundle?a w obiekcie msg wysyłanym do usługi
msg.setData(wysylany);
try
{
//Próba wysłania obiektu msg do usługi, poprzez Messenger
//mService usługi GanduService
mService.send(msg);
}catch(RemoteException re){
}
}
};
Przedstawiony poniżej odbiór wiadomości logowania odbywa się wewnątrz Handlera usługi GanduService, po rozpoznaniu pola what odebranego obiektu Message (patrz wyżej, implementacja klasy IncomingHandler).
case Common.CLIENT_LOGIN:
//Odebranie obiektu Bundle, przenoszonego wewnątrz obiektu msg klasy
//Message
Bundle odebrany = msg.getData();
//Odczytanie numeru GG, przenoszego wewnątrz Bundle'a pod kluczem
//"numerGG"
ggnum = odebrany.getString("numerGG");
//Odczytanie hasła, przenoszego wewnątrz Bundle?a pod kluczem
//"hasloGG"
ggpass = odebrany.getString("hasloGG");
//inicjujLogowanie zwróci false w przypadku nieudanej próby
//połączenia z serwerem GG. Może wystąpić także w przypadku braku
//połączenia urządzenia z internetem
if (!inicjujLogowanie(ggnum)) {
//W przypadku nieudanej próby zainicjowania logowania odeślij
//do aktywności GanduClient odpowiednią wiadomość
Message msg3 = Message.obtain(null,
Common.CLIENT_INITIALIZE_FAILED, 0, 0);
try {
msg.replyTo.send(msg3);
} catch (RemoteException e) {
}
}
break;
Program Gandu w wersji beta (dział Downloads) oraz pełen kod programu (dział Source -> Browse -> Branch: gandu) jest dostępny do ściągnięcia ze strony projektu.
Zakończenie i podziękowania
Podczas prac nad komunikatorem korzystaliśmy z bardzo rzetelnej dokumentacji protokołu Gadu-Gadu stworzonej przez zespół libgaduii, do którego kierujemy słowa podziękowania i uznania.