Android 3 Tworzenie aplikacji - PDF Free Download (2024)

Termometry Termometr przekazuje wyniki odczytu temperatury i również są to pojedyncze elementy w tablicy values[0]. Wartości są reprezentowane w stopniach Celsjusza. Wartości w skali Fahrenheita uzyskamy, mnożąc wartość wyrażoną w stopniach Celsjusza przez 9/5 i dodając do wyniku 32.

Na przykład 0 stopni Celsjusza (punkt zamarzania wody) w skali Fahrenheita przyjmuje wartość 32, natomiast 100 stopni Celsjusza (punkt wrzenia wody) to 212 stopni Fahrenheita. W zależności od urządzenia termometr może zostać umieszczony w różnych miejscach, istnieje więc możliwość, że wynik mierzonej temperatury zakłóca ciepło generowane przez telefon. Przykładowo odczyty temperatury w pewnych urządzeniach są zakłócane przez ciepło powstające podczas pracy baterii. Powinniśmy o tym pamiętać podczas pisania aplikacji wykorzystujących termometry. Nie należy oczekiwać, że termometr wbudowany w telefon będzie mierzył wyłącznie temperaturę powietrza otaczającego aparat. Wśród projektów utworzonych na potrzeby tego rozdziału Czytelnik znajdzie jeden ukazujący sposób korzystania z termometru, zatytułowany TemperatureSensor.

Czujniki ciśnienia Co ciekawe, w czasie, gdy pisaliśmy niniejszą książkę, ten rodzaj czujników nie został jeszcze umieszczony w żadnym urządzeniu. Uważamy jednak, że kolejne generacje urządzeń mogą zostać wyposażone w barometryczne czujniki ciśnienia, pozwalające na przykład na pomiar wysokości. Nie należy mylić tego czujnika z funkcją ekranu dotykowego, który reaguje na nacisk palca, może określić jego siłę i generuje obiekt MotionEvent. Wykrywanie tego rodzaju oddziaływań mechanicznych zostało omówione w rozdziale 25., a służąca do tego architektura nie jest częścią omawianej w tym rozdziale struktury czujników. Chociaż utworzenie aplikacji obsługujących czujniki ciśnienia przez proste skopiowanie i zmodyfikowanie zaprezentowanych do tej pory aplikacji monitorujących nie byłoby trudnym zadaniem, to jednak bez wiedzy, jakie jednostki będą stosowane w przypadku tych czujników, napisanie takiej aplikacji nie zdałoby się na wiele. Najwidoczniej programiści z firmy Google planują z wyprzedzeniem.

Żyroskopy Żyroskopy stanowią bardzo ciekawą kategorię urządzeń, mierzącą skręt urządzenia w płaszczyźnie odniesienia. Inaczej mówiąc, za ich pomocą mierzymy prędkość obrotu telefonu w danej osi. Jeśli urządzenie nie będzie obracane, wartości odczytywane przez czujnik będą wynosić 0. W momencie obrotu smartfonu w dowolnym kierunku pojawią się niezerowe odczyty. Sam żyroskop nie poda nam wszystkich wymaganych danych. Niestety, podczas pracy z żyroskopami zawsze wkradną się jakieś błędy. Jednak w sprzężeniu z akcelerometrami możemy określić ścieżkę ruchu urządzenia. Do powiązania odczytów pochodzących z obydwu czujników mogą służyć filtry Kalmana. Akcelerometry nie są zbyt dokładne w krótszych odcinkach czasowych, z kolei żyroskopy tracą ją wraz z upływem czasu, więc ich powiązanie ze sobą może nam zagwarantować całkiem niezłą dokładność przez cały czas. Filtry Kalmana są bardzo skomplikowane, ale istnieje alternatywa zwana filtrami komplementarnymi, które są łatwiejsze do implementacji i generują całkiem poprawne wyniki. Wspomniane tu koncepcje wykraczają poza zakres książki. Żyroskop przekazuje trzy wartości w tablicy wartości, opisujące kolejno punkty na osiach x, y i z. Jednostką przekazywanych wartości są radiany na sekundę, reprezentują one szybkość obrotu urządzenia wokół danej osi. Jednym ze sposobów ich wykorzystania jest ich całkowanie po czasie w celu obliczenia zmiany kąta. W podobny sposób jest całkowana wartość prędkości liniowej po czasie w celu obliczenia odległości.

924 Android 3. Tworzenie aplikacji

Akcelerometry Akcelerometry stanowią chyba najciekawsze z obecnie dostępnych rodzajów czujników. Za ich pomocą aplikacja może określić fizyczne ułożenie urządzenia w zależności od siły ciążenia, a dodatkowo wykrywać siły przesuwające to urządzenie. Dzięki takim informacjom programiści zyskują niespotykane dotąd możliwości, począwszy od nowej jakości sterowania w grach, a skończywszy na pracy w rzeczywistości rozszerzonej. Oczywiście, podstawowym zadaniem akcelerometru jest przekazanie do urządzenia informacji o zmianie jego ułożenia z orientacji pionowej na poziomą, i odwrotnie. System współrzędnych w akcelerometrze działa następująco: oś x czujnika ma swój początek w lewym dolnym rogu urządzenia i jest skierowana w prawą stronę (patrząc od przodu urządzenia). Oś y również ma początek w lewym dolnym rogu urządzenia i jest skierowana w górę telefonu. Także punkt 0 osi z znajduje się w lewym dolnym rogu urządzenia i jest skierowany na zewnątrz, w taki sposób, że oddala się od urządzenia. Zostało to zobrazowane na rysunku 26.2.

Rysunek 26.2. System współrzędnych akcelerometru

System współrzędnych różni się od wykorzystywanego w układach graficznych i grafice dwuwymiarowej. W przypadku tamtych układów współrzędnych ich początek (0, 0) znajduje się w lewym górnym rogu ekranu, a wartości dodatnie osi y rosną w kierunku dolnym. Łatwo się pomylić podczas pracy z systemami współrzędnych w różnych układach odniesienia, należy więc zachować ostrożność. Jeszcze nic nie wspomnieliśmy o znaczeniu wartości przekazywanych przez akcelerometr, a więc co one oznaczają? Przyśpieszenie jest mierzone w metrach na sekundę do kwadratu (m/s2). Przyśpieszenie powodowane ziemską grawitacją wynosi 9,81 m/s2 i jest skierowane w dół, w stronę środka planety. Z punktu widzenia akcelerometru wartość siły ciążenia wynosi –9,81. Jeżeli urządzenie znajduje się w stanie spoczynku (nie porusza się) i jest ułożone na doskonale płaskiej, poziomej powierzchni, odczyty na osiach x i y przyjmą wartości 0, natomiast w osi z — +9,81. W rzeczywistości, zależnie od czułości i dokładności akcelerometru, wartości te nie będą doskonale odwzorowywać faktycznego stanu rzeczy, jednak będą stanowić wystarczająco dobre przybliżenie. W stanie spoczynku jedynie grawitacja będzie wpływać na urządzenie, a ponieważ jej wektor jest skierowany w dół (nasze urządzenie zaś leży płasko), nie będzie miała wpływu na osie x i y. W przypadku osi z będzie mierzona wartość siły działającej na urządzenie

oraz zostanie odjęta wartość siły ciążenia, więc 0 minus –9,81 daje nam ostatecznie wartość +9,81 — i tyle właśnie wynosi wartość siły przyłożonej do tej osi (element values[2] w obiekcie SensorEvent). Wartości przesyłane do aplikacji przez akcelerometr zawsze stanowią sumę sił działających na urządzenie minus wartość przyśpieszenia ziemskiego. Gdybyśmy unieśli w górę nasze ułożone doskonale płasko urządzenie, początkowo wartość mierzona dla osi z wzrosłaby, ponieważ zwiększylibyśmy oddziaływanie wbrew sile grawitacji. Gdy tylko przestaniemy unosić urządzenie, wartość sumaryczna działających sił powróci do wartości grawitacji. Gdyby urządzenie zostało upuszczone (czysto hipotetycznie — nie sprawdzajmy tego), zaczęłoby uzyskiwać przyśpieszenie w kierunku ziemi, a tym samym wartość odczytu w następnych momentach zmalałaby do zera. Wyobraźmy sobie, że urządzenie z rysunku 26.2 obrócimy w taki sposób, aby było ułożone w trybie portretowym, pionowo. Oś x pozostaje bez zmian i wskazuje z lewej strony na prawą. Z kolei oś y zostaje ustawiona prostopadle do ziemi, a oś z zostaje skierowana w naszą stronę. Wartość osi y wynosi teraz +9,81, a osi x i z — po 0. Co się stanie, jeśli obrócimy urządzenie do ułożenia poziomego, w trybie krajobrazowym, i w dalszym ciągu będziemy trzymać je pionowo, tj. ekran będzie się znajdował na wprost twarzy? Nietrudno zgadnąć, że osie y i z przybiorą wartości 0, a w osi x będzie działać siła równa +9,81. Taka sytuacja została zilustrowana na rysunku 26.3.

Rysunek 26.3. Wartości akcelerometru w trybie krajobrazowym, urządzenie ustawione w pionie

Gdy urządzenie znajduje się w stanie spoczynku lub porusza się z jednostajną prędkością, akcelerometry mierzą wyłącznie wartość grawitacji. Dla każdej osi odczyty akcelerometru stanowią składowe grawitacje w tej osi. Zatem za pomocą obliczeń trygonometrycznych możemy określić kąty oraz ułożenie urządzenia względem kierunku działania siły ciążenia. Oznacza to, że możemy się dowiedzieć, czy urządzenie jest ułożone w trybie portretowym, krajobrazowym, czy jakimś pośrednim. W rzeczywistości system korzysta właśnie z tego rozwiązania w czasie rozpoznawania orientacji ułożenia (tryb krajobrazowy lub portretowy). Zwróćmy jednak uwagę, że akcelerometry nie określają ułożenia urządzenia w odniesieniu do północy magnetycznej. Do tego właśnie służy magnetometr, który zostanie omówiony w dalszej części rozdziału.

926 Android 3. Tworzenie aplikacji

Akcelerometry a tryb wyświetlania Akcelerometry jako takie są układami elektronicznymi, trwale przymocowanymi do obudowy urządzenia — i z tego powodu mają określone ułożenie względem reszty architektury telefonu, niezmieniające się w trakcie jego obracania. Odczyty wysyłane w wyniku ruchu będą oczywiście ulegały zmianom, jednak układ współrzędnych akcelerometrów jest oparty na urządzeniu i nie będzie w żaden sposób modyfikowany. Z kolei układ współrzędny ekranu zmienia się w zależności od trybu wyświetlania. Faktycznie, w zależności od ułożenia ekranu tryb portretowy może być obrócony nawet o 180 stopni. Analogiczna sytuacja dotyczy również trybu krajobrazowego. Gdy nasza aplikacja odczytuje dane akcelerometru i ma właściwie modyfikować interfejs użytkownika, musi znać wartość obrotu urządzenia, aby móc wprowadzić niezbędne poprawki. W trakcie zmiany trybu z portretowego na krajobrazowy układ współrzędnych ekranu zostaje obrócony zgodnie z układem współrzędnych akcelerometrów. Aby tak się stało, aplikacja musi wykorzystać metodę Display.getRotation(), która została wprowadzona w wersji 2.2 Androida. Przekazywana wartość jest zwykłą liczbą całkowitą, nie symbolizuje jednak rzeczywistej wartości kąta obrotu. Mamy tu do czynienia z jedną z czterech następujących stałych: Surface. ´ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_180 lub Surface.ROTATION_270. Przyjmują one wartości kolejno: 0, 1, 2, 3. Wartości te informują aplikację, jak bardzo urządzenie zostało obrócone w stosunku do standardowego ułożenia wyświetlacza. Ponieważ nie wszystkie urządzenia obsługujące system Android w domyśle są ułożone w trybie portretowym, nie możemy z góry zakładać, że stała odpowiedzialna za ten tryb to ROTATION_0. Nie wszystkie urządzenia przekazują wszystkie cztery wartości. W telefonie HTC Droid Eris obsługującym wersję 2.1 Androida metoda Display.getOrientation() (poprzedniczka metody Display.getRotation(), obecnie uznanej za przestarzałą) wyświetla tylko wartości 0 i 1, i to tyle. W zwykłym trybie portretowym przekazywana wartość wynosi 0. Jeżeli obrócimy urządzenie o 90 stopni w stronę przeciwną do kierunku ruchu wskazówek zegara, układ graficzny zostanie zmieniony i metoda Display.getOrientation() powróci z wartością 1. Jeżeli w trybie portretowym obrócimy urządzenie o 90 stopni zgodnie z kierunkiem ruchu wskazówek zegara, ekran pozostanie w trybie portretowym, a my otrzymamy wartość 0 z metody Display.getOrientation(). W telefonie Motorola Droid pracującym pod kontrolą Androida 2.2 metoda Display.get powraca z wartościami 0, 1 lub 3. Wartość 2 nie jest przekazywana, co oznacza, że urządzenie nie pracuje w odwróconym trybie portretowym. Jest jednak pewien rozczarowujący wynik: jeżeli odwrócimy urządzenie o 270 stopni w stronę przeciwną do kierunku ruchu wskazówek zegara (z pozycji domyślnej), metoda Display.getRotation() powróci z wartością 1 przy 90 stopniach i urządzenie przejdzie w tryb krajobrazowy, przy 180 stopniach cały czas pozostaje wartość 1 i tryb wyświetlania nie ulega zmianie, przy 270 stopniach układ graficzny zostaje odwrócony na odwrotny krajobrazowy, jednak metoda Display.getRotation() wciąż przekazuje wartość 1. Jeżeli obrócimy urządzenie z pozycji domyślnej o 90 stopni zgodnie z kierunkiem ruchu wskazówek zegara, metoda ta powróci z wartością 3. Ta pozycja wygląda dokładnie tak samo jak odwrócona pozycja z 270 stopni, zostaje jednak przekazana inna wartość w metodzie Display.getRotation(), w zależności od tego, jaką drogą do niej dotarliśmy.

Akcelerometry i grawitacja Do tej pory zajmowaliśmy się bardzo pobieżnie kwestią zachowania odczytów akcelerometru w trakcie przemieszczania się urządzenia. Zastanówmy się teraz nad tym dokładniej.

Wszystkie siły działające na urządzenie zostaną wykryte przez akcelerometry. Jeżeli uniesiemy telefon, siła działająca w osi z będzie początkowo dodatnia, a jej wartość będzie większa niż +9,81. Jeżeli przesuniemy urządzenie w lewo, początkowa wartość wektora siły w osi x będzie ujemna. Zależałoby nam teraz na oddzieleniu wartości wynikających z oddziaływania grawitacyjnego od pozostałych sił działających na urządzenie. Rozwiązanie jest całkiem proste i nosi nazwę filtru dolnoprzepustowego. Siły niebędące oddziaływaniem grawitacyjnym zazwyczaj niestopniowo wpływają na urządzenie. Inaczej mówiąc, jeżeli użytkownik potrząsa urządzeniem, pojawiające się siły są bardzo szybko rejestrowane przez akcelerometry. W związku z tym filtr dolnoprzepustowy usunie składową odpowiedzialną za same wstrząsy i pozostawi wyłącznie niezmienną składową, w tym przypadku przyśpieszenie ziemskie. Zilustrujmy tę koncepcję na przykładzie. Interesujący nas projekt nosi nazwę GravityDemo. Na listingu 26.6 został umieszczony układ graficzny oraz kod Java. Listing 26.6. Pomiar grawitacji za pomocą akcelerometrów

// Jest to plik MainActivity.java. import import import import import import import

android.app.Activity; android.hardware.Sensor; android.hardware.SensorEvent; android.hardware.SensorEventListener; android.hardware.SensorManager; android.os.Bundle; android.widget.TextView;

public class MainActivity extends Activity implements SensorEventListener { private SensorManager mgr; private Sensor accelerometer; private TextView text; private float[] gravity = new float[3]; private float[] motion = new float[3]; private double ratio; private double mAngle; private int counter = 0; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mgr = (SensorManager) this.getSystemService(SENSOR_SERVICE);

928 Android 3. Tworzenie aplikacji accelerometer = mgr.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); text = (TextView) findViewById(R.id.text); } @Override protected void onResume() { mgr.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_UI); super.onResume(); } @Override protected void onPause() { mgr.unregisterListener(this, accelerometer); super.onPause(); } public void onAccuracyChanged(Sensor sensor, int accuracy) {

// Ignorujemy. } public void onSensorChanged(SensorEvent event) {

// Wprowadzamy filtr dolnoprzepustowy, służący do uzyskania składowej grawitacji. // Pozostałością są składowe ruchu. for(int i=0; i<3; i++) { gravity [i] = (float) (0.1 * event.values[i] + 0.9 * gravity[i]); motion[i] = event.values[i] - gravity[i]; }

// Zmienną ratio jest stosunek grawitacji przyłożonej w osi Y do standardowej wartości grawitacji. // Wartość ta powinna mieścić się w zakresie pomiędzy –1 i 1. ratio = gravity[1]/SensorManager.GRAVITY_EARTH; if(ratio > 1.0) ratio = 1.0; if(ratio < -1.0) ratio = -1.0;

// Konwertuje radiany na stopnie, w trakcie kierowania się // w górę wartość zostaje przetworzona na ujemną. mAngle = Math.toDegrees(Math.acos(ratio)); if(gravity[2] < 0) { mAngle = -mAngle; }

// Wyświetla co dziesiątą wartość. if(counter++ % 10 == 0) { String msg = String.format( "Nieprzetworzone wartości\nX: %8.4f\nY: %8.4f\nZ: %8.4f\n" + "Składowa grawitacji\nX: %8.4f\nY: %8.4f\nZ: %8.4f\n" + "Składowa ruchu\nX: %8.4f\nY: %8.4f\nZ: %8.4f\nKąt: %8.1f", event.values[0], event.values[1], event.values[2], gravity[0], gravity[1], gravity[2], motion[0], motion[1], motion[2], mAngle); text.setText(msg);

W wyniku uruchomienia powyższego kodu ujrzymy ekran zaprezentowany na rysunku 26.4. Poniższy zrzut ekranu został wykonany, kiedy urządzenie spoczywało płasko na stole.

Rysunek 26.4. Wartości grawitacji, ruchu i kąta

Aplikacja ta w większości przypomina wcześniejszy przykładowy program Monitor akcelerometru. Różnice pojawiają się w metodzie onSensorChanged(). Zamiast standardowego wyświetlania otrzymywanych wartości z tablicy próbujemy poznać składowe grawitacji i ruchu. Składową grawitacji uzyskujemy poprzez zsumowanie aktualnej i poprzedzającej wartości z tablicy grawitacji, przemnożonych przez współczynniki. Współczynniki te muszą po zsumowaniu dać wartość 1.0, a dobiera się je tak, że aktualną wartość z tablicy wartości grawitacji mnoży się przez mniejszy współczynnik, a poprzednią — przez większy (byle suma tych współczynników była równa wartości 1.0). W naszym przykładzie wykorzystaliśmy współczynniki 0,9 i 0,1. Możemy również wypróbowywać inne wartości współczynników, na przykład 0,8 i 0,2. Tablica wartości grawitacji prawdopodobnie nie będzie zmieniała się tak szybko jak rzeczywiste odczyty czujnika. W ten sposób jednak zbliżamy się do rzeczywistych wartości. Do tego właśnie służy filtr dolnoprzepustowy. Wartości tabeli zdarzeń ulegają zmianom jedynie w przypadku sił poruszających urządzeniem, a my nie chcemy, aby były one mierzone jako część oddziaływania grawitacyjnego. W tabeli wartości grawitacji chcemy jedynie zarejestrować faktyczne odczyty siły ciążenia. Zastosowane tutaj obliczenia matematyczne nie sprawiają, że w magiczny sposób jest rejestrowane wyłącznie przyśpieszenie grawitacyjne, ale uzyskiwane wartości będą znacznie bliższe rzeczywistości niż nieprzetworzone dane.

930 Android 3. Tworzenie aplikacji Analizując kod, zwróćmy również uwagę na tablicę wartości ruchu. Poprzez śledzenie różnicy pomiędzy nieprzetworzonymi wartościami zdarzeń a obliczonymi wartościami grawitacji mierzymy po prostu aktywne siły (niebędące przyśpieszeniem ziemskim) działające na urządzenie. Jeżeli wartości w tablicy ruchu wynoszą zero lub są bliskie zeru, oznacza to, że urządzenie prawdopodobnie znajduje się w stanie spoczynku. Jest to bardzo przydatna informacja. W idealnym przypadku urządzenie poruszające się ze stałą prędkością również powinno generować wartości tabeli ruchu bliskie zeru, w rzeczywistości jednak są one większe.

Używanie akcelerometrów do mierzenia kąta ułożenia urządzenia Zanim przejdziemy dalej, chcielibyśmy zaprezentować jeszcze jedną cechę akcelerometrów. Jeżeli przypomnimy sobie lekcje trygonometrii ze szkoły średniej, zauważymy, że cosinus kąta jest stosunkiem długości przyprostokątnej bliższej do tego kąta i przeciwprostokątnej. Jeśli weźmiemy pod uwagę kąt pomiędzy osią y a wektorem działania siły grawitacji, moglibyśmy zmierzyć wartość grawitacji działającej w kierunku y, a za pomocą funkcji arcus cosinus obliczyć kąt. Ta metoda znalazła zastosowanie w naszym kodzie. Musimy jednak w tym przypadku zmierzyć się z pewnym bałaganem związanym z architekturą czujników w Androidzie. W klasie SensorManager istnieją różne stałe definiujące grawitację, w tym również ziemską. Prawdopodobnie jednak mierzone wartości mogą przekraczać wartości określane przez te stałe. Za chwilę wyjaśnimy, co mamy na myśli. Nasze urządzenie, pozostając w stanie spoczynku, powinno teoretycznie mierzyć wartość grawitacji równą wspominanej już wielokrotnie stałej, tak się jednak nieczęsto dzieje. W takim przypadku akcelerometr będzie prawdopodobnie dawał mniejsze lub większe odczyty od założonych. Zatem współczynnik grawitacji może być większy od 1 lub mniejszy od –1. Nasza funkcja acos() może zacząć w takim przypadku dawać niestabilne wyniki, zatem zamykamy otrzymywane wartości w zbiorze liczb z przedziału od –1 do 1. Jest to zakres kątowy równoważny wartościom od 0 do 180 stopni. Wygląda zachęcająco, ale w ten sposób nie uzyskamy kątów ujemnych, od 0 do –180 stopni. Żeby uzyskać takie ujemne wartości, korzystamy z kolejnego elementu tablicy wartości grawitacji, którym jest wartość z. Jeżeli wartość z grawitacji jest ujemna, oznacza to, że urządzenie jest skierowane wyświetlaczem w dół. W przypadku wszystkich wartości, w których urządzenie jest skierowane w dół, wprowadzamy znak minus, dzięki czemu zakres kątowy wynosi od –180 do +180 stopni, dokładnie tak, jak powinno być. Nie bójmy się poeksperymentować z tą przykładową aplikacją. Zwróćmy uwagę, że wartość kąta nachylenia urządzenia wynosi 90 stopni, gdy leży ono na stole, a 0 stopni (lub blisko tej wartości), gdy trzymamy je naprzeciwko twarzy. Jeżeli będziemy przechylali urządzenie jeszcze bardziej z pozycji płaskiej, wartość tego kąta zacznie przekraczać 90 stopni. Jeżeli będziemy je pochylać coraz bardziej z pozycji 0 stopni, wartość kąta będzie przybierać wartości ujemne, aż w końcu urządzenie będzie ułożone wyświetlaczem do dołu i wartość tego kąta osiągnie wartość –90 stopni. Na koniec — pewnie zauważyliśmy w kodzie licznik kontrolujący częstotliwość aktualizacji ekranu. Ponieważ odczyty czujnika mogą być dość szybko odświeżane, postanowiliśmy pokazywać na wyświetlaczu zaledwie co dziesiąty wynik.

Magnetometry Magnetometr mierzy indukcję pola magnetycznego w otoczeniu i określa jej rozkład w osiach x, y i z. Układ współrzędnych jest taki sam jak w przypadku akcelerometrów, więc możemy zastosować tu taki, jaki widać na rysunku 26.2. Jednostkami stosowanymi w magnetometrach są mikrotesle (μT). Czujnik ten wykrywa ziemskie pole magnetyczne, dlatego też pozwala nam

określać kierunek północny. Magnetometr jest często nazywany również kompasem, nawet w znaczniku

jest stosowana nazwa android.hardware.sensor.compass. Magnetometr jest bardzo niewielkim i czułym urządzeniem, dlatego na jego odczyty wpływa pole magnetyczne generowane przez inne urządzenia znajdujące się w pobliżu, a w pewnym stopniu nawet układy znajdujące się w samym telefonie. Zatem wskazania magnetometru mogą być czasami niewiarygodne. Wśród projektów przygotowanych specjalnie na potrzeby tego rozdziału umieściliśmy prostą aplikację CompassSensor, warto więc zaimportować ją i trochę z nią poeksperymentować. Jeżeli zbliżymy metalowe przedmioty do urządzenia w trakcie działania aplikacji, możemy zauważyć zmianę odczytów. Oczywiście, jeżeli blisko czujnika ustawimy magnes, odczyty na pewno ulegną zmianie, nie radzimy jednak tego robić, gdyż magnetometr może zostać rozkalibrowany. Możemy się zastanawiać, czy istnieje możliwość wykorzystania magnetometru jako kompasu do wskazywania kierunku północnego. Odpowiedź brzmi: niebezpośrednio. Chociaż magnetometr wykrywa strumienie magnetyczne otaczające urządzenie, jeżeli urządzenie nie będzie ułożone doskonale poziomo, jego odczyty jako kompasu będą niemiarodajne. Mamy jednak do dyspozycji akcelerometry, które informują nas o ułożeniu urządzenia względem osi stanowiącej kierunek działania siły grawitacji! Możemy więc wykorzystywać magnetometr jako kompas, będzie nam jednak do tego potrzebna pomoc akcelerometrów. Zobaczmy, jak się to robi.

Współpraca akcelerometrów z magnetometrami Klasa SensorManager zawiera pewne metody umożliwiające łączenie odczytów magnetometru z wartościami mierzonymi przez akcelerometr w celu określenia orientacji w przestrzeni. Jak niedawno wspomnieliśmy, nie możemy w tym celu wykorzystać samego magnetometru. W klasie SensorManager znajdziemy więc metodę getRotationMatrix(), pobierającą wartości akcelerometru i kompasu, a następnie przekazującą macierz danych pozwalających na określenie orientacji w przestrzeni. Inna metoda tej klasy, getOrientation(), pobiera wspomnianą wcześniej tablicę obrotów i przetwarza ją na tablicę kierunkowości. Wartości podane w tej macierzy informują nas o obrocie urządzenia względem północy magnetycznej oraz o nachyleniu bocznym i wzdłużnym w stosunku do poziomu. Byłoby wspaniale, gdyby wszystkie te obliczenia były dokonywane automatycznie. Niestety, mechanizm ten stanowi wielkie wyzwanie, przynajmniej do wersji 2.2 Androida, gdzie jednym z problemów, i to wcale nie największym, jest brak ciągłości, w przypadku gdy trzymamy urządzenie naprzeciwko siebie, a następnie unosimy je nieznacznie w taki sposób, że spoglądamy nieco do góry na ekran. Owa nieciągłość polega na tym, że gdy tylko urządzenie przekroczy punkt 0 stopni (zgodnie z którym jeszcze znajdujemy się naprzeciwko urządzenia), magnetometr odwraca kierunki, co jest zupełnie nieintuicyjne. Na szczęście w wersji 2.3 Androida umieszczono kilka dodatkowych metod korygujących ten błąd (więcej informacji znajdziemy w punkcie „Czujniki wektora obrotu”). Jednak w międzyczasie powinniśmy również obsłużyć odczyty czujników w urządzeniach wyposażonych w wersje systemu starsze od 2.3.

Czujniki orientacji w przestrzeni Nadszedł czas na omówienie czujników orientacji. W poprzednim punkcie stwierdziliśmy, że można powiązać działanie akcelerometrów i magnetometrów w celu uzyskania odczytów dotyczących orientacji urządzenia, dzięki którym możemy dowiedzieć się, w jakim kierunku jest skierowany jego wyświetlacz. Takie samo zadanie wykonuje czujnik orientacji. W rzeczywistości

932 Android 3. Tworzenie aplikacji stanowi on połączenie akcelerometru i magnetometru na poziomie sterowników. Inaczej mówiąc, czujnik orientacji nie jest oddzielnym urządzeniem, ale oprogramowanie systemowe wiąże dwa wspomniane czujniki w taki sposób, że działają jak jeden układ elektroniczny. Nie wspominaliśmy o czujnikach orientacji aż do teraz, ponieważ zostały uznane za przestarzałe w wersji 2.2 Androida i nie zaleca się ich stosowania. Jednak są one bardzo przydatne, a do tego o wiele prostsze w użyciu od preferowanego rozwiązania, o czym się wkrótce przekonamy.

Niedawno stwierdziliśmy, że stosowanie preferowanego mechanizmu obliczania orientacji urządzenia w przestrzeni jest trudnym zadaniem. W następnym przykładzie porównamy wartości orientacji uzyskiwane z preferowanego rozwiązania z odczytami czujnika orientacji i przyjrzymy się różnicom. Urozmaicimy nieco przykładową aplikację. Moglibyśmy po prostu wyświetlić wartości przekazywane przez czujniki, ale możemy je jeszcze wykorzystać w interesujący sposób. Wyobraźmy sobie, że stoimy na ulicy w Jacksonville na Florydzie. Nasza aplikacja będzie nam pokazywała zdjęcia w trybie StreetView tego miasta, tak jakbyśmy tam byli, a czujnik orientacji posłuży nam do określenia kierunku, w jakim spoglądamy. Wraz ze zmianą kierunku orientacji telefonu będą się odpowiednio zmieniały widoki w trybie StreetView. Na listingu 26.7 widzimy układ graficzny i kod Java naszej przykładowej aplikacji, nazwanej VirtualJax. Listing 26.7. Uzyskiwanie informacji o położeniu za pomocą czujników

// Jest to plik MainActivity.java import android.app.Activity; import android.content.Intent; import android.hardware.Sensor;

Rozdział 26 „ Czujniki

import import import import import import import import import

android.hardware.SensorEvent; android.hardware.SensorEventListener; android.hardware.SensorManager; android.net.Uri; android.os.Build; android.os.Bundle; android.view.View; android.view.WindowManager; android.widget.TextView;

public class MainActivity extends Activity implements SensorEventListener { private static final String TAG = "VirtualJax"; private SensorManager mgr; private Sensor accel; private Sensor compass; private Sensor orient; private TextView preferred; private TextView orientation; private boolean ready = false; private float[] accelValues = new float[3]; private float[] compassValues = new float[3]; private float[] inR = new float[9]; private float[] inclineMatrix = new float[9]; private float[] orientationValues = new float[3]; private float[] prefValues = new float[3]; private float mAzimuth; private double mInclination; private int counter; private int mRotation; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); preferred = (TextView)findViewById(R.id.preferred); orientation = (TextView)findViewById(R.id.orientation); mgr = (SensorManager) this.getSystemService(SENSOR_SERVICE); accel = mgr.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); compass = mgr.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); orient = mgr.getDefaultSensor(Sensor.TYPE_ORIENTATION); WindowManager window = (WindowManager) this.getSystemService(WINDOW_SERVICE); int apiLevel = Integer.parseInt(Build.VERSION.SDK); if(apiLevel < 8) { mRotation = window.getDefaultDisplay().getOrientation(); } else { mRotation = window.getDefaultDisplay().getRotation(); } } @Override protected void onResume() {

933

934 Android 3. Tworzenie aplikacji mgr.registerListener(this, accel, SensorManager.SENSOR_DELAY_GAME); mgr.registerListener(this, compass, SensorManager.SENSOR_DELAY_GAME); mgr.registerListener(this, orient, SensorManager.SENSOR_DELAY_GAME); super.onResume(); } @Override protected void onPause() { mgr.unregisterListener(this, accel); mgr.unregisterListener(this, compass); mgr.unregisterListener(this, orient); super.onPause(); } public void onAccuracyChanged(Sensor sensor, int accuracy) {

// Ignorujemy } public void onSensorChanged(SensorEvent event) {

// Musimy uzyskać dostęp do akcelerometru i kompasu, // zanim określimy wartości tablicy orientationValues switch(event.sensor.getType()) { case Sensor.TYPE_ACCELEROMETER: for(int i=0; i<3; i++) { accelValues[i] = event.values[i]; } if(compassValues[0] != 0) ready = true; break; case Sensor.TYPE_MAGNETIC_FIELD: for(int i=0; i<3; i++) { compassValues[i] = event.values[i]; } if(accelValues[2] != 0) ready = true; break; case Sensor.TYPE_ORIENTATION: for(int i=0; i<3; i++) { orientationValues[i] = event.values[i]; } break; } if(!ready) return; if(SensorManager.getRotationMatrix( inR, inclineMatrix, accelValues, compassValues)) {

// Uzyskaliśmy dobrą macierz obrotów SensorManager.getOrientation(inR, prefValues); mInclination = SensorManager.getInclination(inclineMatrix);

Rozdział 26 „ Czujniki

935

// Wyświetla co dziesiątą wartość if(counter++ % 10 == 0) { doUpdate(null); counter = 1; } } } public void doUpdate(View view) { if(!ready) return; mAzimuth = (float) Math.toDegrees(prefValues[0]); if(mAzimuth < 0) { mAzimuth += 360.0f; } String msg = String.format( "Preferowana:\nazymut (Z): %7.3f \nprzechył wzdłużny (X): %7.3f\nprzechył boczny (Y): %7.3f", mAzimuth, Math.toDegrees(prefValues[1]), Math.toDegrees(prefValues[2])); preferred.setText(msg); msg = String.format( "Czujnik orientacji:\nazymut (Z): %7.3f\nprzechył wzdłużny (X): ´%7.3f\nprzechył boczny (Y): %7.3f", orientationValues[0], orientationValues[1], orientationValues[2]); orientation.setText(msg); preferred.invalidate(); orientation.invalidate(); } public void doShow(View view) {

// google.streetview:cbll=30.32454,–81.6584&cbp=1,yaw,,pitch,1.0 // yaw = wartość w stopniach, zgodnie ze wskazówkami zegara od bieguna północnego // W przypadku odchylenia (ang. yaw) możemy wykorzystać wartości mAzimuth // lub orientationValues[0]. // // pitch = wartość w stopniach, przechył w górę lub dół. –90 oznacza spoglądanie w górę, // +90 to spoglądanie w dół, // nie biorąc pod uwagę faktu, że przechył wzdłużny (ang. pitch) nie jest poprawnie obliczany. Intent intent=new Intent(Intent.ACTION_VIEW, Uri.parse( "google.streetview:cbll=30.32454,-81.6584&cbp=1," + Math.round(orientationValues[0]) + ",,0,1.0" )); startActivity(intent); return; } }

936 Android 3. Tworzenie aplikacji Interfejs użytkownika stanowią dwa przyciski i para listingów zawierających odczyty czujników, z których na jednym są ukazane wartości generowane przez preferowaną metodę, a na drugim — wyniki z czujnika orientacji. Po uruchomieniu tej aplikacji powinniśmy ujrzeć ekran podobny do przedstawionego na rysunku 26.5.

Rysunek 26.5. Dwa sposoby określania orientacji w przestrzeni

Zanim przyjrzymy się wynikom, wyjaśnijmy, jakie jest zadanie tej aplikacji. W metodzie onCreate() przeprowadzamy takie same czynności jak w poprzednich przykładach: tworzymy odniesienia do widoków tekstowych, klasy SensorManager oraz trzech typów czujników, jakie chcemy wykorzystać: akcelerometru, kompasu i czujnika orientacji. Definiujemy również zmienną przechowującą wartość obrotu. Za chwilę dowiemy się po co. W metodzie onResume() uruchamiamy czujniki, a w metodzie onPause() — wyłączamy je. Podczas otrzymywania aktualizacji odczytów czujnika określamy, do której kategorii one należą, i rejestrujemy te wartości w lokalnych członkach: accelValues, compassValues lub orientation ´Values. Zauważmy, że moglibyśmy skopiować tablicę zdarzeń w celu zachowania lokalnych kopii odczytów; oznaczałoby to jednak ciągłe tworzenie obiektów, a to nie jest dobry pomysł. Tworzenie nowych obiektów i późniejsze ich usuwanie może być naprawdę kosztowne, jeśli chodzi o zużycie zasobów, dlatego ograniczamy się wyłącznie do aktualizowania już istniejących tablic. Zwróćmy uwagę, że zanim zajmiemy się przetwarzaniem dalszej części kodu, sprawdzamy za pomocą operacji logicznej, czy posiadamy wartości zarówno w tablicy accelValues, jak i compass ´Values. Widzimy następnie wywołanie metody getRotationMatrix(), po której następuje wywołanie metody getOrientation(). Wprowadziliśmy również metodę getInclination(). Nie będziemy z niej korzystać, warto jednak wiedzieć, że reprezentuje ona kąt pomiędzy strumieniem magnetycznym a powierzchnią Ziemi. Im bliżej znajdujemy się któregoś z biegunów, tym większa jest przekazywana wartość kąta. Następnie tworzymy licznik, za pomocą którego będzie wyświetlana co dziesiąta aktualizacja wartości. Podobnie jak we wcześniejszych przykładach, mechanizm ten służy do zminimalizowania obciążenia interfejsu użytkownika, dzięki czemu aplikacja zyskuje na wydajności.

Rozdział 26 „ Czujniki

937

Wewnątrz metody doUpdate(), która może być wywoływana również za pomocą przycisku w interfejsie użytkownika, przeprowadzamy kilka obliczeń i wyświetlamy wyniki. W przypadku zalecanej metody pierwsza wartość, azymut, jest wyznaczana w radianach, w zakresie od ujemnej wartości pi do dodatniej wartości pi (czyli od –180 do 180 stopni). Wartości czujnika orientacji mieszczą się w zakresie od 0 do 360 stopni. Aby odczyty z obydwu rodzajów czujników były porównywalne, wzięliśmy pierwszą wartość z tablicy prefValues, przekształciliśmy radiany na stopnie i dodaliśmy 360, jeśli wartość była ujemna. Teraz wartości pozyskiwane z akcelerometru i magnetometru są porównywalne z odczytami czujnika orientacji. Pozostała część tej metody zapewnia obsługę wyświetlania tych wyników w interfejsie użytkownika. Ostatnią metodą w naszej aplikacji jest doShow(). Uważamy, że jest naprawdę pasjonująca. W rozdziale 25. pokazaliśmy, w jaki sposób można przywołać aplikację StreetView za pomocą intencji. W tamtym rozdziale pominęliśmy część związaną z konfigurowaniem wartości odchylenia, która definiuje kierunek spoglądania użytkownika w przypadku wyświetlania obrazu. Przeanalizujemy teraz sposób przekazania wartości odchylenia, a także przechyłu wzdłużnego. Jako długość i szerokość geograficzną wybraliśmy współrzędne miasta Jacksonville na Florydzie. Możemy wstawić tu oczywiście własne koordynaty. W przypadku odchylenia musimy przekazać wartość w stopniach liczoną od kierunku północnego (0 – 360), zatem możemy skorzystać z wartości zmiennej mAzimuth lub tablicy orientationValues[0], przekształconej w liczbę całkowitą. Jeśli chodzi o przechył wzdłużny, moglibyśmy teoretycznie wykorzystać drugą wartość z którejś z tablic, a następnie dodać wartość 90. Jednak wydaje się, że aplikacja StreetView ma problemy z obsługą wartości przechyłu wzdłużnego innymi niż 0, przynajmniej w tej lokacji. Zatem na razie ustanawiamy wartość 0 przechyłu wzdłużnego. Jeżeli klikniemy przycisk Wyświetl moją pozycję!, zostanie uruchomiona aplikacja StreetView, a także wyświetlony obraz znajdujący się w kierunku, w którym spoglądamy. Jeżeli wciśniemy przycisk cofania, obrócimy się i ponownie klikniemy przycisk Wyświetl moją pozycję!, zostanie wyświetlony nowy obraz. Przyjrzyjmy się teraz rzeczywistym wartościom czujników. Wartości generowane przez kod zgodny z zalecanym rozwiązaniem oraz przez czujnik orientacji wydają się identyczne albo bardzo do siebie zbliżone. Wartości pochodzące z tego drugiego mechanizmu wyglądają bardziej stabilnie, mamy tu również do czynienia z liczbami całkowitymi. Wydaje się, że jest naprawdę nieźle, ale to trochę pochopny wniosek. Jeżeli uniesiemy urządzenie nieco nad głowę i nachylimy je tak, żeby móc spoglądać na wyświetlacz, odczyty generowane obydwiema metodami zaczną się od siebie znacząco różnić. Obróćmy teraz urządzenie, aby znalazło się w trybie krajobrazowym. Powinniśmy otrzymać wyniki przypominające widoczne na rysunku 26.6. Co się stało? Wartości przechyłu bocznego uzyskane w metodzie zalecanej i z odczytu czujnika orientacji uzyskały przeciwne znaki. Problem polega na tym, że w obydwu mechanizmach stosowane są inne punkty odniesienia. Nie wyjaśniliśmy jeszcze, co się dzieje, gdy urządzenie działa w trybie krajobrazowym, a nie portretowym. Jeżeli urządzenie jest ustawione naprzeciwko twarzy użytkownika w trybie krajobrazowym, akcelerometry nie zamieniają się miejscami, więc oś x zastępuje oś y i odwrotnie. W normalnych warunkach musielibyśmy w tym wypadku wprowadzić nieco przekształceń matematycznych, na szczęście klasa SensorManager zawiera pewną przydatną metodę — remapCoordinateSystem(). Może być ona wywołana pomiędzy momentem uzyskania macierzy obrotów a wywołaniem metody getOrientation(). Podstawową funkcją tej metody jest modyfikacja macierzy obrotów poprzez zamienienie osi układu współrzędnych. Sygnatura tej metody wygląda następująco: public static boolean remapCoordinateSystem (float[] inR, int X, int Y, float[] outR)

938 Android 3. Tworzenie aplikacji

Rysunek 26.6. Dane o orientacji urządzenia w przestrzeni otrzymane dwoma sposobami, urządzenie ustawione w trybie krajobrazowym

Przekazujemy w niej macierz obrotów oraz wartości określające sposób zamiany osi x i y, w wyniku czego otrzymujemy nową tablicę obrotów (outR), a także wartość logiczną, która wskazuje, czy proces przekształcania został zakończony powodzeniem. Wartości x i y są stałymi klasy SensorManager, takimi jak AXIS_Z lub AXIS_MINUS_Y. Omawiane tu rozwiązanie zademonstrowaliśmy w przykładowej aplikacji VirtualJaxWithRemap, którą możemy pobrać wraz z innymi projektami.

Deklinacja magnetyczna i klasa GeomagneticField Chcielibyśmy poruszyć jeszcze jeden temat związany z orientacją w przestrzeni i określającymi ją urządzeniami. Dzięki kompasowi dowiemy się, gdzie znajduje się północ magnetyczna, nie wskaże nam on jednak rzeczywistej północy (geograficznej). Wyobraźmy sobie, że znajdujemy się na linii pomiędzy biegunem magnetycznym północnym a biegunem geograficznym północnym. Tworzyłyby one wtedy kąt 180 stopni. Im bardziej oddalilibyśmy się od obydwu biegunów, tym mniejszy kąt występowałby pomiędzy nimi. Różnica kątowa pomiędzy obydwoma typami biegunów zwana jest deklinacją magnetyczną. Wartość ta może być obliczona jedynie w odniesieniu do określonego punktu na powierzchni Ziemi. Oznacza to, że aby określić kierunek północy geograficznej, jeśli znamy kierunek północy magnetycznej, musimy jeszcze znać swoje położenie geograficzne. Na szczęście Android jak zwykle służy pomocą, tym razem przy użyciu klasy GeomagneticField. Aby utworzyć obiekt klasy GeomagneticField, musimy przekazać jej współrzędne geograficzne. Zatem w celu określenia kąta deklinacji magnetycznej musimy znać położenie punktu odniesienia. Wymagana jest również znajomość godziny, w której będzie obliczana wartość deklinacji. Magnetyczny biegun północny zmienia swoje położenie w czasie. Po utworzeniu tego obiektu wywołujemy po prostu poniższą metodę, aby uzyskać kąt deklinacji (wyrażony w stopniach): float declinationAngle = geoMagField.getDeclination();

Wartość zmiennej declinationAngle będzie dodatnia, jeżeli północ magnetyczna będzie się znajdowała na wschód od północy geograficznej.

Rozdział 26 „ Czujniki

939

Czujniki grawitacji Wraz z wersją 2.3 Androida wprowadzono czujniki grawitacji. W rzeczywistości nie jest to osobny układ elektroniczny. Mamy tu do czynienia z wirtualnym czujnikiem opartym na odczytach akcelerometrów. Tak naprawdę wykorzystywany jest tutaj opisany wcześniej mechanizm określania składowej grawitacji przez akcelerometry, w którym jest ona oddzielana od pozostałych sił działających na urządzenie. Nie możemy jednak modyfikować tego mechanizmu i musimy zaakceptować wszelkie współczynniki i przekształcenia dostępne w klasie tego czujnika. Być może w przyszłości ten wirtualny czujnik będzie wykorzystywał również inne sensory, na przykład żyroskop, do dokładniejszego pomiaru wartości grawitacji. Macierz wartości w przypadku tego czujnika przekazuje odczyty grawitacji w taki sam sposób, jak miało to miejsce w przypadku akcelerometrów.

Czujniki przyśpieszenia liniowego Podobnie jak miało to miejsce w przypadku czujnika grawitacji, czujniki przyśpieszenia liniowego są wirtualnymi sensorami reprezentującymi siły działające na urządzenie z wyłączoną składową grawitacji. Także i w tym przypadku ukazaliśmy wcześniej mechanizm pozwalający na uzyskanie tych wartości oraz usuwający z wyniku składową przyśpieszenia ziemskiego. Omawiany czujnik jeszcze bardziej ułatwia nam to zadanie. Być może w przyszłości również i ten sensor wykorzysta inne czujniki, na przykład żyroskop, do dokładniejszego mierzenia przyśpieszeń liniowych wpływających na urządzenie. Tablica wartości przedstawia wyniki w taki sam sposób jak w przypadku wskazań akcelerometrów.

Czujniki wektora obrotu Czujnik wektora obrotu przypomina wycofany już czujnik orientacji pod tym względem, że odczytuje orientację urządzenia w przestrzeni oraz podaje kąty zależne od punktu odniesienia wobec sprzętowego akcelerometru (rysunek 26.2). W trakcie pisania książki nie było jeszcze żadnych konkretnych informacji na temat tego czujnika. Prosimy zaglądać na naszą stronę (www.androidbook.com), gdyż będziemy zamieszczać tam informacje na jego temat.

Czujniki komunikacji bliskiego pola Wraz z wprowadzeniem wersji 2.3 Androida uzyskaliśmy możliwość stosowania specjalnych terminali wykorzystujących pole NFC (ang. Near Field Communication). Przypominają one nieco terminale RFID (ang. Radio Frequency Identification — identyfikacja na częstotliwości radiowej), główna różnica polega na tym, że zasięg terminalu NFC wynosi 4 cale3. Oznacza to, że czujnik NFC musi bardzo zbliżyć się do terminalu (ang. tag), żeby móc go przeskanować. Terminale NFC mogą zostać zaprogramowane w taki sposób, aby przesyłały dane tekstowe, identyfikatory URI oraz metadane, na przykład język, w jakim dana informacja jest przesyłana. Zwróćmy uwagę, że technologia NFC nie jest nowością i w wielu krajach jest od lat znana i używana. W rzeczywistości w kilku państwach terminale kasowe wyposażone w terminale NFC są dość powszechne. Gdy terminal wykryje czujnik NFC, klient może dokończyć transakcję za pomocą konta skojarzonego z identyfikatorem NFC. W internecie można znaleźć wiele filmów pokazujących użytkowników, którzy w ten sposób przeprowadzają transakcje płatnicze. 3

Około 10 cm — przyp. tłum.

940 Android 3. Tworzenie aplikacji Reprezentanci firmy Google obiecują, że pewnego dnia nasze portfele zostaną zastąpione przez telefony. Jest to istotnie kusząca wizja. Android pozwala przekształcić telefon w taki terminal wobec innego czytnika lub w czytnik wykrywający oraz skanujący terminale NFC. W rzeczywistości istnieją trzy tryby działania mechanizmu NFC. Pierwszy tryb polega na odczytywaniu i zapisywaniu bezkontaktowych terminali. Drugim jest tryb emulacji karty. W tym trybie telefon pracujący pod kontrolą systemu Android może sam zachowywać się jak terminal NFC. Oczywistą zaletą tego rozwiązania jest możliwość zmiany zachowania urządzenia jako terminalu po wciśnięciu jednego przycisku. To właśnie dzięki temu trybowi nasz telefon może przekształcić się w portfel. Bez względu na rodzaj posiadanej karty kredytowej lub biletu nasze urządzenie może naśladować taki obiekt (oczywiście przy zastosowaniu wszelkich niezbędnych zabezpieczeń), dzięki czemu czytnik działa tak, jakby obsługiwał kartę kredytową, chociaż w rzeczywistości ma do czynienia z telefonem. Trzecim trybem mechanizmu NFC jest bezpośrednia, równorzędna komunikacja. W tym przypadku każde urządzenie jest równorzędne i nie są potrzebne terminale. Wraz z wydaniem wersji 2.3.3 Androida możemy odczytywać terminale za pomocą urządzenia obsługującego ten system, podobnie jak ma to miejsce w terminalu kasowym z wcześniejszego przykładu, a także możemy zachowywać informacje w zapisywalnych terminalach NFC. Jeżeli urządzenie użytkownika zostało poprawnie skonfigurowane, może przesyłać dane do innego urządzenia wyposażonego w czujnik NFC za pomocą protokołu P2P, zdefiniowanego przez firmę Google. W trakcie pisania książki nie była jeszcze dostępna funkcja emulacji karty lub, dokładniej, terminalu NFC. W istocie jest to bardzo trudne zadanie do wykonania, częściowo z powodu różnorodności architektur czujników NFC wprowadzanych do urządzeń. Nie wiadomo, kiedy tryb emulacji karty zostanie wprowadzony do pakietu SDK, wierzymy jednak, że kiedyś to nastąpi. W międzyczasie istnieje możliwość emulacji karty w pewnym zakresie na poziomie sterowników za pomocą zestawu Android NDK (ang. Native Development Kit). Zagadnienie to wykracza jednak poza zakres książki. Oprócz przeprowadzania transakcji pieniężnych terminale NFC mogą spełniać również wiele innych zadań. Na przykład mogą być umieszczane w muzeum w pobliżu eksponatów i wysyłać adres URL do strony zawierającej multimedialne informacje na temat danego przedmiotu. Na przystankach autobusowych terminale mogą przechowywać rozkład jazdy interesującej nas linii. Przedsiębiorstwa mogą umieszczać w różnych miejscach terminale NFC umożliwiające łatwą rejestrację do swoich usług mobilnych. Być może zapomnimy o kluczach w hotelach, gdyż będziemy mogli otwierać drzwi urządzeniami wyposażonymi w NFC. Nawet produkty w sklepach mogą zostać zaopatrzone w terminale NFC, zawierające dokładniejsze informacje na ich temat, na przykład składniki i wartości odżywcze, parametry techniczne lub multimedialne reklamy.

Aktywacja czujnika NFC Obsługa czujnika NFC w Androidzie różni się od korzystania z pozostałych rodzajów sensorów. Nie stosujemy klasy SensorManager, tylko NfcAdapter. Zazwyczaj w urządzeniu dostępny jest tylko jeden czujnik NFC, zarządzający zapisywaniem informacji na terminalach i odczytywaniem ich treści, a także rozdzielaniem terminali pomiędzy aktywności. Adapter może być włączony lub wyłączony, natomiast w widoku Ustawienia znajdziemy opcje pozwalające na jego uaktywnianie lub wyłączanie. Opcje adaptera NFC można zazwyczaj znaleźć w zakładce Sieci bezprzewodowe. Jeżeli adapter jest włączony, po wykryciu terminalu nastąpi dość skomplikowany proces określający, która aktywność powinna otrzymać intencję informującą o obecności tego terminalu. Wszystko zależy od rodzaju danych przechowywanych przez terminal NFC, a tak-

Rozdział 26 „ Czujniki

941

że od obecności filtrów intencji dla zainstalowanych aplikacji w urządzeniu. Uwzględniana jest także jeszcze jedna informacja, mianowicie czy aktualnie pierwszoplanowa aktywność może otrzymywać terminale NFC. Niedługo zajmiemy się dokładniej tym zagadnieniem. Aby uzyskać dostęp do adaptera, tworzymy najpierw za pomocą metody getSystemService() wystąpienie obiektu NfcAdapter. Następnie wywołujemy metodę getDefaultAdapter() w sposób zaprezentowany poniżej: NfcManager manager = (NfcManager) context.getSystemService(Context.NFC_SERVICE); NfcAdapter adapter = manager.getDefaultAdapter();

Otrzymamy w ten sposób singletonowy obiekt klasy NfcAdapter. Aby sprawdzić, czy klasa NfcAdapter jest aktywna, wprowadzamy metodę isEnabled(), która powraca z wartością logiczną określającą, czy technologia NFC została włączona w panelu Ustawienia. Nigdzie nie znaleźliśmy udokumentowanego sposobu na programowe włączanie i wyłączanie adaptera NFC. Jeżeli jest wyłączony, a dana aplikacja wymaga jego uruchomienia, musimy powiadomić użytkownika o konieczności ręcznego włączenia czujnika. Aby wyświetlić użytkownikowi właściwy widok ustawień, możemy skorzystać z następującego fragmentu kodu: startActivityForResult(new Intent( android.provider.Settings.ACTION_WIRELESS_SETTINGS), 0);

Po przetworzeniu tego kodu Android otworzy odpowiedni widok ustawień i użytkownik będzie mógł włączyć adapter NFC. Metoda zwrotna onActivityResult() zostanie wywołana w chwili zamknięcia okna ustawień przez użytkownika. Pamiętajmy, że użytkownik może nie włączyć adaptera pomimo powiadomienia. Nasza aplikacja powinna być przygotowana również na ten scenariusz.

Trasowanie terminali NFC Nadszedł odpowiedni moment na omówienie różnych rodzajów technologii oraz terminali NFC. Mechanizm NFC nie jest ograniczony do jednego standardu. W rzeczywistości użytkownik może natrafić na kilka odmian terminali różniących się pomiędzy sobą. Terminale te nie posiadają takiej samej architektury, co oznacza, że Android musi zawierać dla każdego z nich oddzielną klasę. Jeśli zajrzymy do wnętrza pakietu android.nfc.tech.package, znajdziemy w nim kilka klas dotyczących różnych technologii terminali NFC, począwszy od klasy MifareClassic, poprzez NfcV, a skończywszy na ISO-DEP. Każdy rodzaj terminalu różni się od innych strukturą wewnętrzną, natomiast uzyskanie dostępu do zawartych w nich danych i manipulowanie nimi wymaga stosowania oddzielnych metod. Na szczęście Android został zaopatrzony w klasę Tag ułatwiającą komunikację NFC i za jej pomocą możemy utworzyć dowolny rodzaj terminalu. Po utworzeniu wystąpienia określonego terminalu NFC możemy przeprowadzać na nim dopuszczalne operacje. Oznacza to także, że należy wziąć pod uwagę kilka czynników przed wysłaniem terminalu do aktywności. Wyjaśnimy najpierw, w jaki sposób jest tworzona intencja terminalu NFC, dzięki czemu Czytelnik zrozumie mechanizm tworzenia odpowiednich filtrów intencji. W trakcie przesyłania intencji zawierającej dane terminalu obiekt klasy Tag zawsze jest rozkładany do pakietu dodatkowych danych intencji, a jego kluczem jest EXTRA_TAG. Jeżeli terminal zawiera informacje typu NDEF, zostaje dodana kolejna wartość z kluczem EXTRA_NDEF_ ´MESSAGES. Ostatnim elementem dodatkowym może być identyfikator terminalu, którego klucz to EXTRA_ID. Dwie ostatnie wartości są opcjonalne i zależą od obecności danych w terminalu. Wszystkie intencje NFC są wysyłane za pomocą metody startActivity(). Zauważmy,

942 Android 3. Tworzenie aplikacji że tak naprawdę nie musimy nigdy uzyskiwać dostępu do adaptera, aby otrzymywać komunikaty NFC. Wiadomości z intencji będą przychodziły do aplikacji, tak samo jak wszelkie inne aplikacje przesyłane z różnych źródeł, tak długo, jak odpowiadają one filtrowi (filtrom) intencji. Należy zwrócić uwagę, że technologia NFC dotyczy tylko urządzeń wyposażonych w czujnik NFC. Mechanizmy wymagane do tworzenia odpowiednich intencji zawierają funkcje nieobsługiwane przez pakiet SDK. Oznacza to, że samodzielne tworzenie testowej aktywności nadawczej jest bardzo trudne. W tym podrozdziale staramy się wyjaśnić mechanizmy rządzące tym systemem, dla których nie da się własnoręcznie napisać kodu. Oznacza to również, że aby rzeczywiście przetestować aplikację wykorzystującą mechanizm NFC, trzeba wykorzystać fizyczne urządzenie oraz terminale NFC. Być może kiedyś firma Google zaimplementuje odpowiednie funkcje w emulatorze lub w narzędziu DDMS.

W przypadku intencji terminalu wartość działania zależy od rodzaju informacji wykrytych w terminalu. Dla danej intencji istnieją trzy możliwe wartości działania: 1. ACTION_NDEF_DISCOVERED stanowi działanie w przypadku wykrycia bloku danych NDEF w terminalu. W takim przypadku Android szuka następnie obecności elementu NdefRecord w pierwszym obiekcie NdefMessage. Jeżeli elementem NdefRecord jest identyfikator URI lub rejestr SmartPoster, w polu danych intencji zostanie umieszczony ten identyfikator. Jeżeli z kolei zostanie wykryty rekord MIME, pole typu intencji zostanie zmodyfikowane do odpowiedniego typu MIME terminalu. System zacznie następnie szukać odpowiedniej aktywności dla tej intencji oraz właściwego algorytmu dopasowania intencji. Jeżeli nie zostanie znaleziona żadna aktywność, bieżąca intencja zostanie porzucona i Android spróbuje utworzyć następną intencję NFC. 2. ACTION_TECH_DISCOVERED jest działaniem podejmowanym, w przypadku gdy nie zostaną wykryte dane NDEF lub jeśli nie znajdziemy żadnej aktywności obsługującej ten format danych, lecz dostępna będzie technologia terminali. W tym przypadku Android dodaje metadane do intencji, za pomocą których zostanie uruchomiona odpowiednia technologia terminali. W terminalu NFC może zostać zaimplementowanych kilka różnych technologii, zwłaszcza że format Ndef bardziej przypomina wirtualny mechanizm. Android wyszukuje aktywność pasującą do intencji. Jeżeli zostanie znaleziona, prześlemy do niej intencję, w przeciwnym wypadku intencja ta zostanie porzucona i system wypróbuje trzeci rodzaj intencji NFC. 3. ACTION_TAG_DISCOVERED jest ostatnim działaniem definiowanym dla terminalu NFC. Jest ono podejmowane, gdy wszystkie pozostałe działania okażą się niedopasowane do aktywności. Intencja tego typu nie przenosi również danych ani typu MIME. Jeżeli ta intencja nie zostanie dopasowana do żadnej aktywności w urządzeniu, system NFC zaprzestaje prób i informacje o terminalu zostaną usunięte.

Odbieranie terminali NFC Bez względu na to, czy zdecydujemy się na utworzenie filtrów intencji za pomocą kodu, czy w pliku AndroidManifest.xml, musimy bardzo dobrze wiedzieć, czego szukamy, a filtry intencji przygotować z dużą ostrożnością. Jeżeli zdefiniujemy je zbyt rygorystycznie, aplikacja nie będzie powiadamiana o istotnych terminalach. Z kolei jeśli zdefiniujemy je niezbyt precyzyjnie, aplikacja zacznie otrzymywać komunikaty o terminalach, które nie są dla niej przeznaczone. W przypadku gdy nasza aplikacja otrzyma terminal dla niej nieprzeznaczony, być może w urzą-

Rozdział 26 „ Czujniki

943

dzeniu istnieje inna aplikacja, dla której tego typu terminale są przeznaczone, jednak ta właściwa aplikacja nie otrzymała terminalu. Taka sytuacja może nastąpić, w przypadku gdy filtr intencji znalazł więcej niż jedną aplikację pasującą do terminalu. Wtedy system wyświetla monit, aby użytkownik wybrał właściwą aplikację. Może się zdarzyć, że użytkownik wybierze aplikację, dla której dany terminal nie jest przeznaczony. Istnieje więc kolejny powód, dla którego należy ostrożnie definiować filtry intencji dla terminali NFC: jeżeli użytkownik otrzyma monit o wybór aplikacji, z dużym prawdopodobieństwem przed podjęciem decyzji wyjdzie z zasięgu terminalu. Jeżeli możemy określić, jakiego typu dane terminali będą przetwarzane przez naszą aplikację, oznacza to, że możemy je bardzo dokładnie sprecyzować, na przykład za pomocą niestandardowego schematu identyfikatora URI lub własnego typu MIME. Wybór filtru intencji zależy od rodzaju działania umieszczonego wewnątrz intencji terminalu NFC (zostało to omówione powyżej). Na listingu 26.8 został umieszczony przykładowy filtr intencji dla terminalu NDEF, który możemy umieścić w pliku AndroidManifest.xml. Listing 26.8. Filtr intencji dla terminalu NDEF zawierającego typ MIME

Zamiast wartości type/subtype moglibyśmy oczywiście wskazać określony, poszukiwany przez nas typ MIME lub wprowadzić symbole wieloznaczne w przypadku akceptowania każdego typu lub podtypu. Możemy na przykład zdefiniować atrybut mimeType jako text/*, dzięki czemu akceptowane byłyby wszystkie formaty tekstu. Nie musimy jednak definiować typu MIME dla terminalu NDEF. Jeżeli terminal ten posiada identyfikator URI, możemy utworzyć filtr intencji przypominający kod z listingu 26.9. Listing 26.9. Filtr intencji dla terminalu NDEF zawierającego identyfikator URI

W tym przykładzie definiujemy schemat geo, dzięki czemu po wykryciu terminalu zawierającego identyfikator rozpoczynający się od członu geo: zostanie uruchomiona nasza aktywność. Możemy stosować wszystkie atrybuty węzła do określania, które dane terminalu NFC są oczekiwane przez naszą aktywność. Jeżeli nasza aktywność wymaga terminali NFC utworzonych w określonej technologii, możemy skorzystać z filtru intencji zaprezentowanego na listingu 26.10. Może również zaistnieć sytuacja, w której zostanie wykryty terminal NDEF, lecz żadna aktywność nie będzie dopasowana do przetwarzania intencji NDEF_DISCOVERED. Również w takim przypadku nasza aktywność może otrzymać tę intencję, dopóki jest ona zgodna z filtrem intencji. Inaczej mówiąc, jeżeli intencja terminalu zawierająca działanie NDEF_DISCOVERED nie zostanie dostarczona do aktywności wyszukującej tego typu działania, zostanie wysłana do aktywności oczekującej terminalu utworzonego w określonej technologii.

944 Android 3. Tworzenie aplikacji Listing 26.10. Filtr intencji dla terminalu NFC zawierającego określoną technologię

Zwróćmy uwagę, że wstawiliśmy teraz działanie definiujące poszukiwaną technologię, a zamiast węzła wprowadziliśmy znacznik , który znajduje się poza znacznikiem . Mamy również do czynienia z innymi znacznikami w tym węźle, które znajdują się w oddzielnym pliku, umieszczonym w katalogu /res/xml. Na listingu 26.11 demonstrujemy przykładowy plik nfc_tech_filter.xml. Listing 26.11. Przykładowy plik XML zawierający filtr technologii NFC android.nfc.tech.NfcA android.nfc.tech.MifareUltralight android.nfc.tech.NfcB android.nfc.tech.Ndef

Filtr ten definiuje dwa rodzaje terminali oczekiwanych przez naszą aplikację. Terminal NFC zazwyczaj zawiera listę obsługiwanych przez niego technologii. Jeżeli którykolwiek z elementów tej listy został wymieniony w filtrze z listingu 26.11, nasza aktywność uzyska dostęp do intencji tego terminalu. Na listingu 26.11 pierwszy rodzaj terminalu określa technologie NfcA oraz MifareUltralight, w drugim zaś zdefiniowano: NfcB i Ndef. Możemy dodawać kolejne węzły

do tego pliku w celu definiowania następnych terminali akceptowanych przez naszą aktywność. Uwzględniane tutaj technologie biorą swoje nazwy od nazw klas dostępnych w pakiecie android.nfc.tech, ale powinniśmy wpisywać jedynie te mechanizmy, które będą przydatne aktywności. Węzły potomne znacznika zawierają wszystkie technologie, które powinien posiadać terminal NFC, aby jego intencja pasowała do aktywności. Wszystkie technologie z danej listy muszą się znajdować na liście technologii obsługiwanych przez terminal NFC. Zatem lista technologii w filtrze może zawierać mniej elementów niż analogiczna lista w terminalu NFC, nie może jednak wystąpić odwrotna sytuacja. Kontynuując powyższy przykład, jeżeli w terminalu NFC znajdzie się wyłącznie wskazanie technologii Ndef, terminal ten nie zostanie przepuszczony przez żaden filtr i aktywność nie otrzyma jego intencji. Żadna z wymienionych list filtru intencji nie stanowi podzbioru na liście terminalu. Gdyby ten terminal zawierał technologie NfcA, NfcB oraz Ndef, okazałby się zgodny z drugą specyfikacją i zostałby przesłany do aktywności. Ta druga specyfikacja stanowi podzbiór listy technologicznej terminalu NFC. Terminal ten byłby dopasowany, nawet gdyby zawierał dodatkowe technologie, niewymienione w filtrze intencji.

Rozdział 26 „ Czujniki

945

Ostatni filtr intencji, który może się przydać Czytelnikowi, został zaprezentowany na listingu 26.12. Charakteryzuje go uniwersalność. Oznacza to, że jeśli po wykryciu terminalu NFC nie znaleziono żadnej aktywności odbierającej terminale NDEF bądź zgodnej z określonymi w nim technologiami lub jeśli mamy do czynienia z nieznanym typem terminalu, zostanie utworzona intencja zawierająca działanie ACTION_TAG_DISCOVERED. Listing 26.12. Filtr intencji dla nieznanego lub nieprzetwarzanego terminalu NFC

Zwróćmy uwagę, że dla tego filtru intencji nie zdefiniowano żadnego węzła ani , ponieważ nie są przenoszone żadne dane w intencji oznaczonej działaniem ACTION_TAG_DISCOVERED. W normalnej sytuacji oznaczałoby to konieczność wprowadzenia znacznika . Sprawa ma się jednak inaczej z intencjami terminali NFC. Stanowią one specjalny przypadek, zatem w filtrach intencji nie są wymagane tego typu terminale w celu dopasowania intencji. Jeżeli intencja otrzymuje działania ACTION_TAG_DISCOVERED, oznacza to, że system nie zdołał odnaleźć aktywności dla terminali NFC. W tym momencie każda aktywność przyjmująca to działanie otrzyma intencję tego terminalu. W większości standardowych operacji nigdy nie natrafimy na intencję znacznika ACTION_TAG_DISCOVERED, ponieważ większość terminali NFC będzie dopasowanych do kryteriów NDEF lub TECH. Istnieje jeszcze jeden sposób, w jaki aktywność może otrzymać intencję terminalu NFC — zastosowanie systemu dyspozycji pierwszoplanowej. Jeżeli nasza aktywność znajduje się na pierwszym planie (co oznacza, że została uruchomiona metoda onResume() i użytkownik korzysta z tej aktywności), możemy utworzyć intencję oczekującą, tablicę filtrów intencji, tablicę list technologii, a następnie wprowadzić następujące wywołanie: mAdapter.enableForegroundDispatch(this, pendingIntent, intentFiltersArray, techListsArray);

gdzie mAdapter jest adapterem NFC, a this stanowi odniesienie do naszej aktywności. Za pomocą tego wywołania skutecznie wystawiamy naszą aktywność przed wszystkie pozostałe aktywności i jeżeli którykolwiek z jej filtrów jest dopasowany do wykrytego terminalu NFC, to aktywność ta przetworzy terminal. Jeżeli aktywność nie otrzyma intencji terminalu z powodu niedopasowania, jej działanie będzie sprawdzane wobec pozostałych aktywności. Musimy wywołać tę metodę z poziomu wątku interfejsu użytkownika, a najlepiej tego dokonać w metodzie onResume() naszej aktywności. Wymagane byłoby również wprowadzenie następującego wywołania: mAdapter.disableForegroundDispatch(this);

z poziomu metody zwrotnej onPause(), dzięki czemu nasza aktywność nie otrzyma intencji, której nie będzie mogła przetworzyć. Gdy aktywność w taki sposób otrzyma intencję, przekaże ją za pomocą metody zwrotnej onNewIntent(). Mamy tu do czynienia ze standardową intencją oczekującą. Tablica intentFiltersArray może stanowić zbiór potrzebnych nam obiektów IntentFilter, z których każdy definiuje określone działanie, a także, w razie potrzeby, dowolne dane lub typy MIME. Na listingu 26.13 widzimy przykładowy kod generujący filtr intencji dla obiektu Ndef, który następnie zostaje wstawiony do tablicy.

946 Android 3. Tworzenie aplikacji Listing 26.13. Kod filtru intencji dla technologii Ndef IntentFilter ndef = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED); try { ndef.addDataType("text/*"); } catch (MalformedMimeTypeException e) { throw new RuntimeException("fail", e); } intentFiltersArray = new IntentFilter[] { ndef, };

Nie zapominajmy, że w tablicy filtrów intencji może się znaleźć wiele wystąpień obiektów IntentFilter, zawierających te same lub różne działania, a także posiadających dane lub ich pozbawione. To samo dotyczy wartości pól typów. Obiekt techListsArray jest tablicą, której wartościami są inne tablice — listy zawierające nazwy klas obsługiwanych przez znacznik NFC. Możemy określić wiele list technologii. Zostało to zaprezentowane na listingu 26.14, który jest odpowiednikiem pliku zasobów ukazanego na listingu 26.11. Listing 26.14. Kod tabeli zawierającej listy technologii techListsArray = new String[][] { new String[] { NfcA.class.getName(), MifareUltralight.class.getName() }, new String[] { NfcB.class.getName(), Ndef.class.getName() } };

Jeśli po przeprowadzeniu omawianego procesu konfiguracji nasza aktywność uzyska dostęp do intencji terminalu NFC, spowoduje to uruchomienie metody zwrotnej onNewIntent() w celu odebrania terminalu. Z tego miejsca możemy odczytać dodatkową zawartość intencji, którą stanowią przechowywane informacje terminalu, co będzie tematem następnego podpunktu. Owszem, aby w dynamiczny sposób uzyskiwać dostęp do intencji terminalu NFC, trzeba włożyć dużo pracy, jednak z drugiej strony, jeżeli po uruchomieniu przez użytkownika tylko ta aktywność odbierała terminale, warto wykorzystać pokazane tu rozwiązanie. Zwróćmy jeszcze uwagę, że prawdopodobnie nie ma sensu jednoczesne korzystanie z tej metody i umieszczanie filtrów intencji w pliku manifeście, jednak z technicznego punktu widzenia jest to możliwe.

Odczytywanie terminali NFC Jak już wcześniej sugerowaliśmy, odczytywanie terminali NFC jest dość skomplikowaną czynnością. Dokładniej mówiąc, sam proces dostarczania terminalu do aplikacji jest złożony. Wyjaśniając to na najbardziej podstawowym poziomie, w momencie wykrycia terminalu NFC system spróbuje określić aktywność, do której należy wysłać intencję tego terminalu. W przeciwieństwie do aktywności obsługujących pozostałe czujniki omawiane w tym rozdziale, aktywność przetwarzająca terminale NFC nie musi być uruchomiona w momencie ich wykrycia i z pewnością nie otrzyma informacji o terminalu za pomocą obiektu nasłuchującego. Powiadomiona aktywność otrzyma intencję, a to z kolei może oznaczać jej uruchomienie w celu przetworzenia danych terminalu.

Rozdział 26 „ Czujniki

947

Jedną z pierwszych kwestii rozważanych w procesie projektowania aplikacji otrzymującej i przetwarzającej intencje NFC jest konieczność obsługi fizycznego terminalu znajdującego się w otoczeniu urządzenia za pomocą interfejsu sprzętowego. Interfejs API generuje wywołania blokujące, co oznacza, że nie będą one przekazywane tak szybko, jak byśmy sobie tego życzyli, w związku z czym będziemy musieli uruchamiać metody terminalu w osobnym wątku. Informacje terminalu NFC są umieszczone w pakiecie dodatkowych danych otrzymywanej intencji. Po odebraniu intencji możemy uzyskać dostęp do tych informacji za pomocą następującego fragmentu kodu: Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); String[] techlists = tag.getTechLists();

Jeżeli zdefiniowany przez nas filtr intencji jest bardzo dokładny, będziemy już doskonale wiedzieć, jaki rodzaj terminalu otrzymaliśmy. Jeżeli jednak zdefiniowaliśmy większą liczbę akceptowanych technologii terminalu, możemy teraz sprawdzić listy tych technologii, aby poznać mechanizmy, z jakimi będziemy mieli do czynienia w terminalu. Każdy ciąg znaków na tej liście stanowi nazwę klasy przechowującej technologię obsługiwaną przez wykryty terminal. Jeżeli nasz terminal obsługuje klasę android.nfc.tech.Ndef, możemy wykorzystać poniższy kod do uzyskania bardziej bezpośredniego dostępu do danych NDEF: NdefMessage[] ndefMsgs = intent.getParcelableArrayExtra ´(NfcAdapter.EXTRA_NDEF_MESSAGES);

Teoretycznie możemy otrzymać wartość null, w przypadku gdy intencja nie będzie zawierała informacji NDEF. W przeciwnym wypadku nie powinno być problemu z analizą składni przesyłanych danych. Możemy odczytywać elementy klasy NdefMessage z poziomu intencji, zliczać je i w przypadku każdej z nich odbierać zawarte w nich obiekty NdefRecord. Obiekty klasy NdefRecord są dość interesujące. Nie zaszkodzi, jeżeli Czytelnik zajrzy do specyfikacji technologii NFC, dostępnej pod adresem http://www.nfc-forum.org/specs/. Aby uzyskać do niej dostęp, trzeba zaakceptować postanowienia licencyjne NFC Forum. Jest to bezpłatny proces, należy jednak podać imię i nazwisko, adres, numer telefonu i adres e-mail. Alternatywnym rozwiązaniem jest aplikacja NfcDemo dostępna w pakiecie SDK Androida 2.3.3, w folderze z przykładowymi programami. Kod źródłowy tego przykładu został umieszczony na stronie http://developer. android.com/resources/samples/NFCDemo/index.html. Aplikacja ta odbiera intencje NFC i wyświetla zawartość klasy NdefRecord w widoku ListView. Sytuacja komplikuje się z powodu obecności kilku odmian klasy NdefRecord, które mogą zostać przesłane wraz z obiektami NdefMessage. Każda odmiana tej klasy ma inne zastosowanie. Na przykład odmiana Text przechowuje tekst w określonym języku. W odmianie Uri znajdziemy identyfikator URI. Ze wszystkich znanych rodzajów rekordów NDEF aplikacja NfcDemo obsługuje tylko trzy, z czego dwa przed chwilą omówiliśmy, a trzecim jest SmartPoster, któremu wkrótce przyjrzymy się uważniej. Format klasy NdefRecord składa się z trzybitowego pola TNF (ang. Type Name Format — format nazwy typu), pola typu o zmiennej długości, pola identyfikatora o zmiennej długości oraz pola ładunku o zmiennej długości. A zatem mamy do czynienia z dwoma kategoriami pól. Pole TNF stanowi podstawowy typ tego obiektu i definiuje zawartość całego rekordu. Może być to na przykład bezwzględny rekord URI (TNF_ABSOLUTE_URI) lub oficjalny rekord RTD (TNF_WELL_KNOWN). Następne pole typu przechowuje dokładniejsze informacje na temat rodzaju rekordu w oparciu o wartość pola TNF. Jeżeli w polu TNF została zdefiniowana stała TNF_WELL_KNOWN, pole to będzie się składało ze stałych RTD_* klasy NdefRecord, takich jak RTD_SMART_POSTER. Jeżeli wartością pola TNF jest TNF_ABSOLUTE_URI, kolejnym polem typu będzie konstrukt BNF niezależnego identyfikatora URI, zdefiniowany w specyfikacji RFC 3986.

948 Android 3. Tworzenie aplikacji Typ rekordu TNF_UNCHANGED jest stosowany, w przypadku gdy ładunek komunikatu z powodu rozmiarów zostaje rozmieszczony w kilku obiektach NdefRecord. System automatycznie obsługuje tego typu podzielone obiekty NdefRecord, więc nie powinniśmy nigdy mieć do czynienia z wartością TNF_UNCHANGED. Pakiet android.nfc łączy poszczególne elementy ładunku w jeden wielki obiekt NdefRecord.

Następnym polem obiektu NdefRecord jest jego identyfikator. Odczytywany rekord może posiadać identyfikator, chociaż nie jest to wymagane. Na końcu mamy do czynienia z ładunkiem. Może to być dość duża tablica bajtowa, która posiada jednak wewnętrzną strukturę, zależną od rodzaju obiektu NdefRecord. Trzeba zwrócić uwagę na tę strukturę. W przypadku typu RTD URI pierwszy bajt tej tablicy reprezentuje początek identyfikatora URI. Na przykład wartość 1 oznacza http://www. — i od tego członu będzie rozpoczynał się każdy identyfikator URI znajdujący się we wnętrzu ładunku. W przypadku typu rekordu Text pierwszy bajt tabeli ładunku stanowi wartość kodowania tekstu (UTF-8 lub UTF-16), a także długość tablicy języka, występującej tuż po polu stanu kodowania. Za polem języka znajduje się właściwy tekst. W przypadku obiektów SmartPoster sprawa jest bardziej skomplikowana, gdyż każdy obiekt NdefRecord przechowuje obiekty NdefMessage, przechowujące z kolei więcej obiektów NdefRecord. Te ostatnie mogą zawierać rekord Title (zbudowany tak samo jak rekordy Text), rekord URI (nieróżniący się od omawianego wcześniej), rekord zalecanego działania, rekord rozmiaru, rekord ikony oraz rekord typu. Wartość zalecanego działania wskazuje na czynności, jakie aplikacja może wykonywać w przypadku danych obiektu SmartPoster. Zwróćmy uwagę, że wartości zalecanego działania nie są wymieniane w dokumentacji klasy NdefRecord. Są one następujące: -1 0 1 2

UNKNOWN DO_ACTION SAVE_FOR_LATER OPEN_FOR_EDITING

Tylko od nas zależy, co z nimi zrobimy, chociaż oczywiście prawdopodobnie będziemy chcieli spróbować przeprowadzić na odczytywanym terminalu najwłaściwszą operację. Jeśli na przykład mamy do czynienia z formatem rekordu TNF_WELL_KNOWN, jego typem jest RTD_SMART_POSTER, a zalecane działanie przybiera wartość 0 (DO_ACTION) i jest połączone z adresem URL strony WWW, najprawdopodobniej będziemy chcieli uruchomić przeglądarkę internetową i otworzyć stronę dostępną pod tym adresem. Rekord rozmiaru pozwala zdefiniować wielkość obiektu czekającego po drugiej stronie łącza. Jeżeli terminal odnosi się do pobieralnego pliku, w rekordzie rozmiaru może zostać określony jego rozmiar. W rekordzie ikony przechowywany jest obraz ikony, wykorzystywany przez urządzenie do jej wyświetlania wraz z tytułem oraz adresem URL. Rekord typu nie jest tym samym co wartość formatu TNF czy typ klasy NdefRecord. Jest on wykorzystywany w terminalach SmartPoster, a w tym przypadku reprezentuje on typ MIME obiektu znajdującego się po drugiej stronie adresu URL. Urządzenie może nie obsługiwać danego typu MIME i w ten sposób zostanie uniemożliwione pobieranie tego obiektu. Jedynym niezbędnym podrekordem terminalu SmartPoster jest rekord URI i tylko jedno jego wystąpienie może się znajdować w każdym terminalu. Możemy wypisać wiele rekordów Title, każdy przechowujący tekst w innym języku. Możliwe jest również zamieszczanie wielu rekordów ikony, pod warunkiem że każdy z nich posiada osobny typ MIME dla swojego formatu.

Rozdział 26 „ Czujniki

949

W przypadku wszystkich rodzajów terminali NFC, w tym również terminali NDEF, możemy zastosować poniższy fragment kodu, aby uzyskać wystąpienie danego typu terminali: NfcA nfca = NfcA.get(tag);

Za pomocą tak utworzonego obiektu możemy uzyskać dostęp do określonych metod, najbardziej nadających się dla danego typu terminalu. W przypadku terminali Ndef i NdefFormatable klasy NdefMessage oraz NdefRecord okazują się bardzo przydatne do przetwarzania ich danych. Klasy pozostałych rodzajów terminali posiadają odpowiednie metody umożliwiające obsługę tych terminali i zawartych w nich informacji. Mamy do dyspozycji odpowiednie metody do odczytu i zapisu danych na terminalu. Zwróćmy uwagę, że zapisywanie danych w terminalu nie jest tym samym co emulacja karty. Zapisywanie terminalu oznacza, że w pobliżu urządzenia znajduje się jakiś terminal, który można zmodyfikować (jeśli mamy odpowiednie uprawnienia). Emulacja karty jest oddzielnym procesem.

Emulacja karty NFC Emulacja karty oznacza imitowanie zachowania urządzenia wyposażonego w czujnik NFC jako terminal NFC przed odpowiednim czytnikiem. Oznacza to, że w określonym miejscu lokalnego urządzenia są przechowywane dane. Jeżeli w zasięgu tego urządzenia znajdzie się czytnik NFC, który zażąda dostępu do tych informacji, zostaną mu one udostępnione. Funkcja ta nie była jeszcze dostępna w czasie pisania tej książki, chociaż oczekujemy, że w końcu będzie można z niej korzystać. Jeżeli emulacja karty jest Czytelnikowi naprawdę potrzebna, zalecamy zapoznanie się z dokumentacją sieciową, omawiającą przeprowadzenie tego procesu na najbardziej elementarnym poziomie urządzenia, tj. na poziomie zestawu NDK.

Połączenia równorzędne (P2P) NFC Zestaw Android SDK umożliwia w ograniczonym stopniu obsługę komunikacji P2P (ang. peer-to-peer — komunikacja równorzędna) pomiędzy dwoma urządzeniami za pomocą technologii NFC. Funkcja ta nie jest pozbawiona wad, mianowicie korzystająca z niej aplikacja musi być uruchomiona i działać na pierwszym planie, a ponadto obsługiwać format NDEF. Być może pozostałe technologie będą w przyszłości obsługiwać komunikację P2P, na razie jednak pozwala na to jedynie format NDEF. Oznacza to także, że telefon musi być włączony, a aplikacja uruchomiona, aby móc w ten sposób nawiązać komunikację z innym urządzeniem. Aby zaimplementować funkcję P2P, wykorzystamy metodę klasy NfcAdapter zwaną enable Przyjmuje ona dwa parametry: aktywność oraz obiekt NdefMessage, które zostaną wysłane w momencie żądania danych przez inne urządzenie NFC. Podobnie jak w przypadku omówionego wcześniej systemu dyspozycji pierwszoplanowej, metoda ta powinna być wywoływana we wnętrzu metody onResume(), a wyłączana w metodzie onPause(). Obiekt NdefMessage może być dowolny, ale nasza aktywność powinna się znajdować na pierwszym planie podczas próby uzyskania dostępu do danych urządzenia przez czytnik. Firma Google stwierdziła, że urządzenie znajdujące się po drugiej stronie musi posiadać implementację protokołu wysyłania NDEF zawartego w pakiecie com.android.npp, co umożliwi nawiązywanie komunikacji z telefonem, jednak w momencie pisania książki nie było żadnych konkretnych informacji na ten temat. Będziemy jednak zamieszczać aktualizacje na naszej stronie. ´ForegroundNdefPush().

950 Android 3. Tworzenie aplikacji Wyjaśnialiśmy wcześniej sposób wykorzystania węzła uses-feature zawierającego wpisy o czujnikach, dzięki czemu można się było upewnić, że dane urządzenie jest wyposażone w czujniki niezbędne do działania danej aplikacji jeszcze przed jej instalacją. Czujnik NFC nie stanowi w tym przypadku wyjątku. Powinniśmy umieścić następujący fragment w pliku AndroidManifest.xml, jeżeli chcemy, aby nasza aplikacja była instalowana jedynie na urządzeniach wyposażonych w czujnik NFC:

Lepiej upewnić się również, że plik AndroidManifest.xml zawiera odpowiednie uprawnienia, pozwalające aplikacji na uzyskanie dostępu do technologii NFC:

Testowanie technologii NFC za pomocą aplikacji NfcDemo Omówiliśmy dużą część interfejsu technologii NFC, teraz jednak warto zadać pytanie: w jaki sposób możemy przetestować naszą aplikację? Jeśli chodzi o terminale NFC, być może udałoby się znaleźć w pobliżu jakieś wyposażone w nie obiekty. Nie powinno być to zbyt trudne w krajach, w których technologia ta zdążyła się już zadomowić. W Polsce może być o wiele trudniej. Możemy zakupić własne terminale NFC; na całym świecie można znaleźć kilku producentów sprzedających te podzespoły, a także odpowiednie oprogramowanie, umożliwiające zapisywanie na nich informacji. Niestety, narzędzie DDMS nie zostało wyposażone w funkcję przesyłania intencji wykrywanych terminali do emulatora. Przykładowa aplikacja NfcDemo została dołączona do wersji 2.3 Androida w czasach, gdy było dostępne wyłącznie działanie intencji ACTION_TAG_ ´DISCOVERED. Wraz z wersją 2.3.3 Android się rozwinął, czego nie można powiedzieć o aplikacji NfcDemo. Można w niej znaleźć przydatne informacje na temat układów graficznych terminali NFC oraz o znaczeniu poszczególnych bajtów w tabelach danych terminali. Miejmy nadzieję, że wkrótce pojawi się zaktualizowana wersja tej aplikacji testowej, dostosowana do pracy z fizycznymi terminalami NFC oraz nowym systemem technologii NFC. Jeżeli Czytelnik zdecyduje się na wczytanie przykładowej aplikacji NfcDemo, potrzebna mu będzie dodatkowa, zewnętrzna biblioteka. Plik tej biblioteki znajdziemy na stronie http://code.google.com/ p/guava-libraries/. Po rozpakowaniu pliku ZIP uzyskamy dostęp do plików JAR. Należy zapisać w stacji roboczej plik guava (bez członu gwt). Trzeba utworzyć w środowisku Eclipse odniesienie do tego pliku; w tym celu należy kliknąć nazwę projektu prawym przyciskiem myszy, wybrać opcję Build Path, następnie Configure Build Path i otworzyć zakładkę Libraries. Następnie trzeba kliknąć opcję Add External JARs, wyszukać plik JAR, wybrać go i kliknąć przycisk Open. Pozostaje jeszcze ponowne skompilowanie projektu NfcDemo, trzeba więc kliknąć jego nazwę prawym przyciskiem myszy i wybrać opcję Build Project.

Odnośniki Znajdziemy tu łącza do materiałów pomocnych w zrozumieniu koncepcji zawartych w tym rozdziale: „ ftp://ftp.helion.pl/przyklady/and3ta.zip — znajdziemy tu listę projektów utworzonych specjalnie na potrzeby niniejszej książki. Projekty przeznaczone dla tego rozdziału zostały umieszczone w katalogu ProAndroid3_R26_Czujniki. Każdy z zawartych w nim projektów znajduje się w osobnym katalogu. W katalogu umieściliśmy również plik Czytaj.TXT, stanowiący dokładną instrukcję importowania projektów do środowiska Eclipse.

Rozdział 26 „ Czujniki

„ „

„

„

„

„ „

951

http://en.wikipedia.org/wiki/Lux — informacje o jednostce natężenia światła — luksie. http://android-developers.blogspot.com/2010/09/one-screen-turn-deserves-another.html — wpis dotyczący zagadnienia obracania ekranu i poprawnego wyświetlania jego zawartości. www.ngdc.noaa.gov/geomag/faqgeom.shtml — znajdziemy tu informacje o geomagnetyzmie pochodzące od agencji NOOA. www.youtube.com/watch?v=C7JQ7Rpwn2k — prezentacja Google TechTalk autorstwa Davida Sachsa, dotycząca akcelerometrów, żyroskopów i kompasów w odniesieniu do Androida. http://stackoverflow.com/questions/1586658/combine-gyroscope-and-accelerometer-data — przyjemny artykuł dotyczący łączenia odczytów pochodzących z żyroskopów i akcelerometrów w celu wykorzystania tych danych w aplikacjach. www.nfc-forum.org/specs — oficjalna strona specyfikacji technologii NFC. www.slideshare.net/tdelazzari/architecture-and-development-of-nfc-applications — bardzo szczegółowa prezentacja autorstwa Thomasa de Lazzariego dotycząca technologii NFC.

Podsumowanie W niniejszym rozdziale przyjrzeliśmy się głównej architekturze czujników w Androidzie, a także funkcji komunikacji bliskiego pola — NFC. Zademonstrowaliśmy mechanizm odczytywania danych generowanych przez czujniki oraz sposoby ich przetwarzania. Teraz Czytelnik powinien być w stanie tworzyć świetne aplikacje współpracujące z nowoczesnymi urządzeniami w otaczającym świecie.

952 Android 3. Tworzenie aplikacji

R OZDZIAŁ

27 Analiza interfejsu kontaktów

W rozdziale 4., dotyczącym dostawców treści, wymieniliśmy zalety wynikające z eksponowania danych za pomocą abstrakcji dostawcy treści. Udowodniliśmy również, że takie wyodrębnione dane są dostępne w postaci zbioru adresów URL, które inne obiekty mogą odczytywać, wysyłać do nich zapytania, a także aktualizować oraz wstawiać lub usuwać w ich wnętrzu własne informacje. Wspomniane adresy URL oraz ich kursory stanowią podstawę interfejsu API dostawcy treści. Jednym z takich interfejsów API dostawców treści jest interfejs kontaktów służący do pracy z danymi kontaktowymi. W Androidzie kontakty są przechowywane w bazie danych i eksponujemy je za pomocą dostawcy treści, którego uprawnienie rozpoczyna się od segmentu: content://com.android.contacts

Dokumentacja zestawu Android SDK wymienia wiele różnych kontraktów zawieranych przez tego dostawcę kontaktów za pomocą różnorodnych interfejsów i klas Java, które są przechowywane w pakiecie: android.provider.ContactsContract

W czasie tworzenia aplikacji natrafimy na wiele klas, pomocnych w wysyłaniu zapytań, odczytywaniu, aktualizowaniu oraz wstawianiu kontaktów do lub z bazy kontaktów, których nadrzędnym kontekstem jest ContactsContract. Podstawowa dokumentacja omawiająca zastosowanie interfejsu API kontaktów jest dostępna na stronie systemu Android: http://developer.android.com/resources/articles/contacts.html Główny punkt wejściowy interfejsu, czyli ContactsContract, nosi właściwą nazwę, ponieważ klasa ta definiuje kontrakt pomiędzy klientami kontaktów a dostawcą i zabezpieczeniem bazy kontaktów. W tym rozdziale omówimy pojęcie kontraktu dość szczegółowo, nie wymienimy jednak każdego najdrobniejszego detalu. Interfejs kontaktów jest bardzo rozbudowany, a jego korzenie sięgają naprawdę daleko. Gdy jednak poświęcimy mu uwagę, po kilku tygodniach zauważymy, że jego zasadnicza struktura wcale nie jest taka skomplikowana. Właśnie na tej strukturze chcielibyśmy się skupić najbardziej i wyjaśnić jej podstawowe mechanizmy, co umożliwi Czytelnikowi jej zrozumienie w czasie potrzebnym na przeczytanie rozdziału.

954 Android 3. Tworzenie aplikacji

Koncepcja konta Wszystkie kontakty w systemie Android działają w kontekście konta. Czym jest konto? Jeśli na przykład korzystamy ze skrzynki pocztowej umieszczonej na serwerze Google, to znaczy, że posiadamy konto Google. Jeżeli umieściliśmy swoje dane w serwisie Facebook i jesteśmy jego użytkownikami, staliśmy się posiadaczami konta Facebook. Nawet jeśli korzystamy tylko z poczty e-mail na serwerze Google, te same nazwa użytkownika i hasło dają nam dostęp do pozostałych usług tej firmy, zatem nasze konto pocztowe nie jest ograniczone wyłącznie do skrzynki pocztowej. Jednak niektóre rodzaje kont są ograniczone do jednego rodzaju usługi, czego przykładem jest konto pocztowe typu POP (ang. Post Office Protocol — protokół węzłów pocztowych). W przypadku urządzenia mobilnego możemy zarejestrować wiele różnych usług opartych na korzystaniu z konta. Część z takich kont, na przykład konta Google, Facebook lub firmowe konto Microsoft Exchange, możemy ustanowić z poziomu widoku Konta i synchronizacja, dostępnego w aplikacji Ustawienia. Więcej informacji dotyczących kont znajdziemy w instrukcji użytkownika systemu Android. W podrozdziale „Odnośniki” zamieściliśmy do niej adres URL.

Szybki przegląd ekranów związanych z kontami Aby ułatwić zrozumienie natury kontaktów, przejrzyjmy najpierw kilka ekranów z nimi związanych, które znajdziemy w emulatorze. Rozpocznijmy od ekranu aplikacji Ustawienia, pokazanego na rysunku 27.1.

Rysunek 27.1. Wywoływanie widoku ustawień Konta i synchronizacja

Po zaznaczeniu elementu menu Konta i synchronizacja zostanie wyświetlony ekran Ustawienia kont i synchronizacji, który widzimy na rysunku 27.2. Znajdziemy tutaj, obok kilku opcji związanych z kontami, również listę kont powiązanych z urządzeniem.

Rozdział 27 „ Analiza interfejsu kontaktów

955

Rysunek 27.2. Ustawienia kont i synchronizacji

Na rysunku 27.2 interesuje nas głównie lista dostępnych kont. W celach ćwiczeniowych kliknijmy przycisk Dodaj konto, dzięki czemu ujrzymy listę dostępnych kont, które możemy skonfigurować lub dodać (rysunek 27.3).

Rysunek 27.3. Lista kont, które możemy skonfigurować

Lista ta będzie miała różną zawartość w zależności od rodzaju urządzenia oraz dostępnych elementów. Na rysunku 27.3 zaprezentowaliśmy listę dostępnych kont dla wersji 2.3 Androida zainstalowanej na emulatorze, gdzie docelowy jest 9. poziom interfejsów Google API. Jeżeli pobrano samą podstawową wersję zestawu SDK, nie będzie można wybrać interfejsów Google API

956 Android 3. Tworzenie aplikacji dla emulatora, zatem nie będzie można również skonfigurować konta Google, które znajduje się na liście widocznej na rysunku 27.3. Oznacza to również, że lista dostępnych kont zależy od wersji systemu Android, producenta urządzenia oraz operatora sieci lub dostawcy usług. Ponadto, w zależności od dostawcy, liczba kont i rodzaje pól wymaganych do ich konfiguracji mogą się różnić. Jeśli na przykład wybierzemy w emulatorze konto Google, otrzymamy możliwość utworzenia nowego konta lub zalogowania się do już istniejącego (rysunek 27.4).

Rysunek 27.4. Dodawanie konta Google

Jeśli klikniemy przycisk Utwórz, pojawią się pola wymagane do założenia nowego konta Google, co zostało ukazane na rysunku 27.5.

Rysunek 27.5. Tworzenie konta Google

Rozdział 27 „ Analiza interfejsu kontaktów

957

Na rysunku 27.5 widzimy pola wymagane do utworzenia nowego konta Google, jeśli użytkownik jeszcze go nie posiada. Jak już stwierdziliśmy, liczba i treść pól mogą się różnić w zależności od rodzaju konta. Teraz pokażemy, w jaki sposób wprowadzić ustawienia dla już istniejącego konta Google. W tym przypadku cały proces konfiguracji ogranicza się do zalogowania się na swoje konto, tak jak widać na rysunku 27.6.

Rysunek 27.6. Logowanie się na istniejące konto Google

Skoro zademonstrowaliśmy już podstawy dotyczące kont oraz sposób ich umieszczania w urządzeniu, wyjaśnijmy, dlaczego konta pełnią taką ważną rolę dla kontaktów.

Związek pomiędzy kontami a kontaktami Kontakty zarządzane przez użytkownika są powiązane z określonym kontem. Inaczej mówiąc, każde zarejestrowane na urządzeniu konto może przechowywać dużą liczbę związanych z nim kontaktów. Konto jest właścicielem zbioru kontaktów albo jest ono nadrzędne w stosunku do danego kontaktu. Równie dobrze konto może nie zawierać żadnego kontaktu. Konto jest definiowane za pomocą dwóch ciągów znaków: jego nazwy oraz typu. W przypadku konta Google mamy do czynienia z nazwą użytkownika skrzynki pocztowej Gmail, a typem konta jest com.google. Oczywiście, typ konta musi być niepowtarzalny w obrębie całego urządzenia. W zakresie danego typu konta jego nazwa również musi być jedyna w swoim rodzaju. Typ i nazwa tworzą razem konto, a natychmiast po jego utworzeniu możemy zająć się wstawianiem do niego kontaktów.

Wyliczanie kont Zasadniczo interfejs kontaktów obsługuje kontakty przechowywane wewnątrz różnych kont. Samo tworzenie kont zachodzi poza interfejsem kontaktów, zatem opis możliwości związanych z pisaniem własnych dostawców kont oraz synchronizowania z nimi kontaktów wykracza poza zakres tego rozdziału. Sam proces konfiguracji kont jest nieistotny dla niniejszego rozdziału. Jeżeli jednak chcemy dodać kontakt lub listę kontaktów, musimy wiedzieć, jakie konta są dostępne

958 Android 3. Tworzenie aplikacji w urządzeniu. Możemy zastosować kod z listingu 27.1 do wyświetlenia rodzajów kont oraz ich wymaganych właściwości (nazwa i typ konta). Kod z listingu 27.1 generuje nazwy i typy kont w zależności od zmiennego kontekstu. Listing 27.1. Kod umożliwiający wyświetlenie listy kont public void listAccounts(Context ctx) { AccountManager am = AccountManager.get(ctx); Account[] accounts = am.getAccounts(); for(Account ac: accounts) { String acname=ac.name; String actype = ac.type; Log.d("accountInfo", acname + ":" + actype); } }

Oczywiście, aby uruchomić kod z listingu 27.1, w pliku manifeście musi zostać umieszczone odpowiednie uprawnienie, widoczne na listingu 27.2. Listing 27.2. Uprawnienie pozwalające na odczytywanie zawartości kont

Kod z listingu 27.1 spowoduje wyświetlenie mniej więcej następującej informacji: Adres-e-mail-serwisu-google:com.google

W tym przypadku przyjęliśmy, że posiadamy skonfigurowane tylko jedno konto (Google). Jeżeli jest ich więcej, wszystkie zostaną wyświetlone w podobny sposób. Zanim zajmiemy się bardziej szczegółowo kontaktami, zastanówmy się, w jaki sposób użytkownicy tworzą kontakty za pomocą aplikacji fabrycznie umieszczonej w systemie Android.

Aplikacja Kontakty Jeżeli producent urządzenia, na przykład Motorola, lub operator (przykładowo Verizon1) nie przewidzieli własnej aplikacji zarządzającej kontaktami, z pomocą przychodzi system Android i jego domyślna aplikacja. Znajdziemy ją bez trudu na liście aplikacji dostępnych w urządzeniu, jej dokumentacja również została zamieszczona w instrukcji obsługi systemu Android.

Wyświetlanie kontaktów Po uruchomieniu aplikacji Kontakty pierwszym z wyświetlonych ekranów będzie lista kontaktów (rysunek 27.7). Zasadniczo kontaktem są dane dotyczące osoby, którą znamy w kontekście konta, np. Gmail. Jeżeli posiadamy wiele kont, lista z rysunku 27.7 zostanie wypełniona pochodzącymi z nich kontaktami. Spoglądając na ten ekran, nie dowiemy się, z którym kontem jest 1

Verizon Communications, Inc. — amerykański dostawca usług telekomunikacyjnych — przyp. red.

Rozdział 27 „ Analiza interfejsu kontaktów

959

Rysunek 27.7. Wyświetlanie zebranych kontaktów

powiązany dany kontakt. System postara się nie powielać takich samych kontaktów pochodzących z różnych kont, chyba że zostanie to jawnie dozwolone. W następnym podrozdziale zajmiemy się tą heurystyką „podobnych kontaktów”. Odnosząc się do sytuacji z rysunku 27.7, założyliśmy, że są dostępne dwa kontakty, które są alfabetycznie uszeregowane.

Wyświetlanie szczegółów kontaktu Jeżeli klikniemy jeden z kontaktów widocznych na ekranie z rysunku 27.7, zostaną wyświetlone jego szczegóły, widoczne na rysunku 27.8.

Rysunek 27.8. Szczegóły kontaktu

960 Android 3. Tworzenie aplikacji Na rysunku 27.8 zaprezentowaliśmy różne rodzaje informacji, przechowywane przez kontakt. Widzimy na nim również, jakie działania może przeprowadzić aplikacja zarządzająca kontaktami na danym kontakcie w zależności od liczby wypełnionych w nim pól. W przypadku niektórych pól możemy wykonać połączenie telefoniczne i wysłać wiadomość tekstową, a inne pozwalają na wysłanie wiadomości e-mail lub rozmowę przez komunikator internetowy.

Edytowanie szczegółów kontaktu Przyjrzyjmy się teraz, w jaki sposób możemy edytować (lub utworzyć nowy) kontakt zaprezentowany na rysunku 27.8. Dokonujemy tego poprzez wciśnięcie przycisku Menu i wybranie opcji Edytuj kontakt lub Nowy kontakt. Zostanie wyświetlony ekran zilustrowany na rysunku 27.9.

Rysunek 27.9. Edycja kontaktu

W górnej części zaprezentowanego na rysunku 27.9 ekranu Edytuj kontakt ujrzymy nazwę konta, w ramach którego dany kontakt jest modyfikowany lub tworzony. W przypadku omawianego kontaktu jedynym kontem jest telefon, co oznacza, że nie ma dla niego skonfigurowanego żadnego konta serwerowego (np. Google) oraz że mamy do czynienia z kontem lokalnym. Rzeczywiście, w bazie kontaktów nazwa i typ konta przyjmują w tym przypadku wartości null. Firma Google usilnie zaleca utworzenie przynajmniej jednego konta na jej serwerze, zanim urządzenie pracujące pod kontrolą systemu Android zostanie uaktywnione, bez względu na to, czy korzystamy z telefonu, czy tabletu. Jak jednak widać, możemy tworzyć kontakt bez konieczności łączenia go z określonym kontem, w takich zaś przypadkach ekran widoczny w momencie tworzenia takiego kontaktu wygląda tak jak na rysunku 27.9. Patrząc na rysunek 27.9, zauważymy, że tuż za wskaźnikiem typu konta (Tylko telefon itd.) znajduje się miejsce na zdjęcie powiązane z kontaktem, a następnie zestaw pól. Rysunek 27.10 prezentuje następne pola, widoczne po przewinięciu ekranu.

Rozdział 27 „ Analiza interfejsu kontaktów

961

Rysunek 27.10. Więcej pól edycji

Jak widać na rysunku 27.10, istnieje możliwość przypisania do kontaktu różnych rodzajów numerów telefonów i adresów e-mail. Czytelnik zastanawia się pewnie również, czy w kontaktach możemy umieszczać własne pola zawierające niestandardowe dane (widoczne na rysunku 27.10 pola Telefon oraz E-mail stanowią znane, predefiniowane typy pól. Być może ktoś chciałby wstawić mniej oczekiwane formaty danych. To właśnie mamy na myśli, pisząc „niestandardowe”). Interfejs API kontaktów pozwala na wprowadzenie tego typu pól, co zostało zaprezentowane na rysunku 27.11, gdzie dodaliśmy dane adresowe do kontaktu.

Rysunek 27.11. Edytowanie niestandardowych danych kontaktowych

962 Android 3. Tworzenie aplikacji

Umieszczanie zdjęcia powiązanego z kontaktem Możemy wprowadzić również zdjęcie dotyczące danego kontaktu. Na rysunku 27.12 widzimy okno ustawień zdjęcia, które zostanie otwarte po kliknięciu ukazanego na rysunku 27.9 pola zarezerwowanego na fotografię (pierwsza strona szczegółowych danych kontaktu).

Rysunek 27.12. Edycja zdjęcia kontaktu

Eksportowanie kontaktów Zakończmy przegląd aplikacji zarządzającej kontaktami zapoznaniem się z mechanizmem eksportowania kontaktów na kartę SD. Funkcja ta pozwala nam między innymi na przeglądanie rodzajów danych przechowywanych w kontakcie oraz sprawdzenie, w jaki sposób są prezentowane w formie tekstowej (rysunek 27.13).

Rysunek 27.13. Eksportowanie kontaktów

Rozdział 27 „ Analiza interfejsu kontaktów

963

Po wyeksportowaniu kontaktów na kartę SD możemy przejrzeć jej zawartość za pomocą wtyczki ADT środowiska Android. Na rysunku 27.14 widzimy jeden z eksportowanych plików .vcf w perspektywie File Explorer środowiska Eclipse.

Rysunek 27.14. Informacje kontaktowe umieszczone na karcie SD

Możemy skopiować widoczny na rysunku 27.14 plik .vcf z urządzenia do stacji roboczej za pomocą ikon widocznych w prawym górnym rogu zakładki File Explorer. Zawartość pliku .vcf dla dwóch kontaktów widocznych na rysunku 27.8 będzie wyglądała tak jak zaprezentowano na listingu 27.3. Listing 27.3. Eksportowane kontakty w formacie VCF BEGIN:VCARD VERSION:2.1 N:C1-Nazwisko;C1-Imię;;; FN:C1-Imię C1-Nazwisko TEL;TLX:55555 TEL;WORK:66666 EMAIL;HOME:[emailprotected] EMAIL;WORK:[emailprotected] ORG:PracaKomp TITLE:Prezes ORG:Inna praca TITLE:Prezes URL:www.com NOTE:Uwaga1 X-AIM:aim X-MSN:wlive END:VCARD BEGIN:VCARD VERSION:2.1 N:C2-Nazwisko;C2-Imię;;; FN:C2-Imię C2-Nazwisko END:VCARD

964 Android 3. Tworzenie aplikacji

Różne typy danych kontaktowych Za pomocą dotychczas prezentowanych rysunków pokazaliśmy, w jaki sposób można dodawać różne rodzaje informacji do kontaktu. Na listingu 27.4 zaprezentowaliśmy listę typów danych zdefiniowanych w interfejsie kontaktów (w nowszych wersjach systemu może się ona rozrastać; prezentujemy wersję zgodną z Androidem 2.3). Listing 27.4. Standardowe typy danych kontaktowych email event groupmembership im nickname note organization phone photo relation SipAddress structuredname structuredpostal website

Każdy rodzaj danych, na przykład email czy structuredpostal (przechowujący kod pocztowy), posiada własny zestaw pól. Skąd możemy wiedzieć, jaki kształt przybierają te pola? Są one zdefiniowane w pomocniczych klasach, znajdujących się w pakiecie: android.provider.ContactsContract.CommonDataKinds

Dokumentacja tego pakietu jest dostępna pod adresem: http://developer.android.com/reference/android/provider/ContactsContract. CommonDataKinds.html Na przykład klasa CommonDataKinds.Email definiuje pola ukazane na listingu 27.5. Listing 27.5. Rodzaje pól w adresie typu e-mail kontaktu Adres e-mail Typ poczty e-mail: type_home, type_work, type_other, type_mobile Etykieta: do obsługi poczty type_other

Skoro znamy już podstawowe pojęcia i narzędzia wymagane do korzystania z kontaktów i kont, przejdźmy do właściwych szczegółów interfejsu kontaktów.

Analiza kontaktów Jak już stwierdziliśmy, kontakt jest przypisany do konta. Każde konto zawiera własny zbiór kontaktów. Z kolei na każdy kontakt przypada zestaw elementów danych (na przykład adres e-mail, numer telefonu, imię i nazwisko czy kod pocztowy). Co więcej, Android przedstawia

Rozdział 27 „ Analiza interfejsu kontaktów

965

zbiorczy widok nieprzetworzonych kontaktów, które umieszcza na liście kontaktów po sprawdzeniu, czy spełniają określone kryteria. Użytkownik widzi taką zbiorczą listą kontaktów w momencie uruchomienia aplikacji Kontakty (rysunek 27.8). Sprawdzimy teraz, w jaki sposób dane powiązane z kontaktami są przechowywane w różnych rodzajach tabel. Wyjaśnienie reguł rządzących tymi tabelami oraz powiązanymi z nimi widokami stanowi klucz do zrozumienia działania interfejsu kontaktów.

Badanie treści bazy danych SQLite Jednym ze sposobów zrozumienia i analizy bazodanowych tablic kontaktów jest pobranie treści bazy danych z urządzenia czy emulatora i przejrzenie jej za pomocą eksploratora SQLite. Aby pobrać bazę kontaktów, skorzystamy z widocznej na rysunku 27.14 perspektywy File Explorer, gdzie przejdziemy do następującego katalogu, znajdującego się w emulatorze: /data/data/com.android.providers.contacts/databases W zależności od wersji systemu nazwa bazy danych może się nieznacznie różnić, powinna jednak brzmieć contacts.db, contacts2.db lub podobnie. Teoretycznie wystarczy otworzyć bazę danych za pomocą odpowiedniego narzędzia. Wykryliśmy jednak problem związany z jej otwieraniem — większość narzędzi ulegało zawieszeniu. Kłopot polega na niestandardowych schematach zestawiania danych zdefiniowanych przez system Android dla takich działań jak porównywanie numerów telefonów. Najwidoczniej w przypadku bazy danych SQLite niestandardowe schematy zestawiania są kompilowane jako część dystrybucji silnika SQLite. Jeżeli nie posiadamy bibliotek DLL kompilowanych wraz z dystrybucją systemu Android, eksploratory ogólnego przeznaczenia nie będą w stanie poprawnie odczytywać bazy danych. Ponieważ narzędzia te wykorzystują biblioteki DLL systemu Windows do otwierania bazy danych SQLite utworzonej w systemie Android opartym na rdzeniu Linuksa, ich działania kończą się niepowodzeniem. Poza tym wersja silnika SQLite dla systemu Windows nie posiada schematów zestawiania, które są zdefiniowane jako niezbędny element bazy kontaktów. Trochę się nam jednak poszczęściło, gdyż w programie SQLite Explorer znalazł się błąd umożliwiający przeglądanie tabel, chociaż nie pozwala to na wyświetlanie schematu bazy danych. Możemy mieć więcej szczęścia z płatnymi aplikacjami. Jeżeli Czytelnika interesują inne alternatywy, poniżej zamieszczamy odnośnik do listy istniejących eksploratorów baz danych SQLite: http://www.sqlite.org/cvstrac/wiki?p=ManagementTools Jeżeli Czytelnik jest naprawdę dociekliwy, może dowiedzieć się więcej o schematach zestawiania z naszego artykułu Exploring Contacts db, dostępnego pod adresem http://www.androidbook.com/ item/3582. Jeśli Czytelnikowi nie uda się eksplorować bazy danych, nie wszystko jednak jest stracone, ponieważ w tym rozdziale umieściliśmy listę wszystkich najważniejszych tabel. Zaczniemy najpierw od analizy tak zwanych nieprzetworzonych kontaktów.

Nieprzetworzone kontakty Przypominamy, że kontakty widoczne po uruchomieniu aplikacji Kontakty są nazywane kontaktami zbiorczymi. Na każdy kontakt zbiorczy składa się zbiór tak zwanych nieprzetworzonych kontaktów. Kontakt zbiorczy stanowi jedynie widok zestawu podobnych do siebie

966 Android 3. Tworzenie aplikacji nieprzetworzonych kontaktów. Aby zrozumieć koncepcję kontaktów zbiorczych, najpierw musimy przeanalizować pojęcie nieprzetworzonego kontaktu oraz przechowywanych przez niego danych. Zajmiemy się więc w pierwszej kolejności nieprzetworzonymi kontaktami. Zestaw kontaktów przypisany do konta jest w rzeczywistości nazywany zestawem nieprzetworzonych kontaktów. Każdy nieprzetworzony kontakt definiuje określony aspekt osoby znanej użytkownikowi w kontekście danego konta. W przeciwieństwie do kontaktu nieprzetworzonego kontakt zbiorczy może być dostępny poza granicami danego konta i w konsekwencji jest ogólnie wykorzystywany w urządzeniu. Relacja pomiędzy kontem a jego zbiorem nieprzetworzonych kontaktów jest utrzymywana w tabeli nieprzetworzonych kontaktów. Listing 27.6 prezentuje strukturę tej tabeli w bazie kontaktów. Listing 27.6. Definicja tabeli nieprzetworzonych kontaktów CREATE TABLE raw_contacts (_id INTEGER PRIMARY KEY AUTOINCREMENT, is_restricted INTEGER DEFAULT 0, account_name STRING DEFAULT NULL, account_type STRING DEFAULT NULL, sourceid TEXT, version INTEGER NOT NULL DEFAULT 1, dirty INTEGER NOT NULL DEFAULT 0, deleted INTEGER NOT NULL DEFAULT 0, contact_id INTEGER REFERENCES contacts(_id), aggregation_mode INTEGER NOT NULL DEFAULT 0, aggregation_needed INTEGER NOT NULL DEFAULT 1, custom_ringtone TEXT send_to_voicemail INTEGER NOT NULL DEFAULT 0, times_contacted INTEGER NOT NULL DEFAULT 0, last_time_contacted INTEGER, starred INTEGER NOT NULL DEFAULT 0, display_name TEXT, display_name_alt TEXT, display_name_source INTEGER NOT NULL DEFAULT 0, phonetic_name TEXT, phonetic_name_style TEXT, sort_key TEXT COLLATE PHONEBOOK, sort_key_alt TEXT COLLATE PHONEBOOK, name_verified INTEGER NOT NULL DEFAULT 0, contact_in_visible_group INTEGER NOT NULL DEFAULT 0, sync1 TEXT, sync2 TEXT, sync3 TEXT, sync4 TEXT )

Najważniejsze pola zostały wyróżnione pogrubioną czcionką. Podobnie jak w przypadku pozostałych tabel systemu Android, znajdziemy tu kolumnę _ID, niepowtarzalnie definiującą nieprzetworzony kontakt. Pola account_name oraz account_type wspólnie definiują konto tego kontaktu (dokładniej nieprzetworzonego kontaktu). Pole sourceid wskazuje sposób określania nieprzetworzonego kontaktu w nadrzędnym koncie. Załóżmy na przykład, że chcemy dowiedzieć się, w jaki sposób jest zdefiniowany identyfikator nieprzetworzonego kontaktu wewnątrz konta pocztowego Google. W tym przypadku zazwyczaj pole to przechowuje identyfikator poczty e-mail użytkownika.

Rozdział 27 „ Analiza interfejsu kontaktów

967

Kolumna contact_id odnosi się do kontaktu zbiorczego, którego częścią jest dany nieprzetworzony kontakt. Kontakt zbiorczy wskazuje przynajmniej jeden kontakt (lub więcej), który w istocie prezentuje tę samą osobę wykrytą na różnych kontach. Pole display_name przechowuje wyświetlaną nazwę kontaktu. Jest to przede wszystkim pole tylko do odczytu. Jego wartość jest ustanawiana przez wyzwalacze na podstawie informacji umieszczanych w tabeli danych (która zostanie omówiona w następnym punkcie). Pola zawierające człon sync są wykorzystywane przez konto do synchronizowania kontaktów pomiędzy urządzeniem a kontem serwerowym, na przykład pocztą Gmail. Chociaż do analizy tych pól wykorzystywaliśmy narzędzia obsługujące silnik SQLite, istnieje więcej sposobów na ich przeglądanie. Zalecanym rozwiązaniem jest wykorzystanie definicji klas zadeklarowanych w interfejsie ContactsContract. Aby przebadać kolumny należące do tabeli nieprzetworzonych kontaktów, powinniśmy przejrzeć dokumentację klasy ContactsContract. ´RawContact. Rozwiązanie to posiada swoje zalety i wady. Istotną zaletą jest możliwość zapoznania się z polami, które są opublikowane i akceptowane przez zestaw Android SDK. Kolumny bazodanowe mogą być dodawane lub usuwane bez konieczności modyfikowania interfejsu publicznego. Zatem gdybyśmy chcieli bezpośrednio manipulować kolumnami bazodanowymi, możemy, ale nie musimy na nie natrafić. Z kolei korzystając z publicznych definicji tych kolumn, jesteśmy zawsze zabezpieczeni. Należy jednak wspomnieć, że dokumentacja omawianej klasy zawiera mnóstwo innych stałych pomieszanych z nazwami kolumn; nawet my w trakcie opracowywania książki pogubiliśmy się w ich ogromie. Ta olbrzymia liczba definicji klasy daje wrażenie złożoności interfejsu API, podczas gdy w rzeczywistości 80 procent jego dokumentacji omawia stałe tych kolumn oraz umożliwiające do nich dostęp identyfikatory URI. Gdy w następnych podrozdziałach będziemy ćwiczyć stosowanie interfejsu kontaktów, zaczniemy wykorzystywać stałe zdefiniowane w dokumentacji klasy zamiast bezpośrednich nazw kolumny. Mimo to uważamy, że bezpośrednia eksploracja tabel jest najszybszym sposobem pozwalającym na zrozumienie interfejsu kontaktów. Zastanówmy się teraz, w jaki sposób są przechowywane dane związane z kontaktem, na przykład adres e-mail lub numer telefonu.

Tabela danych Jak już wspomnieliśmy podczas omawiania definicji tabeli nieprzetworzonych kontaktów, taki kontakt (w tym rozczarowującym znaczeniu) stanowi wyłącznie identyfikator wskazujący, z jakim kontem jest związany. Większość informacji stanowiących zawartość kontaktu jest przechowywana nie w tabeli nieprzetworzonych kontaktów, lecz w tabeli danych. Każdy element danych, taki jak adres e-mail lub numer telefonu, posiada własne pole w tabeli danych. Wszystkie te wiersze zawierające dane są powiązane z nieprzetworzonym kontaktem za pomocą jego identyfikatora, który stanowi jedną z kolumn tabeli danych oraz jest głównym identyfikatorem w tabeli nieprzetworzonych kontaktów. Tabela danych składa się z szesnastu standardowych kolumn, w których może być przechowywanych szesnaście dowolnych punktów danych danego elementu, na przykład adresu e-mail. Na listingu 27.7 widzimy strukturę tabeli danych.

968 Android 3. Tworzenie aplikacji Listing 27.7. Definicja tabeli danych kontaktów CREATE TABLE data (_id INTEGER PRIMARY KEY AUTOINCREMENT, package_id INTEGER REFERENCES package(_id), mimetype_id INTEGER REFERENCES mimetype(_id) NOT NULL, raw_contact_id INTEGER REFERENCES raw_contacts(_id) NOT NULL, is_primary INTEGER NOT NULL DEFAULT 0, is_super_primary INTEGER NOT NULL DEFAULT 0, data_version INTEGER NOT NULL DEFAULT 0, data1 TEXT,data2 TEXT,data3 TEXT,data4 TEXT,data5 TEXT, data6 TEXT,data7 TEXT,data8 TEXT,data9 TEXT,data10 TEXT, data11 TEXT,data12 TEXT,data13 TEXT,data14 TEXT,data15 TEXT, data_sync1 TEXT, data_sync2 TEXT, data_sync3 TEXT, data_sync4 TEXT )

Najważniejsze kolumny tabeli kontaktów zostały wyróżnione pogrubioną czcionką. Tak jak mogliśmy się spodziewać, pole raw_contact_id stanowi odniesienie do nieprzetworzonego kontaktu, z którym jest związana dana informacja. Pole mimetype_id określa typ MIME danych wejściowych i wskazuje jeden z typów zdefiniowanych na listingu 27.4. Kolumny od data1 do data15 stanowią standardowe tablice przechowujące ciągi znaków, w których możemy umieszczać wszelkie niezbędne informacje, zgodne z danym typem MIME. Także tutaj pola typu sync obsługują synchronizację kontaktów. Tabela zawierająca informacje o typie MIME identyfikatorów została zaprezentowana na listingu 27.8. Listing 27.8. Definicja tabeli wyszukiwania typów MIME CREATE TABLE mimetypes (_id INTEGER PRIMARY KEY AUTOINCREMENT, mimetype TEXT NOT NULL)

Podobnie jak ma to miejsce w tabeli nieprzetworzonych danych, analiza kolumn tabeli danych jest możliwa dzięki dokumentacji pomocniczej klasy ContactsContract.Data. Chociaż możemy rozpoznawać kolumny za pomocą definicji tej klasy, nie zapoznamy się w ten sposób z zawartością pól od data1 do data15. W tym celu trzeba poznać definicje różnorodnych klas umieszczonych w przestrzeni nazw ContactsContract.CommonDataKinds. Poniżej prezentujemy przykłady niektórych zawartych w niej klas: „ ContactsContract.CommonDataKinds.Email, „ ContactsContract.CommonDataKinds.Phone. W istocie dla każdego typu danych wymienionego na listingu 27.4 istnieje jedna klasa ze wspomnianej przestrzeni nazw. W ostatecznym rozrachunku wszystkie elementy podrzędne klasy CommonDataKinds wskazują, które ze standardowych pól danych (data1 – data15) są wykorzystywane oraz w jakim celu.

Kontakty zbiorcze Ostatecznie dochodzimy do wniosku, że kontakt i związane z nim dane w sposób jednoznaczny są przechowywane w tabeli nieprzetworzonych kontaktów i tabeli danych. Z drugiej strony kontakt zbiorczy jest nieco bardziej heurystyczny i już nie tak bardzo jednoznaczny.

Rozdział 27 „ Analiza interfejsu kontaktów

969

Gdy natrafimy na kontakt, który jest taki sam dla różnych kont, chcielibyśmy, żeby jego nazwa pojawiała się na liście tylko raz. W tym celu Android umieszcza wszystkie podobne kontakty w widoku przeznaczonym tylko do odczytu. Takie zbiorcze kontakty są przechowywane w tabeli zwanej kontaktami. Aby zapełnić lub modyfikować taką tabelę kontaktów zbiorczych, Android wykorzystuje wiele wyzwalaczy wobec tabeli nieprzetworzonych kontaktów i tabeli danych. Zanim przejdziemy do omówienia mechanizmów zbierania kontaktów, spójrzmy najpierw na definicję tabeli kontaktów (listing 27.9). Listing 27.9. Definicja tabeli kontaktów zbiorczych CREATE TABLE contacts (_id INTEGER PRIMARY KEY AUTOINCREMENT, name_raw_contact_id INTEGER REFERENCES raw_contacts(_id), photo_id INTEGER REFERENCES data(_id), custom_ringtone TEXT, send_to_voicemail INTEGER NOT NULL DEFAULT 0, times_contacted INTEGER NOT NULL DEFAULT 0, last_time_contacted INTEGER, starred INTEGER NOT NULL DEFAULT 0, in_visible_group INTEGER NOT NULL DEFAULT 1, has_phone_number INTEGER NOT NULL DEFAULT 0, lookup TEXT, status_update_id INTEGER REFERENCES data(_id), single_is_restricted INTEGER NOT NULL DEFAULT 0)

Najważniejsze pola zostały oznaczone pogrubioną czcionką. Żaden klient nie aktualizuje bezpośrednio tej tabeli. Po dodaniu nieprzetworzonego kontaktu wraz z jego współistniejącymi danymi Android sprawdza pozostałe nieprzetworzone kontakty pod kątem podobieństwa. Po wykryciu powtarzającego się wpisu jego identyfikator stanie się również identyfikatorem nowego nieprzetworzonego kontaktu. Nie zostanie wprowadzony żaden nowy wpis do tabeli kontaktów zbiorczych. W przypadku braku obecności duplikatu zostanie utworzony nowy kontakt zbiorczy, a jego identyfikator będzie przypisany również do nowego nieprzetworzonego kontaktu. Do określenia, czy dwa nieprzetworzone kontakty są podobne, Android stosuje następujący algorytm: 1. Dwa nieprzetworzone kontakty posiadają takie same nazwy. 2. Wyrazy tworzące nazwy kontaktów są takie same, ułożone są jednak w innej kolejności: „pierwszy ostatni”, „pierwszy, ostatni” lub „ostatni, pierwszy”. 3. Rozpoznawane są zdrobnienia imion, na przykład „Robercik” jest zdrobnieniem imienia „Robert”. 4. Jeśli jeden z nieprzetworzonych kontaktów zawiera tylko dane o imieniu lub nazwisku, zostanie włączony mechanizm poszukiwania innych atrybutów, na przykład numeru telefonu lub adresu e-mail, i jeżeli będą one takie same, kontakty zostaną ze sobą powiązane. 5. Jeśli jeden z nieprzetworzonych kontaktów nie będzie zawierał imienia ani nazwiska, podobnie jak w punkcie 4., spowoduje to również włączenie procesu wyszukiwania innych atrybutów.

970 Android 3. Tworzenie aplikacji Ponieważ mamy tu do czynienia z metodami heurystycznymi, niektóre kontakty mogą zostać ze sobą powiązane przypadkowo. W takim wypadku aplikacje klienckie muszą zawierać mechanizm rozdzielający te kontakty. W czasie przeglądania instrukcji obsługi systemu Android dowiemy się, że domyślna aplikacja Kontakty umożliwia rozdzielanie przypadkowo połączonych kontaktów. Możemy również uniemożliwić agregację kontaktów poprzez ustanowienie odpowiedniego trybu agregacji podczas wstawiania nieprzetworzonego kontaktu. Listing 27.10 zawiera spis dostępnych trybów agregacji. Listing 27.10. Stałe trybu agregacji AGGREGATION_MODE_DEFAULT AGGREGATION_MODE_DISABLED AGGREGATION_MODE_SUSPENDED

Pierwsza opcja jest oczywista; jest to domyślny tryb agregacji. W drugim trybie (DISABLED) nieprzetworzony kontakt nie podlega procesowi agregacji. Nawet jeśli kontakt został już dołączony do tabeli kontaktów zbiorczych, zostanie z niej usunięty i otrzyma nowy identyfikator. Po wybraniu trzeciej opcji (SUSPENDED), nawet jeśli właściwości kontaktu ulegną zmianie, co jest równoznaczne z jego odrzuceniem z bieżącego zbioru kontaktów, będzie on ciągle powiązany z jego zbiorczym odpowiednikiem. Z powyższego akapitu wynika, że kontakt zbiorczy jest niestabilny. Załóżmy, że posiadamy unikatowy, nieprzetworzony kontakt, w którym umieszczone są imię i nazwisko. Jeśli dane te nie dublują się z danymi żadnego innego nieprzetworzonego kontaktu, zostanie przydzielony do oddzielnego kontaktu zbiorczego. Identyfikator takiego zbiorczego kontaktu będzie przechowywany w tabeli nieprzetworzonego kontaktu wraz z jego danymi. Załóżmy, że zmienimy nazwisko w tym nieprzetworzonym kontakcie w taki sposób, że zduplikuje on zestaw innych powiązanych ze sobą kontaktów. W takim przypadku nasz zmodyfikowany, nieprzetworzony kontakt zostanie odłączony od pierwotnego kontaktu zbiorczego i przeniesiony do innego obiektu tego typu. Identyfikator pierwotnego kontaktu zbiorczego zostanie natomiast całkowicie porzucony i nie będzie już więcej pasował do żadnego kontaktu, ponieważ w rzeczywistości będzie to sam identyfikator bez żadnych dodatkowych danych. Zatem kontakt zbiorczy jest niestabilny. Przechowywanie przez dłuższy czas identyfikatora takiego zbiorczego kontaktu nie ma wielkiego sensu. Android oferuje pewne wyjście z tej kłopotliwej sytuacji, mianowicie pozwala na stosowanie pola lookup w tabelach kontaktów zbiorczych. Takie pole wyszukiwania pozwala na agregację (konkatenację) konta z niepowtarzalnym identyfikatorem kontaktu w przypadku każdego nieprzetworzonego kontaktu. Informacja ta jest jeszcze bardziej kodyfikowana, dzięki czemu można ją wysłać w postaci adresu URL w celu odczytania identyfikatora najnowszego dołączonego kontaktu. Android wykorzystuje klucz wyszukiwania i znajduje wszelkie identyfikatory nieprzetworzonych kontaktów, które są przechowywane dla tego klucza. Następnie za pomocą algorytmu najlepszego dopasowania określa najodpowiedniejszy (lub być może nowy) identyfikator kontaktu zbiorczego. Skoro jawnie analizujemy bazę kontaktów, przyjrzyjmy się dwóm bazodanowym widokom, które mogą nam się przydać.

Rozdział 27 „ Analiza interfejsu kontaktów

971

view_contacts Pierwszym ze wspomnianych widoków jest view_contacts. Chociaż mamy do dyspozycji tabelę przechowującą zbiorcze kontakty (tabela kontaktów), interfejs API nie pozwala na uzyskanie bezpośredniego dostępu do tej tabeli. Zamiast tego do przeglądania kontaktów zbiorczych służy właśnie widok view_contacts. Gdy wysyłamy zapytanie oparte na identyfikatorze URI ContactsContract.Contacts.CONTENT_URI, otrzymamy kolumny, które bazują na widoku view_contacts. Definicja tego widoku została umieszczona na listingu 27.11. Listing 27.11. Widok umożliwiający odczytywanie kontaktów zbiorczych CREATE VIEW view_contacts AS SELECT contacts._id AS _id, contacts.custom_ringtone AS custom_ringtone, name_raw_contact.display_name_source AS display_name_source, name_raw_contact.display_name AS display_name, name_raw_contact.display_name_alt AS display_name_alt, name_raw_contact.phonetic_name AS phonetic_name, name_raw_contact.phonetic_name_style AS phonetic_name_style, name_raw_contact.sort_key AS sort_key, name_raw_contact.sort_key_alt AS sort_key_alt, name_raw_contact.contact_in_visible_group AS in_visible_group, has_phone_number, lookup, photo_id, contacts.last_time_contacted AS last_time_contacted, contacts.send_to_voicemail AS send_to_voicemail, contacts.starred AS starred, contacts.times_contacted AS times_contacted, status_update_id FROM contacts JOIN raw_contacts AS name_raw_contact ON(name_raw_contact_id=name_raw_contact._id)

Zwróćmy uwagę, że widok ten łączy tabelę kontaktów z tabelą nieprzetworzonych kontaktów, a wspólnym mianownikiem jest tu identyfikator zbiorczego kontaktu.

contact_entities_view Kolejny przydatny widok łączy tabelę nieprzetworzonych kontaktów z tabelą danych. Dzięki niemu możemy odczytywać jednocześnie wszystkie elementy danych określonego, nieprzetworzonego kontaktu, a nawet dane będące częścią wielu nieprzetworzonych kontaktów składających się na jeden kontakt zbiorczy. Na listingu 27.12 widzimy definicję widoku tych encji. Listing 27.12. Widok encji kontaktów CREATE VIEW contact_entities_view AS SELECT raw_contacts.account_name AS account_name, raw_contacts.account_type AS account_type, raw_contacts.sourceid AS sourceid, raw_contacts.version AS version, raw_contacts.dirty AS dirty,

972 Android 3. Tworzenie aplikacji raw_contacts.deleted AS deleted, raw_contacts.name_verified AS name_verified, package AS res_package, contact_id, raw_contacts.sync1 AS sync1, raw_contacts.sync2 AS sync2, raw_contacts.sync3 AS sync3, raw_contacts.sync4 AS sync4, mimetype, data1, data2, data3, data4, data5, data6, data7, data8, data9, data10, data11, data12, data13, data14, data15, data_sync1, data_sync2, data_sync3, data_sync4, raw_contacts._id AS _id, is_primary, is_super_primary, data_version, data._id AS data_id, raw_contacts.starred AS starred, raw_contacts.is_restricted AS is_restricted, groups.sourceid AS group_sourceid FROM raw_contacts LEFT OUTER JOIN data ON (data.raw_contact_id=raw_contacts._id) LEFT OUTER JOIN packages ON (data.package_id=packages._id) LEFT OUTER JOIN mimetypes ON (data.mimetype_id=mimetypes._id) LEFT OUTER JOIN groups ON (mimetypes.mimetype='vnd.android.cursor.item/group_membership' AND groups._id=data.data1)

Identyfikatory URI wymagane do uzyskania dostępu do tego widoku znajdziemy w klasie ContactsContract.RawContacts.RawContactsEntity.

Praca z interfejsem kontaktów Do tej pory omawialiśmy podstawowy mechanizm działania interfejsu kontaktów poprzez analizowanie jego tabel i widoków. Korzystając ze zdobytej wiedzy, utworzymy teraz kilka przykładowych programów. Chociaż możemy bez problemu posiłkować się kodami zawartymi na listingach, na końcu rozdziału umieściliśmy odnośnik do plików zawierających gotowe projekty.

Eksploracja kont Rozpoczniemy ćwiczenia od napisania programu wyświetlającego listę dostępnych kont. Do tego zadania będą nam potrzebne następujące pliki: „ TestContactsDriverActivity.java — główna aktywność sterująca, o której będziemy wspominać kilkakrotnie w dalszej części rozdziału. Aktywność ta zawiera zestaw elementów menu przywołujących poszczególne przykłady. „ DebugActivity.java — podstawowa klasa aktywności sterującej, ukrywająca kilka szczegółów implementacji, których znajomość nie jest wymagana do zrozumienia koncepcji interfejsu kontaktów.

Rozdział 27 „ Analiza interfejsu kontaktów

„

„

„

„

„ „

973

debug_activity_layout.xml — plik układu graficznego wymagany przez aktywność debugującą, przechowywany w podkatalogu /res/layout. AccountFunctionTester.java — klasa Java, która po kliknięciu elementu menu wyświetla (poprzez aktywność sterującą) listę dostępnych kont w emulatorze lub urządzeniu. BaseTester.java — bazowa klasa aplikacji AccountsFunctionTester, ukrywająca szczegóły koordynacji pomiędzy główną aktywnością sterującą a pozostałymi funkcjami testowymi (każdy z prezentowanych przykładów został zaimplementowany w postaci oddzielnej funkcji testowej, dzięki czemu kod odzwierciedlający każdą z omawianych koncepcji został umieszczony w osobnym pliku). IReportBack.java — interfejs implementowany przez klasę DebugActivity, przekazywany klasie BaseTester. Interfejs ten pozwala dziedziczonym funkcjom testowym na wyświetlanie raportów lub komunikatów debuggera na ekranie za pomocą aktywności DebugActivity. main_menu.xml — plik menu obsługujący wszystkie demonstrowane przez nas przykłady. AndroidManifest.xml — niezbędny plik manifest.

Zaprezentujemy teraz po kolei każdy z wymienionych plików. Rozpoczniemy od pliku menu.

Plik menu Plik menu z listingu 27.13 musi nosić nazwę main_menu.xml oraz zostać umieszczony w podkatalogu res/menu naszego projektu. Listing 27.13. Plik głównego menu naszego projektu

Na tym etapie umieszczamy w pliku tylko dwa elementy menu. W trakcie tworzenia dalszych przykładów będziemy wstawiać do niego kolejne obiekty. Pierwszy element menu pozwala na wyświetlanie listy dostępnych kont, drugą opcją jest natomiast przydatna funkcjonalność ogólnego przeznaczenia, służąca do usuwania komunikatów debugowania lub informacji pochodzących z testowej aktywności sterującej.

Pliki związane z funkcjami testującymi koncepcje kont Po umieszczeniu pliku menu we właściwym miejscu przyjrzyjmy się plikom związanym z implementacją kodu, które będą wywoływane w odpowiedzi na kliknięcie elementu menu Konta z listingu 27.13. IReportBack.java Pierwszym z tego typu plików jest IReportBack.java, zaprezentowany na listingu 27.14.

974 Android 3. Tworzenie aplikacji Listing 27.14. IReportBack.java //IReportBack.java public interface IReportBack { public void reportBack(String tag, String message); public void reportTransient(String tag, String message); }

Interfejs ten jest kontraktem dla dziedziczonych klientów, umożliwiającym im wysyłanie komunikatów informacyjnych oraz debugowania. Miejsce i sposób wyświetlania tych komunikatów nie należą do zadań klientów. BaseTester.java Wszystkie funkcje testowe będą posiadały dostęp do interfejsu IReportBack.java, dzięki czemu będą mogły generować komunikaty po ich wywołaniu za pomocą elementu menu. Gwarantuje nam to bazowa klasa dla wszystkich prezentowanych funkcji, zwana BaseTester. Jej kod źródłowy został zademonstrowany na listingu 27.15. Listing 27.15. Kod źródłowy klasy BaseTester public class BaseTester { protected IReportBack mReportTo; protected Context mContext; public BaseTester(Context ctx, IReportBack target) { mReportTo = target; mContext = ctx; } }

Klasa BaseTester przechowuje interfejs IReportBack oraz odniesienie do kontekstu (zazwyczaj jest nim nadrzędna klasa sterująca). Te dwie zmienne są wykorzystywane przez pochodne funkcje testowe. AccountsFunctionTester.java Zaprezentujemy teraz pierwszą z omawianych funkcji testowych, której kod umieszczono na listingu 27.16.

AccountsFunctionTester,

Listing 27.16. Klasa AccountsFunctionTester public class AccountsFunctionTester extends BaseTester { private static String tag = "tc>"; public AccountsFunctionTester(Context ctx, IReportBack target) { super(ctx, target); } public void testAccounts() {

Rozdział 27 „ Analiza interfejsu kontaktów

975

AccountManager am = AccountManager.get(this.mContext); Account[] accounts = am.getAccounts(); for(Account ac: accounts) { String acname=ac.name; String actype = ac.type; this.mReportTo.reportBack(tag,acname + ":" + actype); } } }

Kod widoczny na listingu 27.16 jest raczej nieskomplikowany. Na początku rozdziału omówiliśmy zagadnienie kont oraz mechanizm wyświetlania ich w postaci listy. Kod z listingu 27.16 pobiera jedynie nazwę oraz typ każdego konta, a następnie wywołuje interfejs raportujący, dzięki któremu zostaną wyświetlone wyniki. Dopóki istnieje aktywność sterująca, która może wywołać metodę testAccounts(), powyższy kod może przekazywać nazwę i typ konta. Zastanówmy się teraz nad klasami związanymi z aktywnością sterującą.

Klasy aktywności sterującej Zajmiemy się najpierw podstawową klasą aktywności sterującej. Aktywność ta wykonuje następujące zadania: „ Zapewnienie widoku tekstowego, w którym będą wyświetlane komunikaty. W tym celu będzie wykorzystywany układ graficzny debug_activity_layout. „ Wprowadzenie menu umożliwiającego wywoływanie poszczególnych funkcji testowych. Aktywność sterująca będzie przyjmowała wartości identyfikatora zasobu menu, otrzymywanego (poprzez konstruktor) z pochodnych klas. Zakładamy następnie, że istnieje predefiniowany element menu menu_da_clear, czyszczący zdefiniowany w pliku układu graficznego widok tekstowy. Ta klasa bazowa wyświetla również nazwę zaznaczonego elementu menu w polu tekstowym układu graficznego debuggera. Skoro już znamy przeznaczenie tej aktywności, możemy przyjrzeć się plikowi DebugActivity.java, zaprezentowanemu na listingu 27.17. DebugActivity.java Listing 27.17. Definicja klasy DebugActivity public abstract class DebugActivity extends Activity implements IReportBack {

//Najpierw spełniamy wymagania pochodnych klas protected abstract boolean onMenuItemSelected(MenuItem item);

//Zmienne prywatne, konfigurowane za pomocą konstruktora private static String tag=null; private int menuId = 0; public DebugActivity(int inMenuId, String inTag) { tag = inTag; menuId = inMenuId; }

976 Android 3. Tworzenie aplikacji

}

@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.debug_activity_layout); } @Override public boolean onCreateOptionsMenu(Menu menu){ super.onCreateOptionsMenu(menu); MenuInflater inflater = getMenuInflater(); inflater.inflate(menuId, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item){ appendMenuItemText(item); if (item.getItemId() == R.id.menu_da_clear){ this.emptyText(); return true; } return onMenuItemSelected(item); } private TextView getTextView(){ return (TextView)this.findViewById(R.id.text1); } protected void appendMenuItemText(MenuItem menuItem){ String title = menuItem.getTitle().toString(); TextView tv = getTextView(); tv.setText(tv.getText() + "\n" + title); } protected void emptyText(){ TextView tv = getTextView(); tv.setText(""); } private void appendText(String s){ TextView tv = getTextView(); tv.setText(tv.getText() + "\n" + s); Log.d(tag,s); } public void reportBack(String tag, String message) { this.appendText(tag + ":" + message); Log.d(tag,message); } public void reportTransient(String tag, String message) { String s = tag + ":" + message; Toast mToast = Toast.makeText(this, s, Toast.LENGTH_SHORT); mToast.show(); reportBack(tag,message); Log.d(tag,message); }

Oprócz metod umożliwiających wyświetlanie komunikatów w widoku tekstowym mamy tu do czynienia z metodą reportTransient() interfejsu IReportBack, który służy do wyświetlania informacji za pomocą kontrolki Toast.

Rozdział 27 „ Analiza interfejsu kontaktów

977

debug_layout_activity.xml Widoczny na listingu 27.18 plik debug_layout_activity.xml musi zostać umieszczony w podkatalogu /res/layout. Listing 27.18. Plik układu graficznego debuggera — debug_layout_activity.xml

TestContactsDriverActivity.java Listing 27.19 prezentuje główną aktywność sterującą, która koordynuje działanie poszczególnych elementów menu z wywoływaniem odpowiednich metod, umieszczonych w funkcjach testowych. Listing 27.19. Główna aktywność sterująca public class TestContactsDriverActivity extends DebugActivity implements IReportBack { public static final String tag="Test kontaktów"; AccountsFunctionTester accountsFunctionTester = null; public TestContactsDriverActivity() { super(R.menu.main_menu,tag); accountsFunctionTester = new AccountsFunctionTester(this,this); } protected boolean onMenuItemSelected(MenuItem item) { Log.d(tag,item.getTitle().toString()); if (item.getItemId() == R.id.menu_show_accounts) { accountsFunctionTester.testAccounts(); return true; } return true; } }

978 Android 3. Tworzenie aplikacji Kod tej aktywności sterującej jest czytelny i przejrzysty, ponieważ umieściliśmy większość jej funkcji w klasie bazowej. Pierwszą sprawą, którą należy zauważyć na listingu 27.19, jest sposób przekazywania zasobu menu zdefiniowanego na listingu 27.13 (main_menu.xml) do bazowej aktywności debuggera. Aktywność ta łączy następnie menu w całość. Drugim mechanizmem wartym zaobserwowania jest sposób wykorzystywania funkcji testowych przez aktywność sterującą. W kodzie z listingu 27.19 zademonstrowaliśmy jedynie funkcję testową dla kont. Wraz z postępami prac nad projektem będziemy dodawać kolejne funkcje testowe. Sposób ich stosowania jest zawsze taki sam. Plik manifest Na listingu 27.20 zamieściliśmy zawartość pliku manifestu i tym samym kończymy prezentowanie wszystkich niezbędnych plików. Listing 27.20. Plik manifest przykładowego programu

Uruchomienie programu Listing 27.21 zawiera spis plików wymaganych do skompilowania i uruchomienia naszego prostego przykładu. Listing 27.21. Kompletny spis plików tworzących pierwszą aplikację testową IReportBack.java BaseTester.java AccountsFunctionTester.java DebugActivity.java TestContactsDriverActivity.java /res/menu/main_menu.xml /res/layout/debug_layout_activity.xml AndroidManifest.xml

Rozdział 27 „ Analiza interfejsu kontaktów

979

Jeśli po skompilowaniu i uruchomieniu kodu zawartego w tych plikach klikniemy element menu znajdujący się w głównej aktywności sterującej, zobaczymy ekran zilustrowany na rysunku 27.15.

Rysunek 27.15. Główna aktywność sterująca z dołączonym menu

Menu pokazane na rysunku 27.15 zawiera dwie opcje. Opcja wyczyść stanowi standardowy element menu, zdefiniowany w bazowej klasie aktywności debugowania, za pomocą którego wyzerujemy zawartość widoku tekstowego. Opcja Konta spowoduje wyświetlenie listy kont dostępnych w urządzeniu. Przekonajmy się, co się stanie po jego kliknięciu. Pojawi się ekran widoczny na rysunku 27.16.

Rysunek 27.16. Główna aktywność sterująca, prezentująca spis dostępnych kontaktów

980 Android 3. Tworzenie aplikacji Testowana przez nas wersja emulatora zawierała tylko jedną konfigurację konta — firmy Google, dlatego jego nazwa została wyświetlona przez naszą aplikację.

Badanie kontaktów zbiorczych W następnym przykładowym programie zaprezentujemy rozwiązanie pozwalające na badanie kontaktów zbiorczych. Zademonstrujemy w nim trzy kwestie dotyczące tego rodzaju kontaktów: „ Pokażemy, jak przejrzeć wszystkie wypełnione pola poprzez wykorzystanie identyfikatora URI, dzięki któremu można odczytać zawartość kontaktów zbiorczych. „ Wyświetlimy spis wszystkich zbiorczych kontaktów. „ Przedstawimy wszystkie pola przekazywane przez kursor na podstawie identyfikatora URI wyszukiwania. W celu odczytywania kontaktów musimy umieścić następujące uprawnienie w pliku manifeście (jego kod pokazano na listingu 27.20): android.permission.READ_CONTACTS

Do przetestowania tego przykładu wymagane też będą następujące nowe pliki (obok plików utworzonych wcześniej): „ Utils.java, „ URIFunctionTester.java, „ AggregatedContactFunctionTester.java, „ AggregatedContact.java. Każdy z tych plików zostanie omówiony w dalszej części rozdziału. Musimy także zmodyfikować następujące pliki, będące częścią poprzedniego przykładu: „ main_menu.xml, „ TestContactsDriverActivity.java. Nieco dalej w tym podrozdziale wskażemy, jakie zmiany należy wprowadzić do wymienionych plików. Ponieważ w omawianej przez nas funkcji pojawiają się dostawcy treści, identyfikatory URI oraz kursory, zebraliśmy kilka metod użytkowych w pliku Utils.java, widocznym na listingu 27.22. Listing 27.22. Funkcje użytkowe pozwalające na pracę z kursorami public class Utils { public static String getColumnValue(Cursor cc, String cname) { int i = cc.getColumnIndex(cname); return cc.getString(i); } protected static String getCursorColumnNames(Cursor c) { int count = c.getColumnCount(); StringBuffer cnamesBuffer = new StringBuffer();

Rozdział 27 „ Analiza interfejsu kontaktów

981

for (int i=0;i


Pierwsza funkcja, getColumnValue(), powraca z wartością kolumny, pobierając jej nazwę z bieżącego wiersza kursora. Bez względu na podstawowy typ danych kolumny funkcja ta przekazuje tę wartość w postaci ciągu znaków. Druga funkcja jest bardzo przydatna. Pobiera ona dowolny kursor i przekazuje osobną listę wszystkich jego kolumn. Jest to użyteczne zwłaszcza w przypadku badania nowych identyfikatorów URI pod kątem rodzajów pól, które określają. Chociaż można udokumentować te kolumny w kodzie Java, wspomniana metoda ich odkrywania w czasie działania aplikacji może się przydać w pewnych sytuacjach. Ponieważ ten i następne przykłady wykorzystują koncepcję wysyłania identyfikatora URI i odbierania kursora za pomocą aktywności, umieściliśmy funkcje realizujące te zadania w bazowej klasie URIFunctionTester. Na listingu 27.23 zamieściliśmy kod źródłowy tej klasy, po czym opisaliśmy każdą dostępną w niej metodę. Listing 27.23. Klasa bazowa, umożliwiająca analizowanie funkcji związanych z identyfikatorami URI public class URIFunctionTester extends BaseTester { protected static String tag = "tc>"; public URIFunctionTester(Context ctx, IReportBack target) { super(ctx, target); } protected Cursor getACursor(String uri,String clause) {

// Uruchamia kwerendę Activity a = (Activity)this.mContext; return a.managedQuery(Uri.parse(uri), null, clause, null, null); } protected Cursor getACursor(Uri uri,String clause) {

// Uruchamia kwerendę Activity a = (Activity)this.mContext; return a.managedQuery(uri, null, clause, null, null); } protected void printCursorColumnNames(Cursor c) { this.mReportTo.reportBack(tag,Utils.getCursorColumnNames(c)); } }

982 Android 3. Tworzenie aplikacji Funkcja getACursor() pobiera identyfikator URI albo w postaci ciągu znaków, albo jako obiekt identyfikatora URI wraz z opartą na ciągu znaków klauzulą where, a następnie przekazuje kursor. W omawianych przykładach często wyświetlamy nazwy kolumn pochodzących z otrzymywanego kursora, utworzyliśmy więc metodę printCursorColumnNames(), która z kolei wykorzystuje klasę Utils do analizowania zawartości kursora i uzyskiwania nazw jego kolumn. Każdy wiersz przekazywany przez kursor kontaktu będzie posiadał pewną liczbę pól. W naszym przykładzie interesują nas tylko niektóre z nich. Wyraziliśmy tę koncepcję w kolejnej klasie, nazwanej AggregatedContact, która została ukazana na listingu 27.24. Listing 27.24. Kilka pól pochodzących z kontaktu zbiorczego public class AggregatedContact { public String id; public String lookupUri; public String lookupKey; public String displayName; public void fillinFrom(Cursor c) { id = Utils.getColumnValue(c,"_ID"); lookupKey = Utils.getColumnValue(c,ContactsContract.Contacts.LOOKUP_KEY); lookupUri = ContactsContract.Contacts.CONTENT_LOOKUP_URI + "/" + lookupKey; displayName = Utils.getColumnValue(c,ContactsContract.Contacts.DISPLAY_NAME); } }

Kod z listingu 27.24 wcale nie jest skomplikowany. Wykorzystaliśmy tu kursor do wczytania interesujących nas pól. Zaprezentujemy teraz na listingu 27.25 klasę AggregatedContactFunction ´Tester, która pomoże nam wypełnić zadania ustalone na początku tego podrozdziału. Listing 27.25. Kod umożliwiający testowanie kontaktów zbiorczych public class AggregatedContactFunctionTester extends URIFunctionTester { public AggregatedContactFunctionTester(Context ctx, IReportBack target) { super(ctx, target); }

/* * Pobiera kursor ze wszystkich kontaktów * Bez klauzuli where * Nie stosujmy tego w przypadku dużego zbioru */ private Cursor getContacts() {

// Uruchamia kwerendę Uri uri = ContactsContract.Contacts.CONTENT_URI; String sortOrder = ContactsContract.Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; Activity a = (Activity)this.mContext; return a.managedQuery(uri, null, null, null, sortOrder);

Rozdział 27 „ Analiza interfejsu kontaktów

}

/* * Wykorzystuje powyższą metodę getContacts * do utworzenia spisu kolumn zawartych w kursorze */ public void listContactCursorFields() { Cursor c = null; try { c = getContacts(); int i = c.getColumnCount(); this.mReportTo.reportBack(tag, "Liczba kolumn:" + i); this.printCursorColumnNames(c); } finally { if (c!= null) c.close(); } }

/* * Przy użyciu kursora wypełnionego kontaktami * zostają wyświetlone nazwy kontaktów * wraz z ich kluczami wyszukiwania */ private void printLookupKeys(Cursor c) { for(c.moveToFirst();!c.isAfterLast();c.moveToNext()) { String name=this.getContactName(c); String lookupKey = this.getLookupKey(c); String luri = this.getLookupUri(lookupKey); this.mReportTo.reportBack(tag, name + ":" + lookupKey); this.mReportTo.reportBack(tag, name + ":" + luri); } }

/* * Wykorzystuje funkcję getContacts() * do uzyskania kursora i wyświetlenia wszystkich * nazw kontaktów wraz z kluczami ich wyszukiwania * Stosuje funkcję printLookupKeys() */ public void listContacts() { Cursor c = null; try { c = getContacts(); int i = c.getColumnCount(); this.mReportTo.reportBack(tag, "Liczba kolumn:" + i); this.printLookupKeys(c);

983

984 Android 3. Tworzenie aplikacji } finally { if (c!= null) c.close(); } }

/* * Funkcja użytkowa odczytująca * klucz wyszukiwania z kursora kontaktu */ private String getLookupKey(Cursor cc) { int lookupkeyIndex = cc.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY); return cc.getString(lookupkeyIndex); }

/* * Funkcja użytkowa odczytująca * wyświetlaną nazwę z kursora kontaktu */ private String getContactName(Cursor cc) { return Utils.getColumnValue(cc,ContactsContract.Contacts.DISPLAY_NAME); }

/** * Konstruuje identyfikator wyszukiwania na podstawie * identyfikatora URI kontaktów i klucza wyszukiwania */ private String getLookupUri(String lookupkey) { String luri = ContactsContract.Contacts.CONTENT_LOOKUP_URI + "/" + lookupkey; return luri; }

/** * Wykorzystuje identyfikator URI wyszukiwania * do odczytania pojedynczego kontaktu zbiorczego */ private Cursor getASingleContact(String lookupUri) {

// Uruchamia kwerendę Activity a = (Activity)this.mContext; return a.managedQuery(Uri.parse(lookupUri), null, null, null, null); }

/* * Funkcja sprawdzająca, czy identyfikator URI stworzony za pomocą identyfikatora * URI wyszukiwania zwraca kursor zawierający inny zestaw kolumn. * Jak można było się spodziewać, zwracany jest podobny kursor * zawierający podobny zbiór kolumn. */

Rozdział 27 „ Analiza interfejsu kontaktów

public void listLookupUriColumns() { Cursor c = null; try { c = getContacts(); String firstContactLookupUri = getFirstLookupUri(c); printLookupUriColumns(firstContactLookupUri); } finally { if (c!= null) c.close(); } } public void printLookupUriColumns(String lookupuri) { Cursor c = null; try { c = getASingleContact(lookupuri); int i = c.getColumnCount(); this.mReportTo.reportBack(tag, "Liczba kolumn:" + i); int j = c.getCount(); this.mReportTo.reportBack(tag, "Liczba wierszy:" + j); this.printCursorColumnNames(c); } finally { if (c!=null)c.close(); } }

/* * Pobiera listę kontaktów * Wyszukuje pierwszy kontakt * Przekazuje wartość null, jeśli nie znajdzie żadnego kontaktu */ private String getFirstLookupUri(Cursor c) { c.moveToFirst(); if (c.isAfterLast()) { Log.d(tag,"Brak wierszy, z ktorych mozna pobrac pierwszy kontakt"); return null; }

//Znaleziono wiersz String lookupKey = this.getLookupKey(c); String luri = this.getLookupUri(lookupKey); return luri; }

/* * Pobiera listę kontaktów * Wyszukuje pierwszy kontakt i zwraca go

985

986 Android 3. Tworzenie aplikacji * w formie obiektu AggregatedContact */ protected AggregatedContact getFirstContact() { Cursor c=null; try { c = getContacts(); c.moveToFirst(); if (c.isAfterLast()) { Log.d(tag,"Brak kontaktow"); return null; }

//Znaleziono kontakt AggregatedContact firstcontact = new AggregatedContact(); firstcontact.fillinFrom(c); return firstcontact; } finally { if (c!=null) c.close(); } } }

Główne funkcje publiczne zostały wyróżnione pogrubioną czcionką. Przeznaczenie każdej z nich zostało wyjaśnione w przypisanym do niej komentarzu. Po utworzeniu tej funkcji testowej dodajmy elementy menu z listingu 27.26 do pliku menu (/res/menu/main_menu.xml). Listing 27.26. Elementy menu związane z funkcją testową kontaktów zbiorczych

Możemy wstawić je w dowolnym miejscu pliku main_menu.xml, proponujemy jednak umieszczenie ich w początkowej części kodu, dzięki czemu nowsze funkcje będą wyświetlane na początku menu. Po dodaniu opcji menu modyfikujemy aktywność sterującą w taki sposób, żeby przypominała kod z listingu 27.27. Listing 27.27. Główna aktywność sterująca dostosowana do testowania kontaktów zbiorczych public class TestContactsDriverActivity extends DebugActivity implements IReportBack { public static final String tag="TestContactsDriverActivity "; AccountsFunctionTester accountsFunctionTester = null;

Rozdział 27 „ Analiza interfejsu kontaktów

987

AggregatedContactFunctionTester aggregatedContactFunctionTester = null; public TestContactsDriverActivity() { super(R.menu.main_menu,tag); accountsFunctionTester = new AccountsFunctionTester(this,this); aggregatedContactFunctionTester = new AggregatedContactFunctionTester(this,this); } protected boolean onMenuItemSelected(MenuItem item) { Log.d(tag,item.getTitle().toString()); if (item.getItemId() == R.id.menu_show_accounts) { accountsFunctionTester.testAccounts(); return true; } if (item.getItemId() == R.id.menu_show_contact_cursor) { aggregatedContactFunctionTester.listContactCursorFields(); return true; } if (item.getItemId() == R.id.menu_show_contacts) { aggregatedContactFunctionTester.listContacts(); return true; } if (item.getItemId() == R.id.menu_show_single_contact_cursor) { aggregatedContactFunctionTester.listLookupUriColumns(); return true; } return true; } }

Zwróćmy uwagę na trzy publiczne funkcje, które są wywoływane w wyniku wciśnięcia odpowiednich opcji menu: „ listContactCursorFields(), „ listContacts(), „ listLookupUriColumns(). Omówimy działanie tych funkcji, opierając się na kodzie umieszczonym na listingu 27.26. Funkcja listContactCursorFields odczytuje całą listę kontaktów i wyświetla w kursorze nazwy kolumn. Identyfikatorem URI służącym do odczytywania wszystkich kontaktów jest ContactsContract.Contacts.CONTENT_URI. W celu odczytania kursora przekazujemy ten identyfikator URI metodzie managedQuery(). Możemy przekazać wartość null w trakcie rzutowania kolumn, aby wyświetlić wszystkie kolumny. Chociaż nie jest to zalecane rozwiązanie, w naszym przypadku jest ono logiczne, gdyż chcemy poznać wszystkie kolumny przekazane przez identyfikator URI. Na listingu 27.28 widzimy spis wszystkich kolumn otrzymywanych dzięki temu identyfikatorowi.

988 Android 3. Tworzenie aplikacji Listing 27.28. Kolumny kursora przekazywane przez identyfikator URI dostawcy kontaktów times_contacted; contact_status; custom_ringtone; has_phone_number; phonetic_name; phonetic_name_style; contact_status_label; lookup; contact_status_icon; last_time_contacted; display_name; sort_key_alt; in_visible_group; _id; starred; sort_key; display_name_alt; contact_presence; display_name_source; contact_status_res_package; contact_status_ts; photo_id; send_to_voicemail;

Nasz przykładowy program wygeneruje listę tych kolumn zarówno w widoku programu, jak również w oknie LogCat. Skopiowaliśmy te pola z okna LogCat i sformatowaliśmy w sposób widoczny na listingu 27.28. Podczas pracy z dostawcami treści technika polegająca na korzystaniu z identyfikatora URI oraz wyświetlaniu przekazywanych kolumn może się okazać bardzo przydatna.

Po zapoznaniu się ze spisem kolumn za pomocą identyfikatora URI treści kontaktów zaznaczmy kilka z nich i sprawdźmy, jakie wiersze są dostępne. Kliknijmy w tym celu opcję menu kontakty, co spowoduje wywołanie funkcji listContacts(). Korzysta ona z tego samego identyfikatora URI, tym razem jednak wyświetla dla każdego kontaktu następujące kolumny: „ display name, „ lookup key, „ lookup uri. Bierzemy te pola pod uwagę, ponieważ chcemy zobaczyć, jak wyglądają klucz wyszukiwania oraz identyfikator klucza wyszukiwania w porównaniu do informacji zawartych w części teoretycznej tego rozdziału. Interesuje nas zwłaszcza mechanizm uruchamiania identyfikatora URI wyszukiwania oraz typ otrzymywanego kursora. Kliknijmy w tym celu element menu kursor pojedynczego kontaktu. Zostanie wywołana funkcja listLookupUriColumns(). Pobierze ona pierwszy kontakt z listy kontaktów, a następnie wygeneruje identyfikator URI wyszukiwania dla tego kontaktu, po czym z niego skorzysta, a my poznamy wyniki. Okazuje się, że wspomniana funkcja przekaże kursor zawierający takie same kolumny jak widoczne na listingu 27.28 — jedyna różnica polega na obecności tylko jednego wiersza

Rozdział 27 „ Analiza interfejsu kontaktów

989

wskazującego kontakt, którego dotyczy ten klucz wyszukiwania. Zauważmy również, że wprowadziliśmy następującą definicję identyfikatora URI wyszukiwania: ContactsContract.Contacts.CONTENT_LOOKUP_URI

Podczas dyskusji na temat identyfikatorów URI wyszukiwania stwierdziliśmy, że każdy tego typu obiekt symbolizuje zbiór połączonych ze sobą, nieprzetworzonych kontaktów. W takim przypadku powinniśmy się spodziewać, że otrzymamy zestaw takich samych nieprzetworzonych kontaktów. Powyższy test (listing 27.28) udowadnia nam jednak, że nie dostajemy kursora zawierającego nieprzetworzone kontakty, lecz kursor przechowujący kontakty zbiorcze. W efekcie wyszukiwania opartego na identyfikatorze wyszukiwania kontaktu otrzymujemy kontakt zbiorczy, a nie kontakt nieprzetworzony.

Kolejną istotną i ciekawą cechą procesu wyszukiwania kontaktu zbiorczego opartego na identyfikatorze wyszukiwania jest to, że nie zachodzi w sposób liniowy i że nie jest dokładny. Oznacza to, że system nie będzie poszukiwał trafienia dokładnie odpowiadającego kluczowi dopasowania. Zamiast tego Android analizuje składnię klucza wyszukiwania pod kątem tworzących go nieprzetworzonych kontaktów, następnie odnajduje identyfikator kontaktu zbiorczego, który jest zgodny z większością rekordów nieprzetworzonych kontaktów, i przekazuje rekord kontaktu zbiorczego utworzonego w ten sposób. Jedną z konsekwencji tego mechanizmu jest brak możliwości publicznego przejścia od klucza wyszukiwania do nieprzetworzonych kontaktów, które stanowią jego składowe. Jedynym rozwiązaniem jest odnalezienie identyfikatora kontaktu związanego z kluczem wyszukiwania, a następnie uruchomienie obiektu URI nieprzetworzonego kontaktu wobec identyfikatora znalezionego kontaktu, dzięki czemu zostaną odczytane jego nieprzetworzone kontakty.

Badanie nieprzetworzonych kontaktów W następnym przykładowym programie przedstawimy rozwiązanie pozwalające na eksplorację nieprzetworzonych kontaktów. W tym ćwiczeniu postaramy się wykonać trzy zadania: „ Odkryć wszystkie przekazywane pola poprzez uruchomienie identyfikatora URI odczytującego nieprzetworzone kontakty. „ Wyświetlić wszystkie nieprzetworzone kontakty. „ Wygenerować listę nieprzetworzonych kontaktów tworzących kontakt zbiorczy. W tym przykładzie będą potrzebne następujące nowe pliki: „ RawContact.java, „ RawContactFunctionTester.java. Pliki te zostaną zaprezentowane w trakcie omawiania szczegółów przykładu. Będziemy musieli zaktualizować również następujące pliki pochodzące z poprzedniego przykładu: „ main_menu.xml, „ TestContactsDriverActivity.java. W dalszej części podrozdziału zaprezentujemy również zmiany, jakie należy wprowadzić w powyższych plikach. Plik z listingu 27.29, RawContact.java, pobiera kilka istotnych pól z tabeli nieprzetworzonych kontaktów.

990 Android 3. Tworzenie aplikacji Listing 27.29. Plik RawContact.java public class RawContact { public String rawContactId; public String aggregatedContactId; public String accountName; public String accountType; public String displayName; public void fillinFrom(Cursor c) { rawContactId = Utils.getColumnValue(c,"_ID"); accountName = Utils.getColumnValue(c,ContactsContract.RawContacts.ACCOUNT_NAME); accountType = Utils.getColumnValue(c,ContactsContract.RawContacts.ACCOUNT_TYPE); aggregatedContactId = Utils.getColumnValue(c, ContactsContract.RawContacts.CONTACT_ID); displayName = Utils.getColumnValue(c,"display_name"); } public String toString() { return displayName + "/" + accountName + ":" + accountType + "/" + rawContactId + "/" + aggregatedContactId; } }

Aby móc przetestować zawarte w tym przykładzie funkcje, musimy do pliku main_menu.xml dodać widoczne na listingu 27.30 elementy menu. Listing 27.30. Opcje menu umożliwiające przetestowanie nieprzetworzonych kontaktów

Każdy z tych elementów menu powoduje wywołanie trzech funkcji umieszczonych w pliku RawContactFunctionTester.java. Zawarty w tym pliku kod jest widoczny na listingu 27.31. Listing 27.31. Testowanie nieprzetworzonych kontaktów public class RawContactsFunctionTester extends AggregatedContactFunctionTester { public RawContactsFunctionTester(Context ctx, IReportBack target) { super(ctx, target); }

Rozdział 27 „ Analiza interfejsu kontaktów

public void showAllRawContacts() { Cursor c = null; try { c = this.getACursor(getRawContactsUri(), null); this.printRawContacts(c); } finally { if (c!=null) c.close(); } } public void showRawContactsForFirstAggregatedContact() { AggregatedContact ac = getFirstContact(); this.mReportTo.reportBack(tag, ac.displayName + ":" + ac.id); Cursor c = null; try { c = this.getACursor(getRawContactsUri(), getClause(ac.id)); this.printRawContacts(c); } finally { if (c!=null) c.close(); } } private void printRawContacts(Cursor c) { for(c.moveToFirst();!c.isAfterLast();c.moveToNext()) { RawContact rc = new RawContact(); rc.fillinFrom(c); this.mReportTo.reportBack(tag, rc.toString()); } } public void showRawContactsCursor() { AggregatedContact ac = getFirstContact(); this.mReportTo.reportBack(tag, ac.displayName + ":" + ac.id); Cursor c = null; try { c = this.getACursor(getRawContactsUri(),null); this.printCursorColumnNames(c); } finally { if (c!=null) c.close(); } }

991

992 Android 3. Tworzenie aplikacji private Uri getRawContactsUri() { return ContactsContract.RawContacts.CONTENT_URI; } private String getClause(String contactId) { return "contact_id = " + contactId; } }

Na listingu 27.32 znalazł się zaktualizowany kod aktywności sterującej, przejmującej od elementów menu proces wywoływania funkcji publicznych pochodzących z testera funkcji nieprzetworzonych kontaktów. Listing 27.32. Zaktualizowana aktywność sterująca, pozwalająca na testowanie nieprzetworzonych kontaktów public class TestContactsDriverActivity extends DebugActivity implements IReportBack {

//........kontynuacja RawContactsFunctionTester rawContactFunctionTester = null; public TestContactsDriverActivity() {

//........kontynuacja rawContactFunctionTester = new RawContactsFunctionTester(this,this); } protected boolean onMenuItemSelected(MenuItem item) {

//........kontynuacja if (item.getItemId() == R.id.menu_show_single_contact_cursor) { aggregatedContactFunctionTester.listLookupUriColumns(); return true; }

//początek nowych wpisów if (item.getItemId() == R.id.menu_show_rc_cursor) { rawContactFunctionTester.showRawContactsCursor(); return true; } if (item.getItemId() == R.id.menu_show_rc_all) { rawContactFunctionTester.showAllRawContacts(); return true; } if (item.getItemId() == R.id.menu_show_rc) { rawContactFunctionTester.showRawContactsForFirstAggregatedContact(); return true; }

//koniec nowych wpisów

Rozdział 27 „ Analiza interfejsu kontaktów

993

return true; } }

Na powyższym listingu zaprezentowaliśmy jedynie nowe wiersze, które należy wstawić do aktywności sterującej, gdyż jest to jeden z aktualizowanych plików. Podobnie jak zrobiliśmy w przypadku kontaktów zbiorczych, przyjrzyjmy się najpierw naturze identyfikatorów URI nieprzetworzonych kontaktów oraz przekazywanym przez nie wartościom. Sygnatura identyfikatora nieprzetworzonego kontaktu wygląda następująco: ContactsContract.RawContacts.CONTENT_URI;

Jeżeli przyjrzymy się kodowi metody showRawContactsCursor(), zauważymy, że jest w nim wykorzystywany powyższy identyfikator nieprzetworzonego kontaktu, dzięki czemu zostają wyświetlone pola kursora. Kliknijmy obiekt menu kursor nieprzetworzonych kontaktów. Zobaczymy, że kursor nieprzetworzonego kontaktu zawiera pola wypisane na listingu 27.33. Listing 27.33. Pola kursora nieprzetworzonego kontaktu times_contacted; phonetic_name; phonetic_name_style; contact_id;version; last_time_contacted; aggregation_mode; _id; name_verified; display_name_source; dirty; send_to_voicemail; account_type; custom_ringtone; sync4;sync3;sync2;sync1; deleted; account_name; display_name; sort_key_alt; starred; sort_key; display_name_alt; sourceid;

Skoro zapoznaliśmy się z kolumnami kursora nieprzetworzonych kontaktów, mogą nas zainteresować również wiersze tej tabeli. Kliknijmy teraz opcję menu wszystkie nieprzetworzone kontakty. Zostanie wywołana metoda showAllRawContacts(). Metoda ta będzie manipulowała kursorem bez użycia klauzuli WHERE (dzięki czemu uzyskamy dostęp do wszystkich wierszy) oraz utworzy obiekt RawContact dla każdego wiersza, po czym wyświetli wyniki. Lista nieprzetworzonych kontaktów zostanie ukazana w widoku aplikacji oraz w oknie LogCat. Za pomocą widocznych na listingu 27.33 kolumn kursora sprawdźmy, czy możemy zmodyfikować kwerendę w taki sposób, aby odczytywać kontakty za pomocą danego identyfikatora kontaktów zbiorczych. Przetestujemy taką możliwość, klikając element menu nieprzetworzone

994 Android 3. Tworzenie aplikacji kontakty. Zostanie najpierw wyszukany pierwszy kontakt zbiorczy, a następnie wysłany identyfikator nieprzetworzonego kontaktu wraz z klauzulą WHERE, definiującą wartość kolumny contact_id. Wyniki będą widoczne zarówno w interfejsie użytkownika, jak i w dzienniku LogCat. Chociaż przejrzeliśmy już kontakty zbiorcze i nieprzetworzone kontakty, tak naprawdę nie odczytaliśmy jeszcze zawartości najważniejszych ich pól, na przykład adresu e-mail lub numeru telefonu. W następnym punkcie powiemy, jak można tego dokonać.

Przeglądanie danych nieprzetworzonego kontaktu W kolejnym programie ukażemy sposób przeglądania danych powiązanych z nieprzetworzonymi kontaktami. Spróbujemy teraz wykonać dwa zadania: „ Wykryć wszystkie przekazywane pola poprzez uruchomienie identyfikatora odczytującego dane nieprzetworzonych kontaktów. „ Odczytać dane przechowywane w zestawie kontaktów zbiorczych. W tej przykładowej aplikacji pojawiają się następujące nowe pliki: „ ContactData.java, „ ContactDataFunctionTester.java. Zawartość tych plików zostanie ukazana w trakcie omawiania tej aplikacji. Wymagana będzie również modyfikacja dwóch plików z poprzedniego przykładu: „ main_menu.xml, „ TestContactsDriverActivity.java. Zmiany, które trzeba wprowadzić w tych plikach, zostaną zaprezentowane w dalszej części rozdziału. Kod zawarty w pliku ContactData.java służy do pobrania reprezentacyjnego zestawu danych kontaktu. Na listingu 27.34 znajdziemy kod źródłowy tego pliku. Listing 27.34. Plik ContactData.java public class ContactData { public String rawContactId; public String aggregatedContactId; public String dataId; public String accountName; public String accountType; public String mimetype; public String data1; public void fillinFrom(Cursor c) { rawContactId = Utils.getColumnValue(c,"_ID"); accountName = Utils.getColumnValue(c,ContactsContract.RawContacts.ACCOUNT_NAME); accountType = Utils.getColumnValue(c,ContactsContract.RawContacts.ACCOUNT_TYPE); aggregatedContactId = Utils.getColumnValue(c,ContactsContract.RawContacts.CONTACT_ID); mimetype = Utils.getColumnValue(c,ContactsContract.RawContactsEntity.MIMETYPE); data1 = Utils.getColumnValue(c,ContactsContract.RawContactsEntity.DATA1); dataId = Utils.getColumnValue(c,ContactsContract.RawContactsEntity.DATA_ID); }

Rozdział 27 „ Analiza interfejsu kontaktów

995

public String toString() { return data1 + "/" + mimetype + "/" + accountName + ":" + accountType + "/" + dataId + "/" + rawContactId + "/" + aggregatedContactId; } }

Sposób działania tego przykładu został zdefiniowany w pliku ContactFunctionTester.java. Jego kod jest widoczny na listingu 27.35. Listing 27.35. Testowanie danych kontaktu public class ContactDataFunctionTester extends RawContactFunctionTester { public ContactDataFunctionTester(Context ctx, IReportBack target) { super(ctx, target); } public void showRawContactsEntityCursor() { Cursor c = null; try { Uri uri = ContactsContract.RawContactsEntity.CONTENT_URI; c = this.getACursor(uri,null); this.printCursorColumnNames(c); } finally { if (c!=null) c.close(); } } public void showRawContactsData() { Cursor c = null; try { Uri uri = ContactsContract.RawContactsEntity.CONTENT_URI; c = this.getACursor(uri,"contact_id w (3,4,5)"); this.printRawContactsData(c); } finally { if (c!=null) c.close(); } } protected void printRawContactsData(Cursor c) { for(c.moveToFirst();!c.isAfterLast();c.moveToNext()) { ContactData dataRecord = new ContactData(); dataRecord.fillinFrom(c);

996 Android 3. Tworzenie aplikacji this.mReportTo.reportBack(tag, dataRecord.toString()); } } }

Do wywołania funkcji publicznych występujących w tej klasie potrzebne nam będą elementy menu z listingu 27.36, które należy dodać do pliku main_menu.xml. Listing 27.36. Opcje menu wymagane do testowania danych kontaktu

Aktywność sterująca musi zostać zmodyfikowana zgodnie z zawartością listingu 27.37, gdyż w przeciwnym wypadku nie będzie reagowała na wciśnięcia wspomnianych elementów menu i nie będzie wywoływała funkcji klasy ContactDataFunctionTester. Listing 27.37. Zaktualizowana aktywność sterująca, pozwalająca na testowanie danych kontaktu public class TestContactsDriverActivity extends DebugActivity implements IReportBack { public static final String tag="TestContacts";

...inne funkcje testowe ...dodajmy poniższy wiersz na końcu pozostałych funkcji testowych ContactDataFunctionTester contactDataFunctionTester = null; public TestContactsDriverActivity() {

...dodajmy poniższy wiersz na końcu niniejszej funkcji contactDataFunctionTester = new ContactDataFunctionTester(this,this); } protected boolean onMenuItemSelected(MenuItem item) {

...odpowiada na pozostałe elementy menu ...dodajmy następujące wiersze if (item.getItemId() == R.id.menu_show_rce_cursor) { contactDataFunctionTester.showRawContactsEntityCursor(); return true; } if (item.getItemId() == R.id.menu_show_rce_data) { contactDataFunctionTester.showRawContactsData(); return true; }

...koniec nowych wierszy return true; } }

Rozdział 27 „ Analiza interfejsu kontaktów

997

Przeanalizujmy teraz powyższy kod i cały przykładowy program. Android ustanawia specjalny widok, RawContactEntity, służący do odczytywania danych z tabeli nieprzetworzonych kontaktów oraz z odpowiadających jej tabel danych, co zostało zaprezentowane w punkcie dotyczącym widoku Contact_entities_view. Identyfikator URI uzyskujący dostęp do tego widoku został zdefiniowany w pomocniczej klasie. Pełna ścieżka stałej tego identyfikatora została zaprezentowana na listingu 27.38. Listing 27.38. Identyfikator treści nieprzetworzonego kontaktu ContactsContract.RawContactsEntity.CONTENT_URI

Powyższy identyfikator jest wykorzystywany w omawianym programie do uzyskiwania informacji na temat przekazywanych pól. Spis tych pól otrzymamy po wciśnięciu opcji menu kursor encji kontaktu. Listing 27.39 prezentuje spis kolumn uzyskiwany po kliknięciu wspomnianego elementu menu. Listing 27.39. Kolumny kursora encji kontaktu data_version; contact_id; version; data12;data11;data10; mimetype; res_package; _id; data15;data14;data13; name_verified; is_restricted; is_super_primary; data_sync1;dirty;data_sync3;data_sync2; data_sync4;account_type;data1;sync4;sync3; data4;sync2;data5;sync1; data2;data3;data8;data9; deleted; group_sourceid; data6;data7; account_name; data_id; starred; sourceid; is_primary;

Po zapoznaniu się z tym zestawem kolumn możemy zawęzić wyniki przekazywane przez ten kursor poprzez sformułowanie klauzuli WHERE. Przykładowo po kliknięciu następnego elementu menu uzyskamy elementy danych, którym przypisano identyfikatory kontaktu o wartościach 3, 4 i 5. W tym celu wystarczyło dodać w kodzie następującą klauzulę WHERE: "contact_id in (3,4,5)"

i wysłać ją wraz z kursorem. Dokładnie taka operacja została powiązana z obiektem menu dane kontaktu. Po jego wciśnięciu zostaną wyświetlone takie informacje, jak imię i nazwisko oraz adres e-mail (element danych rozpoznajemy po jego typie MIME).

998 Android 3. Tworzenie aplikacji

Dodawanie kontaktu oraz szczegółowych informacji o nim Dotychczas omawialiśmy jedynie odczytywanie kontaktów. Zajmijmy się teraz przykładowym programem pozwalającym na dodanie nowego kontaktu zawierającego imię, nazwisko, adres e-mail oraz numer telefonu. Aby móc zapisywać informacje w kontakcie, trzeba dodać następujące uprawnienie w pliku manifeście (listing 27.20): android.permission.WRITE_CONTACTS

Do przetestowania tego przykładu będziemy potrzebować nowego pliku: „ AddContactFunctionTester.java. Ponadto musimy zmodyfikować następujące pliki, pochodzące z poprzednich przykładów: „ main_menu.xml, „ TestContactsDriverActivity.java. Plik AddContactFunctionTester.java pozwala na dodawanie kontaktu wypełnionego danymi. Na listingu 27.40 widzimy kod źródłowy tego pliku. Listing 27.40. Dodawanie kontaktów zawierających szczegółowe informacje Import android.provider.ContactsContract.Data;

//...pozostałe instrukcje importu, które środowisko Eclipse może dodać za nas public class AddContactFunctionTester extends ContactDataFunctionTester { public AddContactFunctionTester(Context ctx, IReportBack target) { super(ctx, target); } public void addContact() { long rawContactId = insertRawContact(); this.mReportTo.reportBack(tag, "RawcontactId:" + rawContactId); insertName(rawContactId); insertPhoneNumber(rawContactId); showRawContactsDataForRawContact(rawContactId); } private void insertName(long rawContactId) { ContentValues cv = new ContentValues(); cv.put(Data.RAW_CONTACT_ID, rawContactId); cv.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE); cv.put(StructuredName.DISPLAY_NAME,"Gall Anonim_" + rawContactId); this.mContext.getContentResolver().insert(Data.CONTENT_URI, cv); } private void insertPhoneNumber(long rawContactId) { ContentValues cv = new ContentValues(); cv.put(Data.RAW_CONTACT_ID, rawContactId); cv.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE); cv.put(Phone.NUMBER,"123 123 " + rawContactId); cv.put(Phone.TYPE,Phone.TYPE_HOME);

Rozdział 27 „ Analiza interfejsu kontaktów

999

this.mContext.getContentResolver().insert(Data.CONTENT_URI, cv); } private long insertRawContact() { ContentValues cv = new ContentValues(); cv.put(RawContacts.ACCOUNT_TYPE, "com.google"); cv.put(RawContacts.ACCOUNT_NAME, "[emailprotected]"); Uri rawContactUri = this.mContext.getContentResolver() .insert(RawContacts.CONTENT_URI, cv); long rawContactId = ContentUris.parseId(rawContactUri); return rawContactId; } private void showRawContactsDataForRawContact(long rawContactId) { Cursor c = null; try { Uri uri = ContactsContract.RawContactsEntity.CONTENT_URI; c = this.getACursor(uri,"_id = " + rawContactId); this.printRawContactsData(c); } finally { if (c!=null) c.close(); } } }

Jedyną funkcją publiczną jest addContact(). W celu jej wywołania potrzebny nam będzie element menu zamieszczony na listingu 27.41. Listing 27.41. Element menu pozwalający na dodawanie kontaktu

Powyższe dwa wiersze umieszczamy w pliku main_menu.xml. Musimy zmodyfikować także aktywność sterującą w taki sposób, żeby uruchamiała metodę addContact() po wciśnięciu utworzonej przed chwilą opcji menu. Na listingu 27.42 prezentujemy kod źródłowy zmodyfikowanej aktywności sterującej (pamiętajmy, że mamy tu do czynienia nie z nowym plikiem, lecz z aktualizacją starego). Listing 27.42. Zaktualizowana aktywność sterująca, pozwalająca na dodawanie kontaktów public class TestContactsDriverActivity extends DebugActivity implements IReportBack {

...inne mechanizmy AddContactFunctionTester addContactFunctionTester = null; public TestContactsDriverActivity() {

1000 Android 3. Tworzenie aplikacji ...inne mechanizmy addContactFunctionTester = new AddContactFunctionTester(this,this); } protected boolean onMenuItemSelected(MenuItem item) {

...inne mechanizmy if (item.getItemId() == R.id.menu_add_contact) { addContactFunctionTester.addContact(); return true; } return true; } }

Jeżeli klikniemy teraz opcję menu Dodaj kontakt, kod zawarty na listingu 27.40 (funkcja testowa dodawania kontaktów) wykona następujące czynności: 1. Najpierw doda do predefiniowanego konta (korzystając z jego nazwy i typu) nieprzetworzony kontakt za pomocą metody insertRawContact(). 2. Pobierze identyfikator nieprzetworzonego kontaktu i wstawi w tabeli danych rekord imienia i nazwiska (metoda insertName()). 3. Znowu pobierze identyfikator nieprzetworzonego kontaktu i wstawi w tabeli danych rekord numeru telefonu (metoda insertPhoneNumber()). Na listingu 27.40 widzimy aliasy kolumn wykorzystywane przez wymienione metody podczas wstawiania rekordów. Umieściliśmy je również na listingu 27.43, aby uprościć ich przeglądanie. Listing 27.43. Używanie aliasów kolumn w standardowych strukturach danych kontaktu cv.put(Data.RAW_CONTACT_ID, rawContactId); cv.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE); cv.put(StructuredName.DISPLAY_NAME,"Gall Anonim_" + rawContactId); cv.put(Data.RAW_CONTACT_ID, rawContactId); cv.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE); cv.put(Phone.NUMBER,"123 123 " + rawContactId); cv.put(Phone.TYPE,Phone.TYPE_HOME); cv.put(RawContacts.ACCOUNT_TYPE, "com.google"); cv.put(RawContacts.ACCOUNT_NAME, "[emailprotected]");

Szczególnie istotne jest, aby pamiętać, że takie stałe, jak Phone.TYPE czy Phone.NUMBER, odnoszą się w rzeczywistości do nazw kolumn data1 i data2 umieszczonych w tabeli danych. Aby w końcu ujrzeć dodany rekord, kliknijmy element menu Dodaj kontakt. Zostaną dodane i wyświetlone szczegółowe informacje tego rekordu, gdyż zostaną one odczytane za pomocą funkcji showRawContactsDataForRawContact(). Każde z pól danych będzie umieszczone w strukturze ContactData.

Rozdział 27 „ Analiza interfejsu kontaktów

1001

Kontrola agregacji W tej chwili dla Czytelnika powinno już być jasne, że klienty aktualizujące lub dodające kontakty nie modyfikują tabeli contact w jawny sposób. Tabela ta jest modyfikowana przez obiekty wyzwalające, które śledzą tabelę nieprzetworzonych kontaktów oraz tabelę danych. Z kolei dodawane lub zmieniane nieprzetworzone kontakty oddziałują na kontakty zbiorcze znajdujące się w tabeli kontaktów. Niekiedy jednak powiązanie dwóch kontaktów ze sobą jest niekorzystne. Możemy kontrolować proces łączenia nieprzetworzonych kontaktów poprzez ustalenie trybu agregacji w czasie ich tworzenia. Jak widać po umieszczonych na listingu 27.33 nazwach kolumn tabeli nieprzetworzonych kontaktów, zawiera ona pole aggregation_mode. Wartości umieszczone w tym polu zostały wymienione na listingu 27.2 i objaśnione w punkcie „Kontakty zbiorcze”. Możemy również zapewnić, by dwa kontakty pozostawały rozdzielone, poprzez umieszczenie odpowiednich wierszy w tabeli agg_exceptions. Identyfikatory URI wymagane do wstawiania danych do tej tabeli są zdefiniowane w klasie ContactsContract.AggregationExceptions. Struktura tabeli agg_exceptions została zaprezentowana na listingu 27.44. Listing 27.44. Definicja tabeli zawierającej wyjątki agregacji CREATE TABLE agg_exceptions (_id INTEGER PRIMARY KEY AUTOINCREMENT, type INTEGER NOT NULL, raw_contact_id1 INTEGER REFERENCES raw_contacts(_id), raw_contact_id2 INTEGER REFERENCES raw_contacts(_id))

Kolumna type może przechowywać jedną ze stałych wymienionych na listingu 27.45. Listing 27.45. Typy agregacji definiowane w tabeli wyjątków agregacji TYPE_KEEP_TOGETHER TYPE_KEEP_SEPARATE TYPE_AUTOMATIC

Definicje i zadania poszczególnych typów agregacji są dość zrozumiałe. Typ TYPE_KEEP_TOGETHER nie pozwala na rozdzielenie dwóch kontaktów. Z kolei wartość TYPE_KEEP_SEPARATE uniemożliwia połączenie kontaktów. Ostatnia wartość, TYPE_AUTOMATIC, wykorzystuje domyślny algorytm agregacji kontaktów. Identyfikator URI umożliwiający umieszczanie, odczytywanie i aktualizowanie wierszy zawartych w tej tabeli wygląda następująco: ContactsContract.AggregationExceptions.CONTENT_URI

Również stałe wykorzystywane wraz z definicjami pól w tej tabeli są dostępne w klasie Contacts ´Contract.AggregationExceptions.

1002 Android 3. Tworzenie aplikacji

Konsekwencje synchronizacji Przez większość rozdziału zajmowaliśmy się wyłącznie manipulowaniem kontaktami w obrębie urządzenia. Zazwyczaj jednak konta i przypisane im kontakty są synchronizowane łącznie. Jeśli na przykład utworzyliśmy konto Google w telefonie obsługiwanym przez system Android, wszystkie kontakty zostaną skopiowane z serwera do tego telefonu. Za każdym razem, gdy w urządzeniu dodajemy nowy kontakt lub konto serwerowe, nastąpi proces synchronizacji i odzwierciedlenia danego obiektu w obydwu miejscach. Jednak w tym wydaniu książki nie zajęliśmy się omówieniem interfejsu synchronizowania ani mechanizmem jego działania. Jest to zagadnienie równie obszerne jak tematyka kontaktów. Znajomość działania interfejsu kontaktów znacznie pomaga w zrozumieniu interfejsu synchronizacji. Zalecamy więc zaglądanie na stronę www.androidbook.com, która jest dość regularnie aktualizowana. Natura mechanizmu synchronizacji wpływa również na proces usuwania kontaktów z urządzenia. W czasie usuwania kontaktu za pomocą identyfikatora kontaktu zbiorczego zostaną wykasowane wszystkie związane z nim nieprzetworzone kontakty, a także elementy danych powiązane z tymi kontaktami. Jednak system jedynie oznacza te obiekty jako usunięte i dopiero w procesie przebiegającej w tle synchronizacji z serwerem zaznaczone kontakty zostaną trwale usunięte z urządzenia. Taka kaskada procesów kasowania występuje także na poziomie nieprzetworzonych kontaktów, gdzie zostają usunięte elementy danych powiązane z danym nieprzetworzonym kontaktem.

Odnośniki Zaprezentowane poniżej odnośniki umożliwią Czytelnikowi dostęp do materiałów pomocniczych oraz rozszerzających zakres informacji zawartych w tym rozdziale. Ostatni z zamieszczonych adresów URL umożliwia pobranie projektów utworzonych specjalnie na potrzeby tego rozdziału. „ http://www.google.com/support/mobile/bin/answer.py?answer=182077 — adres instrukcji obsługi Androida w wersji 2.3. Znajdziemy w niej informacje dotyczące aplikacji Kontakty, pozwalającej na zarządzanie kontaktami. Chociaż omówiliśmy podstawowe informacje związane z obsługą tej aplikacji, najważniejsza w tej kwestii pozostaje instrukcja obsługi. Czytelnik może w niej znaleźć informacje, które my mogliśmy przypadkowo przeoczyć. „ http://www.google.com/help/hc/pdfs/mobile/AndroidUsersGuide-30-100.pdf — instrukcja obsługi Androida w wersji 3.0. „ http://developer.android.com/resources/articles/contacts.html — ten adres URL prowadzi do artykułu, w którym omówiono sposób korzystania z interfejsu kontaktów. Jest to podstawowa dokumentacja dotycząca interfejsu kontaktów, stworzona przez firmę Google. „ http://www.androidbook.com/item/3585 — zrozumienie koncepcji interfejsu kontaktów polega przede wszystkim na pojęciu struktury tworzących go tabel. Klasa ContactsContract stanowi jedynie cienką osłonę wokół podstawowej struktury tabel. Pod tym adresem można znaleźć informacje o różnorodnych strukturach tabel opracowanych przez autorów książki. Znajdziemy tam nazwy pól, ich typy, widoki kontaktów zbiorczych i tak dalej.

Rozdział 27 „ Analiza interfejsu kontaktów

„

„

„

„

„

1003

http://developer.android.com/reference/android/provider/ContactsContract.html — dokumentacja Javadoc opisująca klasę wejściową opublikowanego kontraktu kontaktów. Bardzo przydatny adres dla osób zajmujących się pisaniem programów wykorzystujących interfejs kontaktów. http://www.netmite.com/android/mydroid/2.0/packages/providers/ContactsProvider/ — z powodu niedostatku informacji dotyczących obsługi kontaktów być może Czytelnika zainteresuje kod źródłowy dostawcy kontaktów. Pod tym adresem znajdziemy stronę Netmite, gdzie zamieszczono kody źródłowe wszystkich plików tworzących dostawcę treści. http://www.netmite.com/android/mydroid/2.0/packages/apps/Contacts/src/com/andr oid/contacts — podobnie jak w poprzednim przypadku, łącze to prowadzi do kodu źródłowego aplikacji Kontakty. Jeżeli Czytelnik chce poznać mechanizm tworzenia lub aktualizowania kontaktu zbiorczego, właśnie znalazł żyłę złota. http://www.androidbook.com/item/3537 — jeżeli Czytelnik przeglądał kody źródłowe dostępne pod dwoma powyższymi adresami, prawdopodobnie poczuł się nieco ogłuszony. Pod tym adresem znajdziemy więc podsumowanie danych tam zawartych, które być może komuś się przyda. ftp://ftp.helion.pl/przyklady/and3ta.zip — pod tym adresem znajdziemy projekty utworzone specjalnie na potrzeby niniejszej książki. Interesujący nas katalog nosi nazwę ProAndroid3_R27_Kontakty.

Podsumowanie W niniejszym rozdziale zapoznaliśmy się ze strukturą kontaktów, dostępną w systemie Android. Możemy wykorzystać zawarte tu informacje do odczytywania lub aktualizowania kontaktów za pomocą publicznego interfejsu kontaktów. Chociaż poświęciliśmy mnóstwo uwagi interfejsowi kontaktów, nie omówiliśmy pracy z dostawcami treści pracującymi w trybie wsadowym, w którym można dodawać lub usuwać kontakty. Zestaw Android SDK zawiera klasę ContentProviderOperation umożliwiającą przeprowadzenie procesów wstawiania, aktualizowania i usuwania kontaktów w trybie wsadowym, co pozwala na optymalizację działania systemu. Tryb wsadowy staje się tym istotniejszy dla dostawców synchronizacji, im większa liczba kontaktów jest aktualizowana i dodawana. W przypadku kwerend oraz sporadycznych aktualizacji omówione w tym rozdziale rozwiązania są całkowicie wystarczające. Warto jednak co jakiś czas zaglądać na stronę www.androidbook.com.

1004 Android 3. Tworzenie aplikacji

R OZDZIAŁ

28 Wdrażanie aplikacji na rynek — Android Market i nie tylko

Stworzenie wspaniałej aplikacji, którą pokochają użytkownicy, to jedna sprawa. Należy też zadbać o wprowadzenie rozwiązania umożliwiającego jej szybkie znalezienie i pobranie. W tym właśnie celu firma Android zaprojektowała sklep Android Market. Za pomocą ikony umieszczonej po prawej stronie urządzenia użytkownicy mogą przejść wprost na stronę sklepu i przeglądać, wyszukiwać, oceniać oraz pobierać aplikacje. Użytkownicy mogą uzyskać dostęp do serwisu Android Market również z poziomu komputera stacjonarnego, chociaż pobierane pliki będą ostatecznie umieszczane w urządzeniu, a nie w stacji roboczej. Część aplikacji jest dostępna za darmo, a w przypadku płatnych wersji zostały wprowadzone mechanizmy płatnicze usprawniające szybki zakup. Android Market jest dostępny nawet z poziomu intencji wewnątrz aplikacji, dzięki czemu użytkownik w łatwy sposób może znaleźć miejsce, z którego można pobrać składniki wymagane przez ten program. Na przykład po wydaniu nowej wersji aplikacji możemy pozwolić użytkownikowi dostać się bezpośrednio do lokacji, z której można pobrać lub zakupić ten plik. Android Market nie jest jednak jedynym miejscem, w którym można zaopatrzyć się w aplikacje; w internecie cały czas pojawiają się nowe kanały. Android Market nie jest dostępny z poziomu emulatora (chociaż istnieją pewne nielegalne sposoby obejścia tego problemu). Stanowi to swego rodzaju utrudnienie dla programisty. Najlepszym rozwiązaniem jest własne urządzenie pozwalające na połączenie z Android Market. Sklep ten jest dostępny poprzez urządzenie Android Developer Phone, zablokowano w nim jednak dostęp do płatnych aplikacji. Jest to jedno z rozwiązań firmy Google chroniących te aplikacje przed piractwem. W rozdziale tym zajmiemy się konfigurowaniem procesu publikowania aplikacji w sklepie Android Market, przygotowaniem jej do sprzedaży, uproszczeniem procesu wyszukiwania, pobierania i korzystania z niej przez użytkowników, sposobami zabezpieczania aplikacji przed piractwem, a na końcu zaprezentujemy kilka alternatywnych sposobów udostępnienia programów bez wykorzystania sklepu firmy Google.

1006 Android 3. Tworzenie aplikacji

Jak zostać wydawcą? Zanim umieścimy aplikację w sklepie Android Market, musimy zostać wydawcami. W tym celu należy utworzyć konto programisty (ang. Developer Account). Po zarejestrowaniu takiego konta będziemy mogli zamieszczać aplikacje w sklepie Android Market, gdzie będą wyszukiwane i pobierane przez użytkowników. Proces rejestracji konta programisty jest względnie prosty i stosunkowo tani. Aby co*kolwiek opublikować, potrzebne jest konto Google — na przykład konto pocztowe gmail.com. Następnie tworzymy tożsamość w sklepie Android Market. W tym celu otwieramy stronę http://market.android.com/publish/signup. Wprowadzamy tu imię i nazwisko programisty, adres e-mail, adres strony WWW oraz numer telefonu kontaktowego. Po zarejestrowaniu konta dane te będzie można zmienić. Następnie trzeba uiścić opłatę rejestracyjną. Zajmuje się tym system Google Checkout. Przeprowadzenie całej transakcji wymaga zalogowania się na konto Google. Jedną z opcji dostępnych podczas procesu płatności jest Zachowaj poufny charakter mojego adresu e-mail. Odnosi się to do bieżącej transakcji pomiędzy programistą a usługą Google Android Market, dotyczącej „zakupu” praw wydawcy. Zaznaczenie tej opcji spowoduje ukrycie adresu e-mail przed usługą Google Android Market. Nie ma to nic wspólnego z ukrywaniem adresu e-mail przed potencjalnymi użytkownikami aplikacji. Wybór tej opcji nie ma wpływu na dostępność adresu e-mail dla osób kupujących aplikację. W dalszej części rozdziału rozwiniemy ten temat. Następnie zostanie wyświetlona umowa dotycząca dystrybucji produktów przez ich dewelopera za pośrednictwem usługi Android Market. Jest to legalny kontrakt zawierany pomiędzy programistą a firmą Google. Są w nim określone warunki dystrybucji aplikacji, pobierania i zwrotu płatności, wsparcia i obsługi technicznej, systemu oceniania, praw nabywcy, praw wydawcy i tak dalej. Więcej informacji na temat tych reguł znajdziemy w podrozdziale „Postępowanie zgodnie z zasadami”. Po zaakceptowaniu umowy ujrzymy stronę znaną powszechnie jako konsola programisty (ang. Developer Console) — http://market.android.com/publish/Home.

Postępowanie zgodnie z zasadami Umowa dotycząca dystrybucji produktów przez ich dewelopera za pośrednictwem usługi Android Market (ang. Android Market Developer Distribution Agreement — AMDDA) zawiera mnóstwo reguł postępowania. Być może przed jej zaakceptowaniem należałoby zasięgnąć opinii radcy prawnego, w zależności od zakresu planowanych działań wewnątrz serwisu. W tym punkcie omówimy kilka kwestii wartych odnotowania. „ Aby korzystać ze sklepu Android Market, należy być programistą o nieposzlakowanej reputacji,. Oznacza to, że trzeba przejść przez omówiony powyżej proces rejestracji, zaakceptować umowę i przestrzegać jej zasad. Skutkiem złamania zasad może być zablokowanie dostępu do sklepu i usunięcie z niego naszych aplikacji. „ Możemy dystrybuować zarówno produkty bezpłatnie, jak i za opłatą. Umowa dopuszcza obie możliwości. W przypadku sprzedaży produktów musimy korzystać z takiego procesora płatności, jak na przykład Google Checkout. W momencie wydania platformy Android 2.0 jedyną formą pobierania opłat pochodzących ze sklepu Android Market była właśnie usługa Google Checkout. Obecnie użytkownicy mogą

Rozdział 28 „ Wdrażanie aplikacji na rynek — Android Market i nie tylko

„

„

„

„

„

„

1007

po prostu obciążyć swój rachunek telefoniczny podczas pobierania opłat, co zostało ogłoszone przez firmę T-Mobile w 2009 roku oraz przez firmę AT&T w 2010 roku. W październiku 2010 roku zapowiedziano integrację usługi PayPal z serwisem Android Market, lecz po ponad roku ciągle brakuje takiej opcji. W przyszłości to podejście może jednak ulec zmianie. W przypadku zakupu płatnych aplikacji jest pobierana opłata transakcyjna oraz ewentualnie opłata dla operatora sieci komórkowej, które zostaną odjęte od ceny produktu. W listopadzie 2011 roku opłata transakcyjna wynosi 30%, jeśli więc produkt kosztuje 10 dolarów, firma Google otrzymuje 3 dolary, a my 7 (pod warunkiem że nie dochodzą do tego opłaty dla operatora). Do programisty należy obowiązek odprowadzania należnego podatku do właściwych organów podatkowych. Podczas ustanawiania konta handlowego definiuje się odpowiednie wysokości podatku dotyczące zakupu produktu przez osoby znajdujące się w innych rejonach. Usługa Google Checkout pobierze odpowiedni podatek w zależności od tego, w jaki sposób została skonfigurowana. Opłata ta zostanie wysłana do programisty, który musi ją odprowadzić do urzędu skarbowego. Dodatkowe informacje na temat sprzedaży usług i licencji w Polsce można znaleźć pod adresem http://www.vat.pl/ sprzedaz_licencji_ebooki_oprogramowanie_firma_w_internecie_1052.php. Istnieje możliwość umieszczania w serwisie Android Market darmowej wersji demonstracyjnej aplikacji, posiadającej opcję odblokowania wszystkich funkcji pełnej wersji po wniesieniu opłaty; opłata musi być jednak uiszczona poprzez autoryzowany procesor płatności. Nie możemy skierować użytkowników darmowej aplikacji do innego procesora płatności w celu pobrania opłaty za odblokowanie wszystkich funkcji programu. Zabronione jest również pobieranie opłaty za prenumerowanie aplikacji dystrybuowanych poprzez sklep Android Market. Opłaty za usługi zasadniczo są nawet zalecane, gdyż pomagają chronić aplikację przed piractwem oraz zwiększają obroty twórców oprogramowania. Oznacza to jednak, że nie możemy sprzedawać takiej wersji aplikacji poprzez Android Market. Być może zostanie to zmienione w przyszłości. Pomyślmy o tym w następujący sposób: jeżeli chcemy zarabiać poprzez serwis Android Market, firma Google pragnie mieć swój udział w tych zarobkach. W lutym 2011 roku firma Google zapowiedziała wprowadzenie wewnątrzaplikacyjnych mikrotransakcji. Jest to dodatkowy pakiet SDK pozwalający na pobieranie opłat za cyfrowe towary lub dodatkową zawartość programu. Taką zawartością może być nowa broń lub pakiet poziomów w grze bądź pliki graficzne czy muzyczne. Zapłata za takie dodatki wygląda tak samo jak proces kupna aplikacji, co oznacza, że użytkownicy mogą obciążyć swój rachunek telefoniczny. Jeżeli nasza aplikacja wymaga od użytkownika posiadania konta w jakimś serwerze sieciowym, z którego można korzystać za dodatkową opłatą abonamentową, serwer ten może pobierać tę opłatę w dowolny sposób. Taki sposób odłączenia opłaty abonamentowej od aplikacji jest zgodny z umową AMDDA — pod warunkiem że bezpłatna wersja aplikacji nie kieruje użytkowników na jej stronę domową. Nie byłoby jednak łatwiej rozprowadzać aplikację z tego samego serwera, na którym znajduje się usługa? Okazuje się, że istnieje możliwość wprowadzania alternatywnych sposobów przetwarzania płatności wnoszonych w formie darowizny przeznaczonej na rozwój darmowej wersji aplikacji, nie można jednak wewnątrz programu wprowadzać żadnych bodźców motywujących do uiszczenia takiej opłaty.

1008 Android 3. Tworzenie aplikacji „

„

„

„

„

„

„

Zwroty płatności są nieprzyjemną stroną serwisu Android Market. Początkowo użytkownikom przysługiwały 24 godziny na zażądanie zwrotu pieniędzy za zakupioną aplikację. Następnie czas ten został wydłużony do 48 godzin. Natomiast w grudniu 2010 roku został zmieniony na 15 minut! Kwadrans ten jest liczony od samego momentu zakupu, a nie od chwili zakończenia pobierania aplikacji. Zdarzały się nawet przypadki, gdy użytkownik nie zdążył jeszcze pobrać aplikacji, a okno czasowe zagwarantowane na zwrot pieniędzy zakończyło działanie. Co dziwne, umowa AMDDA nie została zaktualizowana pod tym kątem i ciągle widnieje w niej wpis o 48 godzinach przysługujących na zwrot pieniędzy. Zwroty pieniędzy nie przysługują użytkownikom, którzy mogą przeglądać aplikacje przed ich zakupem. Dotyczy to również dzwonków i tapet. Usługa Google Checkout pozwala jednak deweloperom na zwrot pieniędzy nawet po wyznaczonym terminie, użytkownicy mogą więc mimo wszystko odzyskać pieniądze. Deweloperzy jednak nie mają ochoty na własnoręczne oddawanie pieniędzy. Wymagane jest, aby programista zagwarantował odpowiednią pomoc techniczną dotyczącą produktu. Jeżeli pomoc ta nie zostanie zapewniona, użytkownicy mogą żądać zwrotu pieniędzy. Będzie to się wiązać z kosztami dla dewelopera, zwłaszcza że prawdopodobnie będą w to wliczone również koszty manipulacyjne. Użytkownicy mają prawo do nieograniczonej liczby ponownych instalacji aplikacji pobranych ze sklepu Android Market. W przypadku przywrócenia ustawień fabrycznych urządzenia użytkownik będzie mógł pobrać wszelkie zakupione aplikacje bez konieczności ponownego płacenia za nie. Wydawca gwarantuje ochronę prywatności i praw użytkowników. Dotyczy to ochrony (na przykład zabezpieczania) wszelkich danych, które mogą zostać zebrane podczas użytkowania aplikacji. Zmiana warunków dotyczących ochrony danych użytkownika jest dopuszczalna jedynie w przypadku wyświetlenia odpowiedniej umowy i zaakceptowania jej przez użytkownika. Aplikacja nie może konkurować ze sklepem Android Market. Firma Google nie pozwala na umieszczanie aplikacji umożliwiających sprzedaż innych produktów poza sklepem Android Market, co jest równoznaczne z ominięciem procesora płatności. Nie oznacza to wcale, że nie można sprzedawać tej aplikacji innymi kanałami, lecz że ta aplikacja, po jej umieszczeniu w sklepie Android Market, nie może umożliwiać sprzedaży produktów znajdujących się poza tą usługą. Umieszczone produkty zostaną objęte systemem oceniania. Oceny mogą być przydzielane na podstawie udzielanej pomocy technicznej, szybkości instalacji i odinstalowania aplikacji, szybkości zwrotu kosztów oraz (lub) jako tak zwana ocena ogólna dewelopera (ang. Developer Composite Score). Jest ona szacowana na podstawie ocen wystawianych dla poprzednich aplikacji i może wpływać na ocenę przyszłych produktów. Z tego powodu istotne jest, aby wydawać aplikacje wysokiej jakości, nawet jeśli są darmowe. Nie jesteśmy pewni, czy ocena ogólna dewelopera w ogóle istnieje, jeśli jednak tak — nie mamy w nią wglądu. Poprzez sprzedaż aplikacji w sklepie Android Market udzielamy użytkownikowi „niewyłącznej i ogólnoświatowej licencji na odtwarzanie, prezentowanie i użytkowanie Produktu w Urządzeniu”. Jednak nic się nie stanie, jeśli napiszemy własne warunki umowy licencyjnej (ang. End User License Agreement — EULA), zastępujące powyższe stwierdzenie. Należy taką umowę umieścić na własnej stronie WWW lub zagwarantować jakiś inny sposób jawnego zaprezentowania jej użytkownikom.

Rozdział 28 „ Wdrażanie aplikacji na rynek — Android Market i nie tylko

„

1009

Firma Google wymaga przestrzegania cech marki Android. Do tych cech zaliczają się ograniczenia w wykorzystywaniu słowa „Android”, jak również ikony robota, znaku towarowego oraz kroju pisma. Informacje na ten temat znajdziemy pod adresem http://www.android.com/branding.html.

Konsola programisty Konsola programisty jest naszą stroną docelową, pozwalającą na kontrolowanie aplikacji umieszczonych w serwisie Android Market. Z poziomu konsoli programisty możemy zakupić urządzenie ADP (ang. Android Developer Phone — telefon programisty systemu Android), skonfigurować konto handlowe w usłudze Google Checkout (dzięki czemu możemy naliczać opłatę za aplikację), publikować aplikacje oraz przeglądać informacje na temat opublikowanych programów. Możemy także edytować takie szczegóły konta, jak imię i nazwisko programisty, adres e-mail, adres strony WWW oraz numer telefonu. Konsola programisty została zaprezentowana na rysunku 28.1.

Rysunek 28.1. Konsola programisty pozwalająca uzyskać dostęp do serwisu Android Market

Obecnie istnieją trzy rodzaje telefonów testowych obsługujących system Android: Android Developer Phone, Google Nexus One oraz Google Nexus S. Android Developer Phone (ADP) był przez długi czas jedynym telefonem pozwalającym na testowanie programowanych aplikacji. Jest to specjalne urządzenie, zaprojektowane przede wszystkim dla programistów aplikacji dla systemu Android. Jest to profesjonalny telefon, posiadający odblokowane wszystkie funkcje, niezależny od operatorów telefonii komórkowej. Akceptuje wszystkie rodzaje kart SIM, a w jego wyposażeniu znajduje się karta pamięci 1 GB, aparat fotograficzny, wysuwana klawiatura i system GPS. Pisząc o odblokowanych funkcjach, mamy na myśli możliwość wykonania każdej

1010 Android 3. Tworzenie aplikacji czynności w urządzeniu, łącznie z instalacją nowej wersji oprogramowania sprzętowego i systemu Android, nie tylko aplikacji. Chociaż możemy instalować nowe wersje oprogramowania sprzętowego, fabryczna wersja tego urządzenia jest wyposażona w system Android 1.6. Być może Czytelnik pamięta czasy wydania pierwszego telefonu firmy Google, jakim jest Nexus One. Chociaż był zaprojektowany we współpracy z firmą HTC, z powodu słabych wyników sprzedaży zaprzestano produkcji tego telefonu, a następnie przerobiono go na telefon testowy, pozwalający użytkownikom na sprawdzanie działania tworzonych aplikacji. Został w tej roli bardzo dobrze przyjęty, więc firma Google zwiększyła zamówienia na produkcję tego modelu. Specyfikacja techniczna telefonu Nexus One robi wrażenie. Został on zaopatrzony w czujnik zbliżeniowy, czujnik oświetlenia, akcelerometry oraz kompas. Bez większego problemu obsługuje system Android 2.2 i jeszcze przez pewien czas nie powinien mieć problemu z nowszymi wersjami systemu. Specyfikacja tego telefonu jest dostępna pod adresem http://www.htc.com/us/support/nexus-one-google/tech-specs/. Wśród wad należy wymienić brak obsługi technologii 3G, co zmusza do korzystania z innych form sieci bezprzewodowych (AT&T, 2G lub EDGE). W grudniu 2010 roku firma Google rozpoczęła sprzedaż telefonu Nexus S, którego jednak nie można zamówić z poziomu konsoli programisty. Został po raz pierwszy udostępniony w sklepie Best Buy (USA) oraz w Carphone Warehouse (Wielka Brytania) i może zostać zakupiony bez umowy z operatorem sieci komórkowej. Urządzenie to jest jeszcze szybsze i bardziej zaawansowane od telefonu Nexus One, posiada wbudowany żyroskop, czujnik NFC, a także aparat umieszczony z przodu. Jest obsługiwany przez system Android 2.3 (Gingerbread). Specyfikacja, wraz w filmami demonstracyjnymi, jest dostępna pod adresem www.google.com/nexus/#. Jeżeli chcemy testować nowe wersje oprogramowania sprzętowego lub sam system Android, potrzebne nam będzie urządzenie testowe. Z trzech dostępnych rodzajów telefonów najlepszym wyborem jest Nexus S. Jednym z pozytywnych aspektów telefonów z grupy Nexus jest pierwszeństwo w aktualizowaniu ich do nowej wersji systemu. Jest to jeden z powodów, dla których warto wybrać telefon Nexus zamiast zwykłego telefonu związanego z operatorem. Jeżeli chcemy jedynie pisać aplikacje, a nie modyfikować w jakiś sposób system Android, powinien nam wystarczyć zwykły telefon. Każde urządzenie obsługujące system Android może zostać podłączone do stacji roboczej w celach projektowych i testowych. Musimy jednak zwracać uwagę na specyfikację techniczną. Nie wszystkie telefony mogą zostać zaktualizowane do najnowszej wersji systemu, dotyczy to zwłaszcza mniej wydajnych modeli. Jeśli nie skonfigurujemy konta handlowego za pomocą usługi Google Checkout, nie będziemy mogli pobierać opłat za produkty umieszczone w sklepie Android Market. Ustanowienie takiego konta nie jest skomplikowaną czynnością. Wystarczy kliknąć odpowiednie łącze w konsoli programisty, wypełnić formularz aplikacji, zaakceptować warunki korzystania z usługi i to będzie wszystko. Należy mieć przygotowany pod ręką numer karty kredytowej. Informacje o karcie kredytowej są wprowadzane w celu uiszczenia zwrotu zapłaty, w przypadku gdy na koncie Google Checkout nie ma wystarczającej ilości środków. Możemy także wprowadzić dane konta bankowego, dzięki czemu zyski ze sprzedaży aplikacji będą przenoszone na to konto. Zwróćmy uwagę, że usługa Google Checkout obsługuje nie tylko sklep Android Market. Nie powinniśmy więc się zdziwić, jeśli pojawi się informacja o opłacie transakcyjnej za sprzedaż pochodzącą spoza sklepu Android Market. Wspomniana opłata transakcyjna wynosząca 30% ceny aplikacji jest obliczona dla sklepu Android Market. Istnieją również dodatkowe opłaty transakcyjne dla sprzedaży przeprowadzanych poza tą witryną, niezależne od wspomnianej powyżej opłaty.

Rozdział 28 „ Wdrażanie aplikacji na rynek — Android Market i nie tylko

1011

Prawdopodobnie najczęściej wykorzystywanymi funkcjami konsoli programisty będą umieszczanie i monitorowanie aplikacji. W dalszej części rozdziału zajmiemy się procesem publikowania aplikacji w sklepie. W kwestii monitoringu otrzymujemy narzędzia pozwalające obserwować całkowitą liczbę pobrań aplikacji oraz liczbę użytkowników, którzy zainstalowali aplikację. Widoczna jest ogólna ocena programu w zakresie od 0 do 5 gwiazdek, a także liczba osób, które wystawiły ocenę. Z poziomu konsoli programisty możemy ponownie opublikować aplikację — na przykład jej aktualizację — lub wycofać ją ze sklepu. Ta ostatnia czynność nie usuwa aplikacji z urządzeń, a nawet nie musi jej usuwać z serwerów Google, dotyczy to zwłaszcza płatnych aplikacji. Użytkownik, który zapłacił za aplikację, a następnie ją odinstalował, lecz nie zażądał zwrotu kosztów, ma prawo do jej ponownego zainstalowania, nawet jeśli została ona wycofana z obiegu. Program przestaje być naprawdę dostępny dla użytkowników jedynie w przypadku złamania zasad firmy Google. W marcu 2011 roku firma Google dodała w konsoli programisty zestawienia i wykresy pozwalające na obserwowanie statystyk aplikacji w zależności od wersji systemu operacyjnego, rodzaju urządzenia, a także krajów i języków. Oprócz oceniania aplikacji użytkownicy mogą również pozostawiać komentarze. Dla własnego dobra powinniśmy jak najczęściej czytać komentarze dotyczące naszej aplikacji, aby móc szybko rozwiązywać zauważone problemy. Wraz z komentarzem dostępne są ocena aplikacji, nazwa użytkownika oraz data umieszczenia komentarza. Niestety, nie możemy bezpośrednio odpowiadać na komentarze ani nawet umieszczać komentarzy pod komentarzami użytkowników. W ekstremalnych przypadkach, gdy treść komentarza jest wyjątkowo szkodliwa lub niecenzuralna, możemy zgłosić to obsłudze firmy Google, dostępnej pod adresem http://market.android.com/ support/. Możemy również przeglądać komunikaty o błędach wygenerowane przez aplikację oraz sprawdzać, w których momentach ulegała zawieszeniu lub zostawała niespodziewanie zamknięta. Na rysunku 28.2 widzimy ekran raportów o błędach aplikacji.

Rysunek 28.2. Ekran Application Error Reports

Przeglądając szczegóły raportu, możemy dotrzeć do śladu stosu, w którym nastąpiła awaria, jak również uzyskać takie informacje, jak rodzaj urządzenia, na którym działała aplikacja, oraz data awarii. Podobnie jednak jak ma to miejsce w przypadku komentarzy, nie możemy skomunikować się z użytkownikiem, którego aplikacja uległa awarii, aby poznać dalsze szczegóły lub

1012 Android 3. Tworzenie aplikacji pomóc mu wyjść z opresji. Pozostaje nam nadzieja, że tacy użytkownicy napiszą na nasz adres e-mail lub zostawią informację na stronie aplikacji. W przeciwnym wypadku jesteśmy zdani na siebie i musimy samodzielnie wykryć przyczynę problemu oraz spróbować ją naprawić. Dostępna jest jeszcze jedna funkcja konsoli programisty, która może się okazać przydatna — odnośnik do materiałów pomocniczych. Przycisk Help znajduje się w prawym górnym rogu ekranu. Kliknięcie go spowoduje otwarcie witryny pomocy, zawierającej porządną dokumentację dotyczącą serwisu Android Market, a także forum, na którym możemy poszukać odpowiedzi na dręczące nas pytania i umieszczać własne porady. To na forum znajdziemy informacje dotyczące najnowszych zasad zwrotu pieniędzy, problemów i zażaleń. Jeżeli forum nie okaże się przydatne, znajdziemy tu odnośnik do pomocy technicznej (Contacting Support), za pomocą którego możemy wysłać wiadomość wprost do przedstawicieli firmy Google. Omówiliśmy niektóre z przydatnych funkcji konsoli programisty, jednak Czytelnik z pewnością nie może się doczekać najprzydatniejszej części rozdziału — omówienia procesu umieszczania aplikacji w serwisie Android Market. Dzięki temu procesowi użytkownicy będą mogli znaleźć i pobrać aplikację. Zanim jednak przejdziemy do tego etapu, musimy powiedzieć, w jaki sposób odpowiednio przygotować nasz program do pobierania i sprzedaży.

Przygotowanie aplikacji do sprzedaży Po utworzeniu kodu aplikacji, ale jeszcze przed jej umieszczeniem w sklepie Android Market, należy przeprowadzić kilka czynności przygotowawczych. Poświęcimy im teraz trochę uwagi i omówimy kolejne czynności, które trzeba wykonać.

Testowanie działania na różnych urządzeniach Bardzo istotne jest, aby przy lawinowej produkcji coraz to nowych urządzeń pracujących pod kontrolą systemu Android, z których każde zawiera potencjalnie odmienną konfigurację sprzętową, utworzona aplikacja została przetestowana pod kątem działania na docelowych telefonach. W idealnym przypadku programista ma dostęp do każdego rodzaju telefonu, który chce przetestować. Jest to dość kosztowna propozycja. Innym dobrym rozwiązaniem jest wykorzystanie urządzeń AVD dla każdego rodzaju telefonu poprzez utworzenie odpowiedniej konfiguracji sprzętowej, a następnie uruchomienie i przetestowanie takiego urządzenia na emulatorze. Niektórzy producenci urządzeń udostępniają pakiety Androida specyficzne dla danego telefonu, warto zatem przeglądać witryny sieciowe danej firmy. Zestaw Android SDK posiada klasę Instrumentation oraz program UI/Application Exerciser Monkey, usprawniające proces testowania. Wymienione narzędzia umożliwiają automatyzację testowania, nie musimy więc marnować czasu na powtarzanie tych samych czynności. Przed rozpoczęciem testowania powinniśmy usunąć zbędne artefakty z kodu oraz z folderu /res. Chcemy przecież, aby aplikacja była jak najmniejsza oraz jak najszybsza przy minimalnym zużyciu pamięci.

Obsługa różnych rozmiarów ekranu W momencie wydania środowiska Android SDK 1.6 programiści zaczęli się borykać z nowymi rozmiarami wyświetlaczy. Aby uruchomić aplikację na nowym, mniejszym ekranie, należy ustanowić swoisty element

jako element potomny węzła w pliku AndroidManifest.xml. Bez wprowadzenia tego znacznika definiującego obsługę małych ekranów przez aplikację nie będzie ona dostępna w sklepie Android Market dla urządzeń posiadają-

Rozdział 28 „ Wdrażanie aplikacji na rynek — Android Market i nie tylko

1013

cych niewielkie wyświetlacze. Oznacza to oczywiście, że aplikacja musi zostać skompilowana wobec środowiska Android SDK co najmniej w wersji 1.6. Jeśli chcemy, aby program działał w urządzeniach obsługujących starsze wersje zestawu Android SDK, musimy się upewnić, że nie będzie korzystał z żadnego interfejsu API wprowadzonego w wersji 1.6 lub nowszej oprogramowania. Należy następnie przetestować aplikację zarówno na urządzeniach AVD imitujących starsze telefony, jak i reprezentujących nowsze urządzenia. W celu obsługi różnych rozmiarów ekranu prawdopodobnie będziemy musieli utworzyć alternatywne pliki zasobów w podkatalogu /res. Na przykład w przypadku dodatkowej obsługi niewielkich wyświetlaczy oprócz plików znajdujących się w katalogu /res/layout trzeba będzie umieścić odpowiedniki tych plików w katalogu /res/layout-small. Nie oznacza to, że musimy tworzyć również odpowiedniki tych plików w katalogach /res/layout-large i /res/layout-normal, gdyż jeśli Android nie znajdzie takiego specyficznego katalogu, jak na przykład /res/layout-large, wykorzysta zasoby dostępne w katalogu /res/layout. Pamiętajmy również, że możemy tworzyć kombinacje kwalifikatorów dla plików zasobów — na przykład katalog /res/layout-small-land może zawierać układy graficzne dla małych ekranów zorientowanych w trybie poziomym. Omówiliśmy to zagadnienie w rozdziale 6. Obsługa małych wyświetlaczy oznacza prawdopodobnie również utworzenie alternatywnych wersji obiektów rysowanych, takich jak ikony. W przypadku tych obiektów może również zaistnieć potrzeba utworzenia alternatywnych katalogów zasobów, odpowiadających rozdzielczości ekranu oraz jego rozmiarowi. Oczywiście, pod względem rozmiarów ekranu tablety podążają w przeciwnym kierunku i w ich przypadku stosujemy wartość xlarge. Ten sam znacznik

służy do definiowania aplikacji uruchamianej na bardzo dużym ekranie, natomiast wprowadzanym atrybutem jest android:xlargeScreens. W niektórych przypadkach możemy mieć do czynienia z aplikacją obsługiwaną wyłącznie przez tablety, wtedy należy przypisać wartość false wszystkim pozostałym rozmiarom ekranu.

Przygotowanie pliku AndroidManifest.xml do umieszczenia w sklepie Android Market Plik AndroidManifest.xml prawdopodobnie powinien zostać troszeczkę zmodyfikowany przed umieszczeniem go w sklepie Android Market. Domyślnie narzędzia ADT środowiska Eclipse wstawiają atrybut android:icon do znacznika

, a nie do znaczników . Jeżeli istnieje możliwość uruchomienia większej liczby aktywności, powinniśmy dla każdej z nich ustanowić oddzielną ikonę, aby użytkownik mógł je łatwiej rozróżniać. Ciągle jednak musi być określona jedna ikona w znaczniku , która posłuży jako domyślna ikona dla wszystkich aktywności nieposiadających własnej ikony. Aplikacja posiadająca atrybut android:icon wyłącznie wewnątrz węzła będzie bezproblemowo działała w urządzeniach oraz na emulatorze, jednak podczas umieszczania aplikacji na serwerze usługa Android Market przeszukuje znacznik pod kątem informacji o ikonie. Przesyłanie aplikacji zostaje przerwane również w przypadku, gdy nazwa zastosowanego pakietu rozpoczyna się od ciągów znakowych com.google, com.android, android lub com.example, mamy jednak nadzieję, że nie zostały one zastosowane w aplikacjach Czytelników. Istnieje również wiele innych kwestii związanych z kompatybilnością, które należy wziąć pod uwagę podczas testowania aplikacji na różnych konfiguracjach sprzętowych. Niektóre urządzenia posiadają aparat fotograficzny, inne nie mają klawiatury fizycznej, jeszcze inne mają manipulator kulkowy zamiast klawiszy nawigacyjnych. W razie potrzeby zastosujmy znaczniki i w pliku AndroidManifest.xml do zdefiniowania

1014 Android 3. Tworzenie aplikacji wymagań sprzętowych i programowych naszej aplikacji. Android Market wymusi te wymagania i uniemożliwi wyświetlanie naszej aplikacji urządzeniom nieposiadającym odpowiedniej konfiguracji. Zwróćmy uwagę, że mamy tu do czynienia z innymi znacznikami niż

pliku AndroidManifest.xml. Chociaż urządzenie użytkownika może być wyposażone w aparat fotograficzny, nie jest wcale powiedziane, że użytkownik zechce przydzielić naszej aplikacji dostęp do tego aparatu. Jednocześnie zadeklarowanie uprawnienia aplikacji do korzystania z aparatu wcale nie musi oznaczać, że obecność tej funkcji jest wymagana przez aplikację. W większości przypadków będziemy umieszczać obydwa znaczniki w pliku AndroidManifest.xml, aby określić wymóg obecności aparatu fotograficznego oraz uprawnienie korzystania z aparatu w razie potrzeby. Jednak nie wszystkie funkcje wymagają uprawnień, więc w naszym najlepszym interesie leży zdefiniowanie opcji potrzebnych naszej aplikacji. Istnieje jeszcze jedna zasadnicza różnica pomiędzy węzłami a . Za pomocą tego drugiego znacznika możemy określać, czy dana funkcja jest konieczna do działania aplikacji lub czy nasz program może się bez niej obyć. To znaczy, że mamy do dyspozycji atrybut android:required, któremu możemy przypisać wartość true lub false (domyślna jest wartość true). Na przykład program może wykorzystywać funkcje sieci Bluetooth, jeżeli jest dostępna, będzie jednak działał równie dobrze bez nich. Zatem możemy wstawić w pliku manifeście następujący wiersz:

Wewnątrz kodu aplikacji powinniśmy wywołać klasę PackageManager w celu określenia, czy technologia Bluetooth jest dostępna, czego możemy dokonać za pomocą poniższego fragmentu: boolean hasBluetooth = getPackageManager().hasSystemFeature( PackageManager.FEATURE_BLUETOOTH);

Następnie możemy wykonać odpowiednią czynność, w zależności od obecności funkcji Bluetooth. Dokumentacja Androida w tym miejscu jest dość niejednoznaczna. Jeżeli zajrzymy do informacji o znaczniku

w konsoli programisty, nie znajdziemy tak wielu funkcji jak w dokumentacji klasy PackageManager, w której zostały zdefiniowane stałe FEATURE_* dla każdej dostępnej funkcji. Znacznik jest nieco inny. Definiuje on rodzaje wymaganych elementów urządzenia, takich jak rodzaj klawiatury, ekran dotykowy czy mechanizm sterowania. Jednak w przeciwieństwie do węzła nie wprowadzamy tutaj niezależnych wyborów, lecz tworzymy kombinacje konfiguracji wymaganych przez naszą aplikację. Jeżeli na przykład nasz program wymaga obecności kontrolera ruchu w postaci podkładki kierunkowej lub manipulatora kulkowego oraz ekranu dotykowego (wykorzystującego rysik lub palec), możemy zdefiniować dwa następujące wiersze:

Lokalizacja aplikacji Jeśli aplikacja będzie wykorzystywana w innych krajach, możemy rozważyć proces jej lokalizacji. Z technicznego punktu widzenia jest to względnie prosta czynność. Znalezienie osoby odpowiedzialnej za przeprowadzenie tego procesu to zupełnie inna sprawa. Z technicznego punktu widzenia tworzymy po prostu kolejny folder w katalogu /res — na przykład /res/values-fr przechowujący francuską wersję pliku strings.xml. Następnie bierzemy plik strings.xml, tłumaczymy wartości typu string na nowy język i zachowujemy tak zmodyfikowany plik w nowym folderze

Rozdział 28 „ Wdrażanie aplikacji na rynek — Android Market i nie tylko

1015

zasobów, nie zmieniając jednocześnie nazwy tego pliku. W przypadku innych rodzajów zasobów — na przykład obiektów rysowanych lub menu — stosujemy dokładnie taką samą technikę. Obrazy i kolory mogą lepiej spełniać swoje zadanie, jeśli będą przystosowane do odmiennych krajów i kultur. Z tego właśnie powodu nie warto stosować prawdziwych nazw zasobów kolorów. W internetowej dokumentacji dotyczącej kolorów często można natrafić na taki zapis:

#f00

Oznacza on, że w takim kodzie lub innym pliku zasobów odnosimy się do koloru za pomocą jego rzeczywistej nazwy, w naszym przypadku jest to solid_red. Aby dostosować kolor do innego kraju lub kultury, najlepiej stosować nazwy kolorów typu accent_color1 lub alert_color. W Wielkiej Brytanii odpowiedniejszy może być kolor czerwony, podczas gdy w Hiszpanii swoje zadanie będzie lepiej spełniał jakiś odcień żółci. Ponieważ nazwa alert_color nie określa zastosowanego koloru, jego zmiana nie jest już tak bardzo dezorientująca. Jednocześnie możemy zaprojektować przyjemny schemat kolorystyczny, zawierający barwy bazowe i ich odcienie, mając jednocześnie pewność, że właściwe kolory zostały użyte we właściwych miejscach. W niektórych krajach opcje menu powinny zostać zmienione poprzez dodanie lub usunięcie pewnych elementów, ewentualnie można wprowadzić inną organizację menu, w zależności od miejsca stosowania aplikacji. Pliki menu są zazwyczaj przechowywane w katalogu /res/menu. Jeżeli natrafimy na taką sytuację, prawdopodobnie będzie lepiej, jeśli umieścimy wszystkie tekstowe ciągi znaków w pliku strings.xml lub innym pliku przechowywanym w podkatalogu /res/values, a następnie będziemy wszędzie odnosić się do tych zasobów za pomocą ich identyfikatorów. Zmniejszymy w ten sposób znacznie niebezpieczeństwo pominięcia tłumaczenia tekstu w jakimś zapomnianym pliku zasobów. Tłumaczenie jest wtedy zatem ograniczone do plików znajdujących się w podkatalogu /res/values.

Przygotowanie ikony aplikacji Osoby kupujące oraz użytkownicy będą wyraźnie widzieć ikonę oraz etykietę aplikacji zarówno w sklepie Android Market, jak i — po jej pobraniu — w urządzeniu. Powinniśmy poświęcić szczególną uwagę utworzeniu jak najlepszej ikony i etykiety swojej aplikacji oraz jej aktywności. W razie potrzeby możemy je również zlokalizować. Nie zapominajmy również o ewentualnym dostosowaniu rozmiarów ikon do rozmiarów ekranu. Przyglądajmy się dziełom innych wydawców, szczególnie zaś aplikacjom należącym do tej samej kategorii co nasza. Chcemy, aby nasza aplikacja była widoczna, musimy więc unikać wtapiania się w tłum. Jednocześnie musimy pamiętać, że ikona i etykieta aplikacji muszą harmonizować z ikonami innych aplikacji zainstalowanych w urządzeniu użytkownika. Użytkownik nie może zastanawiać się nad przeznaczeniem aplikacji, której ikona wskazuje zupełnie inną funkcję. Podczas tworzenia dowolnego obrazu wykorzystywanego w aplikacji, zwłaszcza jej ikony, musimy brać pod uwagę gęstość ekranu w docelowym urządzeniu. Gęstość jest definiowana jako liczba pikseli na cal. Mały ekran zazwyczaj oznacza również małą gęstość, wobec czego mniej pikseli składa się na jednostkę odległości, podczas gdy większe wyświetlacze często posiadają dużą gęstość. W przypadku ekranu posiadającego niewielką gęstość odpowiedni rozmiar ikony powinien składać się z mniejszej liczby pikseli, najczęściej o wymiarach 36×36. W przypadku wyświetlacza o większej gęstości najprawdopodobniej utworzymy ikonę o wymiarach 72×72. Ikona dla ekranu o średniej gęstości przybiera wartości 48×48, a przy bardzo dużych gęstościach może posiadać rozmiary 96×96.

1016 Android 3. Tworzenie aplikacji

Problemy związane z zarabianiem pieniędzy na aplikacjach Podczas ustalania ceny za aplikację trzeba rozważyć pewne kwestie. Czy utworzyć dwie wersje tej samej aplikacji — darmową i płatną — wymagające oddzielnej obsługi i zarządzania nimi? A może korzystamy ze wspólnego kodu bazowego i stosujemy jakąś technikę, dzięki której wiadomo, że została uiszczona opłata za program? Bez względu na zastosowane rozwiązanie, w jaki sposób chronimy aplikację przed jej kopiowaniem i instalowaniem na innych urządzeniach przez nieupoważnione do tego osoby? Z powodu ograniczonych zabezpieczeń stosowanych w telefonach oraz zdolności pewnych osób do omijania tych środków ostrożności zarządzanie niezawodnymi technologiami ochrony przed nieuprawnionym kopiowaniem jest niezwykle trudne. Jednym z rozwiązań utrzymywania pojedynczego kodu bazowego, pozwalającego na umieszczenie oddzielnych trybów (bezpłatnego i płatnego), jest wykorzystanie możliwości klasy PackageManager: this.getPackageManager().checkSignatures(mainAppPkg, keyPkg)

Metoda ta porównuje sygnatury dwóch pakietów oraz przekazuje wartość PackageManager. jeżeli obydwa pakiety istnieją i są identyczne. Nazwy pakietów muszą się różnić dla każdej aplikacji współistniejącej w sklepie Android Market, ale to nic nie szkodzi. W naszym kodzie, jeżeli chcemy zadecydować o udostępnieniu wszystkich funkcji aplikacji, możemy wywołać tę metodę i wprowadzić nazwę pakietu aplikacji głównej lub aplikacji odblokowującej. Ten drugi program ustanawiamy następnie jako płatny. Jeżeli użytkownik zakupi aplikację odblokowującą i pobierze ją na urządzenie, główna aplikacja otrzyma dopasowanie sygnatury i odblokuje dodatkowe funkcje. Nieco mniej przyjemnym rozwiązaniem wykorzystującym pojedynczy kod bazowy jest wprowadzenie systemów oznaczających kolejne wersje kodu źródłowego, które skonfigurują odpowiednie współdzielenie wspólnych elementów, oraz stworzenie skryptów tworzących darmowe i płatne wersje aplikacji.

´SIGNATURE_MATCH,

Kolejnym rozwiązaniem umożliwiającym zarabianie na aplikacjach jest wprowadzenie wewnętrznych reklam. Istnieje mnóstwo okazji, aby dołączyć reklamy do aplikacji. Dwoma powszechnie występującymi możliwościami są AdMob i AdSense. Proces polega przede wszystkim na wstawieniu ich pakietów SDK do naszej aplikacji, określeniu odpowiedniego miejsca i czasu, w którym będą wyświetlane reklamy i dodaniu uprawnienia INTERNET do programu (dzięki czemu wyświetlane reklamy będą pobierane z internetu). Na koniec będziemy otrzymywać pieniądze za każde kliknięcie reklamy. Sama aplikacja może być bezpłatna, dzięki czemu łatwiej będzie ją umieścić w serwisie Android Market, a do tego nie musimy się tak bardzo przejmować kwestią piractwa. Wielu programistów przyznaje, że zarabia w ten sposób przyzwoite pieniądze. Kolejną nową funkcją jest wprowadzona w lutym 2011 roku waluta klienta (ang. Buyer’s Currency). Przedtem klienci — użytkownicy — musieli płacić w walucie sprzedawcy, co mogło stanowić problem dla osób mających kłopoty z wymianą waluty sprzedawcy na swoją własną. Oznaczało to również, że sprzedawca mógł tak naprawdę ustalić tylko jedną cenę dla użytkowników na całym świecie. Teraz, gdy sprzedawca może ustalić cenę dla danego kraju, może ją nie tylko podnosić lub obniżać w zależności od rejonu geograficznego, lecz także komfort użytkownika staje się wyraźnie większy.

Kierowanie użytkowników z powrotem do sklepu W systemie Android wprowadzono nowy schemat identyfikatorów URI, ułatwiający wyszukiwanie aplikacji w sklepie Android Market — market://. Jeśli na przykład chcemy skierować użytkowników do sklepu w celu znalezienia potrzebnego składnika lub dokupienia dodatkowej

Rozdział 28 „ Wdrażanie aplikacji na rynek — Android Market i nie tylko

1017

aplikacji odblokowującej nowe funkcje naszej aplikacji, powinniśmy wprowadzić następujący kod, w którym w miejsce MY_PACKAGE_NAME wprowadzamy nazwę naszego pakietu: Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://search?q=pname:MY_PACKAGE_NAME")); startActivity(intent);

Za pomocą tego kodu zostanie uruchomiona aplikacja Market, która wyświetli użytkownikowi nazwę tego pakietu. Użytkownik może wtedy pobrać lub zakupić aplikację. Zwróćmy uwagę, że powyższy schemat nie działa w standardowej przeglądarce WWW. Możemy wyszukiwać za pomocą nazwy pakietu (pname), a także szukać wydawcy za pomocą wyrażenia market://search?q=pub:\"Fname Lname\" lub przy użyciu dowolnego pola publicznego sklepu Android Market (nazwa aplikacji, nazwa wydawcy oraz opis aplikacji), wpisując market:// ´search?q=

. Jeżeli Czytelnik połączy zdobytą wiedzę z technikami omówionymi w poprzednim podrozdziale, powinien umieć napisać kod, który będzie próbował odnaleźć pakiet odblokowujący w danym urządzeniu. Jeśli pakiet nie zostanie znaleziony, możemy wyświetlić monit i zapytać użytkownika, czy nie chce go pobrać z internetu. W przypadku odpowiedzi twierdzącej aplikacja wywoła intencję otwierającą usługę Android Market i automatycznie uruchamiającą stronę, na której będzie można pobrać lub zakupić aplikację odblokowującą.

Usługa licencyjna systemu Android Architektura aplikacji tworzonych dla systemu Android powoduje, niestety, że stanowią one łatwy cel dla piratów. Istnieje możliwość kopiowania tych aplikacji oraz rozsyłania tych kopii do innych urządzeń. W jaki sposób możemy więc sprawić, aby użytkownicy, którzy nie zakupili naszej aplikacji, nie mogli jej uruchomić? Nasze wymagania spełnia utworzona przez firmę Google biblioteka LVL (ang. License Verification Library — biblioteka weryfikująca licencje). Jeżeli dana aplikacja została pobrana za pomocą usługi Android Market, musi istnieć kopia aplikacji Android Market w tym urządzeniu. Dodatkowo aplikacja ta musiała wprowadzić uprawnienia pozwalające na odczytywanie wartości przechowywanych w urządzeniu, takich jak nazwa konta Google użytkownika, numer IMSI oraz inne informacje. Począwszy od wersji 1.5 Androida, aplikacja Android Market została zmodyfikowana w taki sposób, że reaguje na żądania weryfikacji licencji pochodzące od aplikacji. W tym celu wywołujemy bibliotekę LVL z poziomu aplikacji, biblioteka ta nawiązuje łączność z aplikacją Android Market, z kolei program Android Market łączy się z serwerami firmy Google i nasza aplikacja otrzymuje odpowiedź wskazującą, czy użytkownik urządzenia posiada licencję na korzystanie z naszego kodu. Posiadamy kontrolę nad ustawieniami definiującymi zachowanie aplikacji w przypadku braku dostępu do sieci. Pełny opis implementacji biblioteki LVL znajdziemy pod adresem http://developer.android.com/guide/publishing/licensing.html. Powinniśmy mieć jednak świadomość, że mechanizm LVL jest narażony na ataki hakerów. Jeżeli ktoś wie, gdzie znaleźć wartość przekazywaną w wyniku wywołania biblioteki LVL, i jeśli uzyska dostęp do naszego pliku .apk, może rozłożyć aplikację na czynniki pierwsze i odpowiednio ją zmodyfikować. Jeżeli zastosujemy oczywiste rozwiązanie polegające na wykorzystaniu instrukcji switch po otrzymaniu odpowiedzi z mechanizmu LVL, które służy do określenia zachowania aplikacji w zależności od otrzymanej wartości, haker może po prostu wymusić przekazanie tej pożądanej wartości i w tym momencie zabezpieczenia takiej aplikacji przestają istnieć. Z tego właśnie powodu twórcy Androida zalecają jak największe zagmatwanie kodu,

1018 Android 3. Tworzenie aplikacji aby ukryć logikę odpowiedzialną za sprawdzanie wartości zwracanej przez bibliotekę LVL. Możemy sobie wyobrazić, że jest to dość skomplikowana procedura. Wraz z wersją 2.3 Androida firma Google wprowadziła pewną formę obsługi takiego gmatwania kodu w postaci funkcji ProGuard. Jeżeli ustalimy docelową wersję systemu na co najmniej 2.3, nasza aplikacja automatycznie otrzyma plik proguard.cfg. Poprzez skonfigurowanie funkcji ProGuard w tym pliku możemy zmusić narzędzia ADT do gmatwania kodu w momencie kompilowania wersji rynkowej pliku .apk. Jeżeli wolimy tworzyć aplikacje za pomocą narzędzia ant, możemy je również skonfigurować w taki sposób, aby została w nim wykorzystana funkcja ProGuard do gmatwania kodu. Aby włączyć tę możliwość, musimy wprowadzić właściwość proguard.config w pliku default.properties, który umieszczamy w tej samej lokacji co plik proguard.cfg. W trakcie przeprowadzania procesu gmatwania funkcja ProGuard wygeneruje plik mapping.txt wraz z plikiem .apk. Musimy pozostawić ten plik, gdyż jest on niezbędny do odwrócenia procesu gmatwania stosu w aplikacji.

Przygotowanie pliku .apk do wysłania Aby przygotować swoją aplikację do wysłania — to znaczy utworzyć w tym celu plik .apk — trzeba postąpić zgodnie z poniższym algorytmem (został on szczegółowo omówiony w rozdziale 10.): 1. Jeśli jeszcze tego nie zrobiłeś, utwórz certyfikat produktu, za pomocą którego podpiszesz swoją aplikację. 2. Jeżeli aplikacja wykorzystuje mapy, zamień klucz API MAP w pliku AndroidManifest.xml na klucz API MAP produktu. Jeśli tego nie zrobisz, użytkownicy nie będą widzieli map. 3. Eksportuj aplikację poprzez kliknięcie prawym przyciskiem myszy nazwy projektu w oknie Package explorer środowiska Eclipse, wybranie opcji Android Tools/Export Unsigned Application Package oraz dobranie odpowiedniej nazwy pliku. Warto nadać temu plikowi tymczasową nazwę, ponieważ po uruchomieniu aplikacji zipalign w punkcie 5. będzie trzeba wprowadzić nazwę pliku wyjściowego, która będzie stanowić nazwę naszego pliku .apk. 4. Uruchom aplikację jarsigner wobec naszego nowego pliku .apk, aby podpisać go za pomocą utworzonego w punkcie 1. certyfikatu produktu. 5. Uruchom aplikację zipalign wobec pliku .apk, aby dopasować nieskompresowane dane do granic pamięci, dzięki czemu uzyskasz lepszą wydajność po uruchomieniu aplikacji. To właśnie teraz wprowadza się ostateczną nazwę pliku .apk naszej aplikacji. 6. Obecnie w środowisku Eclipse możesz wykorzystać opcję Export Signed Application Package, wykorzystującą kreator do przeprowadzenia etapów 3., 4. i 5.

Wysyłanie aplikacji Wysyłanie aplikacji jest prostym procesem, wymagającym jednak nieco przygotowań. Przed rozpoczęciem wysyłania musimy ustanowić kilka parametrów oraz podjąć pewne decyzje. Niniejszy podrozdział został poświęcony omówieniu tych przygotowań i decyzji. Gdy już wszystko będzie gotowe, przejdziemy do konsoli programisty i wybierzemy opcję Upload Application. Zostaniemy poproszeni o wprowadzenie wielu informacji dotyczących naszej aplikacji, usługa Market przebada aplikację oraz dostarczone dane i w końcu nasz program zostanie przygotowany do publikacji w sklepie Android Market.

Rozdział 28 „ Wdrażanie aplikacji na rynek — Android Market i nie tylko

1019

W poprzednim podrozdziale omówiliśmy proces przygotowania pliku .apk do wysłania. Przyciągnięcie uwagi klientów wymaga z naszej strony szczypty marketingu. Musimy stworzyć dobry opis aplikacji oraz jej przeznaczenia, konieczne też będzie wykonanie zrzutów ekranu, aby użytkownicy wiedzieli, że nie kupują kota w worku. Jednym z pierwszych elementów, o jakie zostaniemy poproszeni podczas wysyłania aplikacji, są zrzuty ekranu. Najprostszym sposobem ich uzyskania jest zastosowanie narzędzia DDMS. Uruchamiamy środowisko Eclipse, następnie włączamy aplikację na emulatorze lub w rzeczywistym urządzeniu i przełączamy perspektywę na widoki DDMS i Device. Wewnątrz widoku Device wybieramy urządzenie, na którym uruchomiliśmy aplikację, i klikamy przycisk Screen Capture (symbolizuje go ikona małego obrazu umieszczona w prawym górnym rogu ekranu) lub wybieramy go z menu View. Jeżeli pojawi się taka możliwość, wybierzmy 24-bitowy kolor. Android Market przekonwertuje zrzuty ekranu na skompresowane pliki JPEG; początkowa wartość 24 bitów przyniesie lepsze rezultaty niż początkowa wartość 8 bitów. Wybierzmy takie zrzuty, które przy ukazywaniu oryginalności aplikacji prezentują jednocześnie jej funkcje. Musimy wprowadzić co najmniej dwa zrzuty ekranu, maksymalnie zaś możemy zamieścić ich osiem. Następną kwestią jest wygenerowana ikona aplikacji w wysokiej rozdzielczości. Możemy w tym celu wykorzystać projekt tej samej ikony, która zostanie zastosowana w urządzeniu, ale musi ona posiadać wymiary 512×512 pikseli. Jest to jeden z wymogów usługi Android Market. Możemy umieścić również grafikę promującą, jej rozmiar będzie jednak mniejszy od zrzutu ekranu. Chociaż taka grafika stanowi jedynie dodatek, warto ją również opublikować. Nigdy nie wiadomo, kiedy zostanie wyświetlona; bez niej nie będziemy pewni, co (jeżeli co*kolwiek) pojawi się na jej miejscu. Jednym z miejsc, w którym pojawia się grafika promocyjna, jest górna część ekranu wyświetlającego szczegóły aplikacji w sklepie Android Market. Kolejnym opcjonalnym rodzajem obrazu jest grafika wymieniająca cechy aplikacji, o rozmiarze 1024×500 pikseli. Grafika ta będzie wykorzystywana w sekcji Polecane serwisu Android Market, więc powinniśmy się naprawdę postarać, aby ten obraz wyglądał jak najlepiej. Ostatnim elementem graficznym związanym z naszą aplikacją jest opcjonalny plik wideo, który możemy zamieścić w serwisie YouTube. Adres do tego pliku możemy umieścić w opisie aplikacji. Android Market poprosi następnie o informację tekstową dotyczącą aplikacji, która będzie widoczna dla klientów, włącznie z tytułem, opisem oraz tekstem promującym. Możliwość wprowadzenia tekstu promującego stanie się dostępna jedynie po umieszczeniu grafiki promującej. Możemy opublikować tekst w wielu językach, gdyż nasza aplikacja będzie dostępna na całym świecie. Grafika promująca może być umieszczona w sklepie Android Market wyłącznie jednorazowo, zatem jeśli zrzuty ekranu wyglądają inaczej w różnych ustawieniach regionalnych, powinniśmy rozważyć umieszczenie ich w innym miejscu dostępnym dla klientów, na przykład na stronie domowej. Takie podejście może w przyszłości ulec zmianie. Jeśli napisaliśmy własną wersję umowy EULA, powinniśmy w opisie zamieścić odnośnik do niej, dzięki czemu użytkownicy zapoznają się z jej treścią przed pobraniem aplikacji. Należy wziąć pod uwagę, że klienci prawdopodobnie będą chcieli wykorzystać funkcję wyszukiwania aplikacji, zatem najlepiej byłoby umieścić w tekście odpowiednie słowa kluczowe, dzięki czemu znacząco wzrośnie prawdopodobieństwo natrafienia na naszą aplikację przez osoby szukające zapewnianych przez nią funkcji. W końcu warto również umieścić krótki komentarz wraz z adresem e-mail na wypadek pojawienia się problemów z aplikacją. Bez tej prostej zachęty użytkownicy mogą częściej wystawiać negatywne opinie, a taka negatywna opinia naprawdę ogranicza możliwość rozwiązania problemu w porównaniu do wymiany informacji z użytkownikiem, który natrafił na jakiś błąd.

1020 Android 3. Tworzenie aplikacji Jedną z wad omówionego wcześniej mechanizmu wsparcia technicznego jest brak rozróżnienia pomiędzy wersjami aplikacji. Jeżeli wersja 1. oprogramowania otrzymała negatywne opinie, a my wydaliśmy wersję 2. pozbawioną wszystkich usterek poprzedniczki, recenzje dotyczące wersji 1. nie znikają, a klienci nie wiedzą, której wersji dotyczą te opinie. Po wydaniu zaktualizowanej aplikacji jej ocena (liczba gwiazdek) również nie zostaje wyzerowana. Częściowo z tego powodu firma Google zaczęła implementować pole tekstowe Najnowsze zmiany, gdzie możemy umieścić listę zmian wprowadzonych w nowej wersji aplikacji. To właśnie tutaj możemy stwierdzić, że jakiś błąd został naprawiony lub wymienić nowe funkcje. Dostępne jest także oddzielne pole na tekst promujący, pozwalające na wstawienie 80 znaków. Po wyświetleniu naszej aplikacji na samej górze listy w sklepie Android Market tam właśnie są umieszczone grafika promująca oraz tekst promujący. Wypełnienie tych pól informacjami jest naprawdę dobrym pomysłem. Jednym z obowiązków wydawcy aplikacji jest ujawnienie w opisie tej aplikacji wymaganych przez nią uprawnień. Mamy na myśli te same uprawnienia, które są definiowane za pomocą znaczników

w pliku AndroidManifest.xml. Kiedy użytkownik pobiera aplikację na urządzenie, system sprawdza plik AndroidManifest.xml i przed zakończeniem instalacji pyta użytkownika o wszystkie zamieszczone w nim uprawnienia. Równie dobrze możemy umieścić je w opisie aplikacji. W przeciwnym wypadku ryzykujemy otrzymanie negatywnych opinii od użytkowników zaskoczonych faktem, że aplikacja wymaga uprawnień, których nie zamierzali jej przyznać. Nie wspominamy nawet o zwrotach kosztów, które zaniżają ogólną ocenę dewelopera. Podobnie jak w przypadku uprawnień, jeśli aplikacja wymaga określonego typu ekranu, aparatu fotograficznego lub jakiejś innej funkcji urządzenia, odpowiednie informacje powinny zostać zamieszczone w opisie programu. Najlepszym rozwiązaniem jest nie tylko zamieszczenie wymaganych przez aplikację uprawnień i funkcji, lecz także opis sposobu ich wykorzystania. Powinniśmy z góry odpowiedzieć na pytanie, dlaczego taka aplikacja wymaga obecności funkcji X. W trakcie wysyłania aplikacji musimy wybrać jej rodzaj i określić kategorię. Ponieważ z upływem czasu wartości te ulegają zmianom, nie będziemy ich wymieniać, wystarczy przejść do ekranu Upload Application, żeby ujrzeć dostępne możliwości. Następnym etapem jest ustalenie ceny aplikacji. Domyślnie aplikacja jest darmowa, a żeby to zmienić, trzeba posiadać skonfigurowane konto handlowe w usłudze Google Checkout. Wybór odpowiedniej ceny dla aplikacji wcale nie jest taki łatwy, chyba że posiadamy wyjątkowo rozwinięty zmysł marketingowca, a nawet wtedy nietrudno o pomyłkę. Zbyt wysokie opłaty mogą zniechęcać użytkowników, poza tym zwiększają odczuwalne dla wydawcy skutki zwracania kosztów ludziom, którzy uznali aplikację za niewartą swojej ceny. Z kolei zbyt niskie ceny mogą również zniechęcić ludzi, którzy uznają, że taka aplikacja jest niskobudżetowa. Android Market posiada opcję włączenia ochrony przed kopiowaniem po wysłaniu aplikacji. Serwis ten automatycznie wyposaży naszą aplikację w ten mechanizm, powinniśmy jednak pamiętać, że rozwiązanie to nieco bardziej obciąża pamięć urządzenia. Nie jest ono również niezawodne i nie gwarantuje całkowitego zabezpieczenia aplikacji. Ponieważ funkcja ochrony przez kopiowaniem znajduje się na etapie wycofywania, powinniśmy wziąć pod uwagę alternatywne lub dodatkowe sposoby ochrony programu, na przykład opisaną wcześniej usługę licencyjną systemu Android. Pod koniec 2010 roku firma Google wprowadziła schemat oceniania aplikacji. Jego zadaniem jest określenie przedziału wiekowego grupy docelowej użytkowników. Niestety, połowa grup wiekowych dotyczy nastolatków. Do wyboru mamy oceny: Dla wszystkich, Niska dojrzałość,

Rozdział 28 „ Wdrażanie aplikacji na rynek — Android Market i nie tylko

1021

Średnia dojrzałość i Wysoka dojrzałość. Wybór odpowiedniej grupy wiekowej zależy od treści aplikacji oraz od ilości tej treści. Firma Google zdefiniowała zasady związane z położeniem geograficznym oraz umieszczaniem lub publikowaniem lokacji. Najlepiej samemu przejrzeć te zasady, znajdziemy je pod adresem www.google.com/support/androidmarket/bin/answer.py?hl= en&answer=188189. Jedną z ostatnich decyzji, jakie należy podjąć, jest wybór regionów geograficznych oraz operatorów telefonii komórkowej, dla których nasza aplikacja będzie widoczna. Poprzez wybór opcji All aplikacja będzie dostępna na całym świecie. Czasami jednak należy ograniczyć dystrybucję do danego regionu geograficznego lub operatora. W zależności od funkcji oferowanych przez aplikację wymagane jest nieraz ograniczenie liczby krajów, w których aplikacja będzie dopuszczona do obrotu, ze względu na konieczność przestrzegania prawa eksportowego Stanów Zjednoczonych. Ograniczenie dystrybucji aplikacji do określonych operatorów jest konieczne, gdy wystąpią problemy z kompatybilnością z urządzeniami lub niezgodność z zasadami danego operatora. Aby przejrzeć listę dostępnych operatorów, klikamy w konsoli programisty nazwę wybranego kraju, dzięki czemu zostanie wyświetlony spis dostępnych operatorów w danym państwie. Zaznaczenie opcji All spowoduje również, że dana aplikacja będzie dostępna dla wszelkich kolejnych państw i operatorów, którym kiedyś firma Google udostępni serwis Android Market — w tym celu nie będą potrzebne żadne działania programisty. Chociaż profil programisty zawiera informacje kontaktowe, możemy wprowadzić inne dane podczas wysyłania każdej aplikacji. Usługa Market monituje o wprowadzenie adresu strony WWW, adresu e-mail oraz numeru telefonu, służących jako informacje kontaktowe związane z daną aplikacją. W celu umożliwienia obsługi klientów musimy wypełnić przynajmniej jedno z wymienionych pól, nie jest konieczne jednak wprowadzenie danych do wszystkich trzech elementów. Podawanie tutaj prywatnego adresu e-mail nie jest najlepszym pomysłem, tak samo jak nie chcielibyśmy najprawdopodobniej podawać tutaj prywatnego numeru telefonu. Gdy będziemy zarabiać miliony dolarów na sprzedaży naszej aplikacji, raczej zatrudnimy kogoś odbierającego telefony i odpowiadającego na e-maile od użytkowników. Jeżeli od razu założymy adres e-mail do celów obsługi pomocy technicznej, nie będziemy mieli później problemu z rozdzieleniem poczty osobistej i wiadomości od użytkowników. Po podjęciu wszystkich omówionych decyzji musimy naszej aplikacji nadać atest przestrzegania polityki treści w usłudze Android Market dla programistów (nie jest zbyt rygorystyczna), a także drugi atest — umożliwiający eksportowanie programu poza granice Stanów Zjednoczonych. Aplikacje udostępniane poprzez serwis Android Market podlegają prawu eksportowemu Stanów Zjednoczonych, ponieważ serwery Google są umieszczone w tym kraju. Dotyczy to nawet aplikacji utworzonych w innym państwie oraz sytuacji, kiedy zarówno programista, jak i jego użytkownicy znajdują się poza USA. Nie zapominajmy, że zawsze możemy dystrybuować aplikację innymi kanałami. Gdy już wprowadzimy wszystkie niezbędne informacje i wyślemy zdjęcie, możemy w końcu wcisnąć przycisk Save. Nasza aplikacja zostanie wtedy przygotowana do wysłania w świat. W końcu możemy opublikować naszą aplikację poprzez wciśnięcie przycisku Publish. Android Market sprawdzi publikowany program, zwłaszcza pod kątem daty wygaśnięcia certyfikatu aplikacji. Jeżeli cały proces przebiegnie pomyślnie, nasz kod stanie się dostępny do pobrania przez innych użytkowników. Gratulacje!

1022 Android 3. Tworzenie aplikacji

Korzystanie ze sklepu Android Market Z poziomu urządzeń Android Market jest dostępny już od pewnego czasu, natomiast mniej więcej od lutego 2011 roku można również korzystać z niego poprzez internet. Wydawcy nie mają żadnej kontroli nad działaniem sklepu, mogą co najwyżej wstawić ciekawy tekst i zrzuty ekranu do opisu umieszczanej aplikacji. Zatem pod tym względem o komfort użytkownika musi zadbać sama firma Google. Za pomocą urządzenia użytkownik może przeszukiwać bazę danych przy zastosowaniu słowa kluczowego, przeglądać najczęściej pobierane aplikacje (płatne oraz darmowe), zalecane programy lub nowości oraz całe kategorie. Po znalezieniu odpowiedniej aplikacji użytkownik może ją od razu zaznaczyć, co spowoduje wyświetlenie ekranu szczegółów programu, na którym można wybrać opcję jego instalacji lub kupna. Wybór opcji kupna uruchomi usługę Google Checkout, gdzie zostanie przeprowadzona finansowa część transakcji. Pobrana aplikacja pojawia się na urządzeniu użytkownika wśród pozostałych programów. Interfejs użytkownika w wersji internetowej serwisu Android Market (http://market.android.com) wygląda praktycznie tak samo jak w urządzeniach, został jednak dostosowany do rozmiarów monitora. Jedna z zasadniczych różnic polega na konieczności wprowadzenia nazwy użytkownika konta Google, aby móc przeglądać wersję sieciową serwisu Android Market. W ten sposób firma Google łączy działania użytkownika w internetowej wersji serwisu Android Market z działaniami użytkownika na urządzeniu. Oznacza to, że po pierwsze, kiedy użytkownik korzysta z wersji sieciowej serwisu, Android Market ma dostęp do informacji, które aplikacje są zainstalowane w należącym do użytkownika urządzeniu. Po drugie, w trakcie zakupów pobierana aplikacja będzie wysyłana wprost do urządzenia użytkownika, nawet jeśli została zakupiona poprzez stację roboczą. Witryna Android Market daje możliwość przeglądania pobranych aplikacji w katalogu Moje zamówienia. Można tu oglądać wszystkie zarówno zainstalowane aplikacje, jak i zakupione programy, nawet po ich usunięciu (najczęściej zostają usunięte wyłącznie z powodu braku miejsca w urządzeniu). Oznacza to, że użytkownik może usunąć płatną aplikację i ponownie ją zainstalować w innym terminie bez ponoszenia dodatkowych kosztów. Oczywiście, po wybraniu opcji zwrotu kosztów dana aplikacji nie będzie wyświetlana w katalogu Moje zamówienia. W folderze tym nie będą również pokazywane darmowe aplikacje po ich wykasowaniu. Lista aplikacji umieszczonych w tym katalogu jest powiązana z kontem Google obsługiwanym przez urządzenie. Oznacza to, że możemy zmienić urządzenie bez obaw o utratę zakupionych aplikacji. Pamiętajmy jednak o tym, że jeśli posiadamy kilka tożsamości na serwerach Google, zakupione wcześniej aplikacje możemy pobrać jedynie z konta, na którym za nie zapłaciliśmy. Podczas przeglądania aplikacji w katalogu Moje zamówienia wszelkie nowe wersje programów będą zaznaczone oraz gotowe do uaktualnienia. Android Market filtruje aplikacje dostępne dla określonych użytkowników. Proces ten jest przeprowadzany na wiele sposobów. W niektórych krajach dostępne są wyłącznie bezpłatne wersje programów z powodu różnorakich wymogów prawa handlowego, które nie odpowiadają firmie Google w danym państwie. Firma Google bardzo stara się przezwyciężyć te przeszkody, aby płatne aplikacje były dostępne na całym świecie. Do tego czasu użytkownicy w niektórych krajach mogą cieszyć się jedynie darmowymi aplikacjami. Osoby posiadające urządzenia pracujące pod kontrolą starszej wersji systemu Android nie mają dostępu do aplikacji obsługiwanych przez nowsze wersje zestawu Android SDK. Użytkownicy korzystający z urządzeń niespełniających wymagań sprzętowych danej aplikacji (definiowanych w znacznikach

pliku AndroidManifest.xml) również nie będą widzieć takich programów. Na przykład aplikacje nieobsługujące małych wyświetlaczy nie będą widziane w sklepie Android Market przez

Rozdział 28 „ Wdrażanie aplikacji na rynek — Android Market i nie tylko

1023

użytkowników posiadających urządzenia z takimi właśnie ekranami. Taka filtracja została wprowadzona głównie po to, aby uchronić użytkowników przed pobraniem aplikacji, która nie będzie działać w ich telefonach. Jeżeli kupujemy aplikację w innych krajach, na etapie transakcji może nastąpić przewalutowanie, co zazwyczaj oznacza dodatkową opłatę, chyba że sprzedawca ustalił cenę w naszej walucie. Aplikacje są w rzeczywistości kupowane z kraju sprzedawcy za pośrednictwem usługi Google Checkout. Sklep Android Market wyświetla przybliżoną kwotę, lecz w rzeczywistości opłaty mogą być nieco inne w zależności od momentu przeprowadzenia transakcji oraz użytych procesorów płatności. Osoby kupujące mogą zauważyć, że ich konto zostaje obciążone symboliczną opłatą (na przykład 1 dolara) w czasie przeprowadzania transakcji. Firma Google upewnia się w ten sposób, że wprowadzone informacje o płatności są poprawne, a wspomniana opłata w rzeczywistości nie zostanie uiszczona. Istnieje w internecie kilka stron, które stanowią odzwierciedlenie witryny Android Market. Użytkownicy mogą tam wyszukiwać aplikacje, przeglądać kategorie oraz czytać informacje na temat programów bez konieczności posiadania urządzenia. Rozwiązanie to działa na zasadzie filtrowania przez Android Market konfiguracji urządzenia oraz regionu geograficznego, w którym znajduje się użytkownik. Nie ma jednak możliwości pobrania w ten sposób aplikacji na urządzenie. Przykładami takich lustrzanych witryn są www.androlib.com, www.androidzoom.com oraz www.cyrket.com.

Alternatywy dla serwisu Android Market Serwis Android Market nie jest jedynym graczem na rynku. Nie istnieje żaden przymus korzystania z tej usługi. Powinniśmy brać pod uwagę również inne kanały dystrybucji, nie tylko po to, aby udostępniać aplikacje użytkownikom w niektórych krajach, lecz również po to, aby korzystać z innych metod pobierania opłat oraz możliwości zarabiania. Istnieją sklepy z aplikacjami zupełnie niezależne od witryny Android Market. Przykładami mogą być www.andappstore.com, slideme.org, www.getjar.com i www.androidgear.com. Serwis Amazon również uruchamia własną wersję sklepu Android App Store. Możemy na tych stronach wyszukiwać, przeglądać, czytać informacje o aplikacjach, a także pobierać je zarówno z poziomu telefonu, jak i przeglądarki. Witryny te nie muszą żądać przestrzegania zasad firmy Google, wliczając w to również opłaty transakcyjne oraz formy płatności. Te oddzielne sklepy akceptują takie procesory płatności, jak na przykład PayPal. Zostaje w nich również pominięte ograniczenie dotyczące rejonu geograficznego i konfiguracji sprzętowej. Niektóre ze sklepów oferują instalację klienta Android, inne zaś dokonują jego wstępnej instalacji w urządzeniu. Użytkownicy mogą po prostu otworzyć przeglądarkę WWW w urządzeniu i wyszukać w internecie daną aplikację; po jej zapisaniu na karcie pamięci system będzie posiadał informacje niezbędne do jej zainstalowania. Pobrany plik .apk jest traktowany jak aplikacja systemu Android. Jeżeli użytkownik kliknie w przeglądarce w historii pobranych plików nazwę takiego pliku (nie należy mylić tego mechanizmu z omówionym wcześniej katalogiem Moje zamówienia), zostanie zapytany, czy chce zainstalować pobraną aplikację. Taka swoboda oznacza, że programista może ustanawiać własne metody pobierania aplikacji przez użytkowników, nawet z domowej strony WWW, oraz zastosować wybrane przez siebie metody płatności. Nadal jednak trzeba wypełniać zobowiązania podatkowe wobec urzędu skarbowego. Chociaż takie alternatywne metody dystrybuowania aplikacji nie są ograniczane przez zasady firmy Google, nie oferują również tak wysokiego poziomu ochrony kupca, jak ma to miejsce

1024 Android 3. Tworzenie aplikacji w sklepie Android Market. Istnieje możliwość, że użytkownik zakupi z takiego źródła aplikację, która nie będzie działać na jego urządzeniu. Osoba kupująca musi również zapewnić sobie tworzenie kopii zapasowych aplikacji na wypadek jej przypadkowej utraty lub w momencie zmiany urządzenia. Te inne kanały dystrybucji pozwalają nam zarabiać na sprzedaży każdego egzemplarza aplikacji, podobnie jak ma to miejsce w przypadku usługi Android Market. Możemy także implementować w ich obszarze alternatywne mechanizmy uiszczania zapłaty. Oczywiście, możemy również wprowadzać reklamy do aplikacji i zarabiać w ten sposób. Nie ma także przeciwwskazań co do umieszczania tych mechanizmów wewnątrz aplikacji. Na przykład serwis PayPal zawiera odpowiednią bibliotekę dostosowaną do systemu Android (warto zajrzeć na stronę http://www.x.com). W ten sposób umożliwiamy użytkownikom zakup dodatków, nowej treści lub płatnych aktualizacji wprost z poziomu aplikacji. W ten sam sposób można uzyskiwać datki. Możemy nawet zaimplementować mobilny sklepik, w którym byłby wykorzystywany mechanizm PayPal. Pamiętajmy, że firma Google nie zabrania wydawcom jednoczesnej sprzedaży aplikacji w wielu różnych sklepach i w usłudze Android Market. Zatem w celu zwiększenia efektywności powinniśmy wziąć pod uwagę wszystkie możliwości.

Odnośniki Poniżej prezentujemy odnośniki do materiałów, które mogą pomóc w zrozumieniu koncepcji omówionych w niniejszym rozdziale: „ http://developer.android.com/guide/topics/manifest/manifest-intro.html — jest to strona poradnika programisty poświęcona plikowi AndroidManifest.xml, zawierająca opisy zastosowania znaczników

, oraz . „ http://developer.android.com/guide/practices/screens_support.html — strona poradnika programisty zawierająca informacje o tak zwanej obsłudze wielu ekranów. Znajdziemy tu wiele przydatnych informacji na temat korzystania z różnorodnych rozmiarów i gęstości wyświetlaczy. „ http://developer.android.com/guide/practices/ui_guidelines/icon_design.html — strona poradnika programisty zawierająca wskazówki dotyczące projektowania ikon. Umieszczono tu interesujące informacje na temat tworzenia ikon wpływających na jakość naszej aplikacji. „ http://android-developers.blogspot.com/2010/09/securing-android-lvl-applications.html oraz http://android-developers.blogspot.com/2010/09/proguard-android-and-licensing-server.html — dwa wpisy dotyczące metod stosowania biblioteki LVL w celu zapobieżenia piractwu. „ http://developer.android.com/guide/market/billing/index.html — dokumentacja dotycząca wbudowanego modułu pobierania opłat.

Podsumowanie Teraz możemy już podbić cały świat naszymi aplikacjami utworzonymi dla systemu Android! Pokazaliśmy, w jaki sposób należy przygotować siebie oraz swoją aplikację, jak należy ją opublikować oraz umożliwić użytkownikom jej wyszukanie, pobranie i użytkowanie.

R OZDZIAŁ

29 Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

Aż do teraz zajmowaliśmy się mechanizmami wspólnymi dla wszystkich wersji systemu Android. Zdumiewające, że minęły zaledwie dwa lata od zaprezentowania Androida w urządzeniach dostępnych na rynku, a już nastał świt nowej ery urządzeń wykorzystujących ten system — tabletów. Interfejs użytkownika dostępny w wersji 3.0 Androida został od podstaw zaprojektowany na potrzeby tabletów. Na szczęście nie oznacza to wcale, że musimy zignorować całą dotychczas zdobytą wiedzę i rozpocząć cały proces nauki od początku. W rzeczywistości wszystko, czego się do tej pory dowiedzieliśmy, okaże się przydatne w procesie pisania aplikacji przeznaczonych dla tabletów. Wraz z wersją 3.0 Androida zostały zaprezentowane nowe koncepcje oraz funkcje, których opanowanie jest niezbędne do posługiwania się bardzo dużymi (xlarge) ekranami tabletów. Większość aplikacji napisanych dla wcześniejszych wersji systemu będzie działała na tabletach, jednak mogą się pojawić kłopoty z ich optymalizacją. Jest to pierwszy rozdział tej książki, w którym zajmiemy się objaśnieniem nowych pojęć i funkcji. Jedną z nowych rdzennych klas systemu Android 3.0 jest klasa Fragment, zawierająca kilka klas potomnych. W tym rozdziale zapoznamy się z koncepcją fragmentu, jego budową oraz powiązaniami z architekturą aplikacji, a także sposobami jego wykorzystania. Dzięki fragmentom możemy przeprowadzać teraz wiele czynności, które wcześniej były bardzo trudne do zaimplementowania. Interesujący jest również fakt, że fragmenty mogą zostać wykorzystane w aplikacjach dla starszych wersji Androida, ponieważ firma Google wydała zestaw SDK zawierający architekturę fragmentów działającą z tymi systemami. Zatem nawet jeśli nie jesteśmy zainteresowani tworzeniem aplikacji dla tabletów, możemy stwierdzić, że fragmenty ułatwią nam życie nawet w urządzeniach, które nie są wyposażone w ekran o wysokiej rozdzielczości. Zacznijmy od koncepcji fragmentów.

1026 Android 3. Tworzenie aplikacji

Czym jest fragment? W pierwszym podrozdziale wyjaśnimy, czym są fragmenty i do czego służą. Najpierw jednak Czytelnik powinien przyswoić sobie odpowiednie podstawy, aby zrozumieć, po co w ogóle została zaprojektowana koncepcja fragmentów. Jak już powiedzieliśmy, aplikacje systemu Android wykorzystują aktywności w urządzeniach wyposażonych w niewielkie ekrany do prezentowania danych oraz różnorodnych funkcji użytkownikowi, a każda taka aktywność posiada w miarę proste, wyraźnie zdefiniowane przeznaczenie. Przykładowo za pomocą aktywności możemy wyświetlać listę kontaktów umieszczonych w książce adresowej. Inna aktywność może pozwalać na pisanie wiadomości e-mail. Aplikacja składa się z zestawu tego typu aktywności, zgrupowanych w większe jednostki w celu spełnienia bardziej złożonego zadania, na przykład zarządzania kontem pocztowym poprzez odczytywanie i wysyłanie wiadomości. Jest to dobre rozwiązanie w przypadku urządzeń wyposażonych w niewielkie gabarytowo ekrany, jeżeli jednak mamy do czynienia z bardzo dużym wyświetlaczem (co najmniej 10 cali), do dyspozycji będziemy mieć miejsce pozwalające na wykonywanie większej liczby czynności. Być może przydatna okaże się możliwość przeglądania listy wiadomości w skrzynce wiadomości przychodzących i jednoczesnego podglądu zaznaczonego listu w osobnym oknie. Ewentualnie aplikacja może wyświetlać listę kontaktów i w tym samym czasie prezentować szczegółowy widok zaznaczonego kontaktu. Jako programiści aplikacji dla Androida wiemy, że możemy osiągnąć ten cel poprzez zdefiniowanie jeszcze jednego układu graficznego, przeznaczonego dla bardzo dużych ekranów, zawierającego kontrolki ListView i inne rodzaje widoków. Poprzez „jeszcze jeden układ graficzny” mamy na myśli dodatkowy układ graficzny, występujący równorzędnie z analogicznymi układami, zdefiniowanymi dla urządzeń posiadających mniejsze wyświetlacze. Oczywiście, niezbędne okaże się zaprojektowanie oddzielnego układu graficznego dla trybu portretowego i krajobrazowego. W przypadku bardzo dużego ekranu oznacza to dość dużo widoków dla etykiet, pól, obrazów i innych obiektów, które należy rozmieścić i odpowiednio zaprogramować. Nasuwa się myśl, że przydałby się jakiś sposób pogrupowania tych elementów i utworzenia dla nich wspólnej logiki, dzięki czemu dane elementy składowe aplikacji mogłyby być wykorzystywane w aplikacjach dla ekranów o różnych rozmiarach oraz dla różnych urządzeń, minimalizując w ten sposób pracę programisty. Dlatego właśnie została zaprojektowana koncepcja fragmentów. Fragment można uznać za swego rodzaju podaktywność. Rzeczywiście, semantyka fragmentu bardzo przypomina semantykę aktywności. Zawiera on podobną hierarchię widoków oraz analogiczny cykl życia. Fragmenty mogą nawet reagować w taki sam sposób na wciśnięcia przycisku cofania jak aktywności. Jeżeli Czytelnik zastanawia się, czy w ten sposób można jednocześnie umieścić wiele aktywności na ekranie tabletu, to oznacza, że jest na dobrej drodze. Jednak ponieważ utrzymywanie więcej niż jednej uruchomionej aktywności w danej aplikacji może powodować nieporządek, zadanie to zostało przypisane właśnie fragmentom. Oznacza to, że fragmenty są przechowywane wewnątrz aktywności. Mogą one przebywać jedynie we wnętrzu kontekstu aktywności; fragment nie może bez niej istnieć. Współegzystują one z pozostałymi elementami aktywności, co oznacza, że nie musimy konwertować całego interfejsu użytkownika, aby móc korzystać z fragmentu. Możemy utworzyć wcześniejszy układ graficzny aktywności i wykorzystać fragment odnoszący się do tylko jednego elementu interfejsu użytkownika. W porównaniu do aktywności fragmenty zachowują się jednak inaczej, gdy mamy do czynienia z zachowywaniem i odczytywaniem stanu. Struktura fragmentu zawiera kilka funkcji, które pozwalają na o wiele łatwiejsze zapisywanie i wczytywanie jego stanu, niż ma to miejsce w przypadku aktywności. To, kiedy należy wprowadzić fragmenty, zależy od kilku czynników, które teraz omówimy.

Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

1027

Kiedy należy stosować fragmenty? Jednym z głównych powodów stosowania fragmentów jest możliwość wykorzystywania elementów interfejsu użytkownika oraz jego funkcji w różnych urządzeniach oraz dla różnych rozmiarów ekranu. Stwierdzenie to dotyczy zwłaszcza tabletów. Pomyślmy, ile rzeczy naraz może się dziać na ekranie tabletu. W tym przypadku mamy do czynienia raczej z komputerem biurkowym niż telefonem, a wiele aplikacji biurowych posiada wielopanelowy interfejs użytkownika. Zgodnie z tym, co powiedzieliśmy wcześniej, możemy w jednym momencie wyświetlić na ekranie listę elementów oraz szczegółowy opis jednego z nich. Łatwo to zobrazować w trybie krajobrazowym, gdzie lista może się znajdować po lewej stronie ekranu, a szczegółowy opis — po prawej. Co się jednak stanie, w przypadku gdy użytkownik obróci urządzenie do trybu portretowego i ekran stanie się węższy i wyższy? Być może będziemy teraz chcieli, aby lista znalazła się w górnej części wyświetlacza, a szczegóły w jego dolnej części. Co jednak zrobić, w przypadku gdy wyświetlacz będzie zbyt mały na jednoczesne wyświetlanie dwóch elementów? Czy najlepszym rozwiązaniem nie byłoby oddzielenie aktywności listy od aktywności szczegółów, lecz w taki sposób, aby mogły współdzielić logikę wykorzystywaną w tej samej aplikacji, ale podczas obsługi dużych ekranów? Mamy nadzieję, że Czytelnik odpowiedział twierdząco. Także w tym przypadku fragmenty okazują się pomocne. Cofnijmy się do przykładu ze zmianą orientacji ekranu. Wiemy, że w przypadku pisania kodu obsługującego zmiany, które zachodzą w aktywności podczas obrotu urządzenia, prawdziwym utrapieniem okazuje się zapisywanie bieżącego stanu aktywności oraz jego przywracanie po odtworzeniu aktywności w nowym trybie. Czy nie podobałoby się nam, gdyby aktywność składała się z elementów, które byłyby utrzymywane w trakcie zmian trybu orientacji, dzięki czemu uniknęlibyśmy tego całego chaosu związanego z usuwaniem i odtwarzaniem aktywności? Oczywiście, że bardzo by nam się podobało. Od tego są fragmenty. Wyobraźmy sobie teraz, że użytkownik korzysta z naszej aktywności i przeprowadza jakąś czynność. Załóżmy, że interfejs użytkownika uległ zmianie w obrębie tej aktywności i użytkownik chce cofnąć się o jeden albo dwa ekrany, a może nawet trzy. W klasycznej aktywności wciśnięcie przycisku cofania uniemożliwi użytkownikowi powrót do danej aktywności. W przypadku fragmentów każde wciśnięcie tego przycisku spowoduje cofnięcie o jeden fragment w ich stosie i użytkownik cały czas będzie mógł korzystać z bieżącej aktywności. Pomyślmy teraz o interfejsie użytkownika, w którym zmianie ulega ogromny zakres treści; chcielibyśmy, aby ten proces przebiegał w elegancki sposób, jak na dopracowaną aplikację przystało. Także tutaj fragmenty okażą się pomocne. Skoro już mniej więcej wiemy, czym jest fragment oraz do czego może nam się przydać, zajrzyjmy nieco głębiej w jego strukturę.

Struktura fragmentu Jak już wspomnieliśmy, fragment przypomina nieco podaktywność: posiada jasno określone przeznaczenie i niemal zawsze prezentuje interfejs użytkownika. Jednak aktywność stanowi klasę podrzędną w stosunku do kontekstu, natomiast fragment jest rozszerzeniem klasy Object, będącej częścią pakietu android.app. Fragment nie stanowi rozszerzenia aktywności. Jednak, podobnie jak ma to miejsce w przypadku aktywności, klasa Fragment (lub jej elementy podrzędne) będzie zawsze rozszerzana w celu przesłonięcia jej zachowania.

1028 Android 3. Tworzenie aplikacji Fragment może posiadać hierarchię widoków służących do nawiązywania kontaktu z użytkownikiem. Hierarchia ta nie różni się od innych hierarchii widoków pod tym względem, że może zostać utworzona (rozwinięta) za pomocą specyfikacji układu graficznego w pliku XML lub implementacji w kodzie Java. Jeżeli ma być widziana przez użytkownika, taka hierarchia musi zostać dołączona do hierarchii widoków aktywności nadrzędnej, czym wkrótce zajmiemy się dokładniej. Obiekty tworzące hierarchię widoków fragmentów nie różnią się od widoków wykorzystywanych w innych rejonach Androida. Zatem cała wiedza zdobyta na temat widoków znajduje również zastosowanie w przypadku fragmentów. Oprócz hierarchii widoków fragment zawiera także pakiet służący jako argumenty inicjalizacyjne. Analogicznie do aktywności, fragment może zostać automatycznie zachowany, a następnie wczytany przez system. W trakcie wczytywania fragmentu zostaje wywołany domyślny konstruktor (na przykład nieposiadający argumentów), następnie odczytany pakiet argumentów wobec nowego fragmentu. Kolejne metody zwrotne fragmentu uzyskują dostęp do tych argumentów, które mogą zostać wykorzystane do przywrócenia poprzedniego stanu. Z tego powodu koniecznie musimy: „ upewnić się, że istnieje domyślny konstruktor klasy fragmentu; „ dodać pakiet argumentów tuż po utworzeniu nowego fragmentu, dzięki czemu następne metody skonfigurują nasz fragment we właściwy sposób, a system w razie konieczności bezbłędnie go wczyta. Aktywność w danej chwili może posiadać kilka aktywnych fragmentów, a jeżeli jeden fragment został wymieniony na inny, cały proces wymiany zostanie zapisany w stosie drugoplanowym. Stos ten jest zarządzany przez powiązany z aktywnością menedżer fragmentów. To właśnie za jego pomocą definiowane jest zachowanie fragmentów po wciśnięciu przycisku cofania. Menedżer fragmentów zostanie omówiony w dalszej części tego rozdziału. Teraz Czytelnikowi wystarczy wiedza, że fragment doskonale „wie”, z którą aktywnością jest powiązany, dzięki czemu może bez problemu nawiązać komunikację z menedżerem fragmentów. Fragment może również uzyskać dostęp do zasobów poprzez aktywność. Ponieważ istnieje możliwość zarządzania fragmentem, zawiera on pewne dane identyfikacyjne, w tym znacznik oraz identyfikator. Dane te przydają się podczas wyszukiwania fragmentu, dzięki czemu może być wielokrotnie wykorzystywany. Również analogicznie do aktywności, podczas odtwarzania pakietu można zapisać jego stan w obiekcie pakietu, który zostaje przekazany do metody zwrotnej onCreate(). Taki zachowany pakiet jest również przekazywany do metod onInflate(), onCreateView() i onActivity ´Created(). Zwróćmy uwagę, że nie jest to ten sam pakiet, który jest wykorzystywany jako argument inicjalizacji. To właśnie w tym pakiecie będziemy najprawdopodobniej zapisywać stan fragmentu, a nie wartości wykorzystywane do jego inicjalizacji.

Cykl życia fragmentu Zanim zaczniemy testować fragmenty w przykładowych aplikacjach, musimy koniecznie zapoznać się z ich cyklem życia. Dlaczego? Cykl życia fragmentu jest bardziej skomplikowany od cyklu życia aktywności i jest bardzo ważne, aby zrozumieć, kiedy można wykonać daną operację na fragmencie. Na rysunku 29.1 przedstawiliśmy cykl życia fragmentu. Jeżeli porównamy go z rysunkiem 2.15 (na którym pokazaliśmy cykl życia aktywności), dostrzeżemy kilka różnic, najczęściej związanych z oddziaływaniem pomiędzy fragmentem a aktywnością.

Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

1029

Rysunek 29.1. Cykl życia fragmentu

Fragment jest bardzo uzależniony od aktywności, do której jest przypisany, i może przejść przez kilka etapów cyklu życia, podczas gdy aktywność w tym samym czasie znajduje się ciągle na jednym etapie. Na samym początku tworzy się wystąpienie fragmentu. Istnieje on teraz jako obiekt w pamięci. Prawdopodobnie pierwszą czynnością będzie dodanie argumentów inicjalizacji do tego obiektu. Stwierdzenie to jest najbardziej prawdziwe w przypadku odtwarzania wcześniej zachowanego stanu fragmentu. W momencie odtwarzania stanu fragmentu zostaje przywołany domyślny konstruktor, po czym następuje dołączenie pakietu argumentów inicjalizacji. W przypadku tworzenia nowego wystąpienia fragmentu warto zapoznać się z kodem z listingu 29.1 — zaprezentowaliśmy w nim bardzo przydatny schemat metody fabrykującej obiekty wewnątrz definicji klasy MyFragment. Listing 29.1. Tworzenie wystąpienia fragmentu za pomocą statycznej metody fabrykującej public static MyFragment newInstance(int index) { MyFragment f = new MyFragment(); Bundle args = new Bundle(); args.putInt("index", index); f.setArguments(args); return f; }

Z perspektywy klienta otrzymuje on nowe wystąpienie fragmentu poprzez wywołanie statycznej metody newInstance() zawierającej jeden argument. Otrzymuje z powrotem utworzoną instancję obiektu, natomiast argument inicjalizacji został zdefiniowany w pakiecie argumentów fragmentu. Jeżeli ten fragment zostanie zapisany i odtworzony w późniejszym terminie, system przeprowadzi bardzo podobny proces, angażujący domyślny konstruktor i przyłączający argumenty

1030 Android 3. Tworzenie aplikacji inicjalizacji. W tym konkretnym przypadku moglibyśmy zdefiniować sygnaturę metody (lub metod) newInstance(), która przyjmowałaby właściwą liczbę i typ argumentów, a następnie poprawnie zbudowałaby pakiet argumentów. Jest to jedyne zadanie metody newInstance(). Występujące po niej metody zwrotne prowadzą pozostałą część procesu konfiguracji fragmentu.

Metoda zwrotna onInflate() Następnym procesem, jaki mógłby nastąpić, jest rozwinięcie widoku układu graficznego. Jeżeli nasz fragment jest zdefiniowany przez znacznik

w rozwijanym układzie graficznym (najczęściej ma to miejsce w momencie wywołania metody setContentView() wobec głównego układu graficznego aktywności), w naszym fragmencie zostałaby wywołana osobna metoda zwrotna onInflate(). Zostają w niej przekazane interfejs AttributeSet, atrybuty znacznika oraz pakiet atrybutów zachowanego stanu. Jeżeli dany fragment został odtworzony oraz został wcześniej zapisany jakiś stan w metodzie onSaveInstanceState(), we wspomnianym pakiecie są przechowywane wartości tego stanu. Oczekujemy po metodzie onInflate(), że będą odczytywane wartości stanu oraz zostaną one zachowane na później. W rzeczywistości na tym etapie istnienia fragmentu jest jeszcze za wcześnie, żeby móc co*kolwiek zrobić z interfejsem użytkownika. Fragment nie został jeszcze nawet powiązany z aktywnością. Ale to będzie właśnie następna czynność, jaka zostanie na nim przeprowadzona. Do rejestru błędów został wprowadzony defekt numer 14796, który wynika z rozbieżności pomiędzy dokumentacją metody onInflate() a tym, co faktycznie zachodzi w systemie Honeycomb. Zgodnie z dokumentacją metoda onInflate() jest zawsze wywoływana przed metodą onAttach(). W rzeczywistości po ponownym uruchomieniu aktywności metoda ta może zostać wywołana po metodzie onCreateView(). Jest już wtedy za późno na umieszczenie wartości w pakiecie i wywołanie metody setArguments(). Więcej informacji na ten temat znajdziemy pod adresem http://code.google.com/p/android/issues/ detail?id=14796. Z tego samego powodu metoda zwrotna onInflate() nie została umieszczona na schemacie z rysunku 29.1; zbyt trudno przewidzieć, kiedy zostanie wywołana.

Metoda zwrotna onAttach() Metoda zwrotna onAttach() zostaje przywołana po przyłączeniu fragmentu do aktywności. Jeżeli chcemy wykorzystać odniesienie do aktywności, możemy uzyskać do niego dostęp. Taką aktywność możemy wykorzystać przynajmniej do uzyskania informacji o otaczającej aktywności. Możemy również stosować aktywność w postaci kontekstu dla innych czynności. Warto odnotować fakt, że klasa Fragment zawiera metodę getActivity(), która w razie potrzeby zawsze będzie przekazywała aktywność dołączoną do naszego fragmentu. Pamiętajmy, że przez cały cykl życia pakiet argumentów inicjalizacji będzie dostępny za pomocą metody getArguments() fragmentu. Jednak po przyłączeniu fragmentu do aktywności nie będziemy mogli już wywołać tej metody, zatem możemy dodawać argumenty inicjalizacji jedynie na samym początku.

Metoda zwrotna onCreate() Następnym etapem jest metoda onCreate(). Przypomina ona analogiczną metodę aktywności, różnica polega na tym, że nie powinniśmy w jej wnętrzu umieszczać kodu zależnego od obecności hierarchii widoków aktywności. Chociaż nasz fragment może być już powiązany z aktywnością, nie zostaliśmy jeszcze powiadomieni o zakończeniu działania metody onCreate() aktywności. Dopiero zbliżamy się do tego momentu. Jeżeli posiadamy pakiet argumentów

Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

1031

zachowanego stanu, zostanie on przekazany tej metodzie. Metoda ta może jako pierwsza utworzyć wątek drugoplanowy, pobierający dane wymagane przez fragment. Kod fragmentu jest przetwarzany w wątku interfejsu użytkownika i nie chcemy, aby były w nim przeprowadzane również operacje wejścia-wyjścia lub sieciowe. W rzeczywistości logicznym rozwiązaniem jest przygotowanie danych za pomocą wątku pobocznego. To właśnie w nim powinny występować wszystkie wywołania blokujące. Musimy później połączyć się w jakiś sposób z tymi danymi, istnieją na to jednak odpowiednie sposoby. Jednym ze sposobów wczytania danych w wątku pobocznym jest zastosowanie klasy Loader. Zabrakło nam miejsca na jej opis w książce, zapraszamy jednak do przejrzenia naszej oficjalnej strony WWW w celu zapoznania się z niezbędnymi informacjami.

Metoda zwrotna onCreateView() Kolejną metodę zwrotną stanowi onCreateView(). Spodziewamy się po niej, że przekaże naszemu fragmentowi hierarchię widoków. Wśród przekazywanych argumentów znajdziemy tutaj klasę LayoutInflater (służącą do rozwijania układu graficznego danego fragmentu), klasę nadrzędną ViewGroup (na listingu 29.2 noszącą nazwę container) oraz pakiet zachowanego stanu (jeżeli istnieje). Jest bardzo istotne, aby zwrócić tutaj uwagę, że nie powinniśmy dołączać hierarchii widoku do przekazywanego widoku potomnego ViewGroup. Powiązanie to nastąpi automatycznie później. Mamy do dyspozycji klasę nadrzędną, dzięki czemu możemy ją wykorzystać wraz z metodą inflate() klasy LayoutInflater, chociaż w razie konieczności możemy samodzielnie sprawdzić tę klasę. Najprawdopodobniej jednak, jeżeli przyłączymy w tej metodzie zwrotnej hierarchię widoków fragmentu do klasy nadrzędnej, pojawią się wyjątki. Na listingu 29.2 prezentujemy przykład operacji, jaką można wykonać w tej metodzie. Listing 29.2. Utworzenie hierarchii widoków fragmentu w metodzie onCreateView() @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.details, container, false); TextView text1 = (TextView) v.findViewById(R.id.text1); text1.setText(myDataSet[ getPosition() ] ); return v; }

Widzimy tutaj, w jaki sposób możemy uzyskać dostęp do układu graficznego wykorzystywanego wyłącznie przez ten fragment i rozwinąć go do widoku, który zostanie następnie przekazany obiektowi wywołującemu. Takie rozwiązanie posiada kilka zalet. Możemy zawsze utworzyć hierarchię widoków za pomocą kodu, jednak poprzez rozwinięcie układu graficznego z pliku XML wykorzystujemy możliwości technologii wyszukiwania zasobów. W zależności od konfiguracji urządzenia lub, dokładniej mówiąc, od aktualnie używanego urządzenia zostanie wybrany najwłaściwszy plik układu graficznego. Możemy wtedy uzyskać dostęp do określonego widoku przechowywanego w układzie graficznym; w naszym przypadku — pola text1 klasy TextView. Jeszcze raz powtarzamy: nie należy dołączać w tej metodzie zwrotnej widoku danego fragmentu do klasy nadrzędnej. Na listingu 29.2 widzimy, że wykorzystujemy pojemnik w wywołaniu metody inflate(), przekazujemy jednak również wartość false w parametrze attachToRoot.

1032 Android 3. Tworzenie aplikacji

Metoda zwrotna onActivityCreated() Zbliżamy się do momentu, w którym użytkownik będzie mógł oddziaływać na fragment. Następną metodą cyklu życia fragmentu jest onActivityCreated(). Zostaje ona wywołana po zakończeniu działania metody zwrotnej onCreate(). Mamy teraz pewność, że hierarchia widoków aktywności, w tym również hierarchia widoków fragmentu (jeśli została wcześniej przekazana), jest gotowa i dostępna. To właśnie na tym etapie wprowadzamy ostatnie poprawki w interfejsie, zanim oddamy go w ręce użytkownika. Jest to szczególnie istotne w przypadku odtwarzania aktywności i jej fragmentów z zachowanego stanu. Teraz właśnie do aktywności zostają również dołączone pozostałe fragmenty.

Metoda zwrotna onStart() Kolejna metoda cyklu życia fragmentu to onStart(). Na tym etapie fragment jest już widoczny dla użytkownika. Nie rozpoczęliśmy jednak jeszcze interakcji z użytkownikiem. Metoda ta jest powiązana z metodą zwrotną onStart() aktywności. W ten sposób możemy wstawić część operacji uprzednio umieszczanych w metodzie onStart() aktywności do metody onStart() fragmentu, gdyż to właśnie w nim znajdują się składniki interfejsu użytkownika.

Metoda zwrotna onResume() Ostatnią metodą zwrotną występującą przed rozpoczęciem interakcji użytkownika z fragmentem jest onResume(). Metoda ta jest powiązana z metodą onResume() aktywności. Po jej powrocie użytkownik może już korzystać z fragmentu. Jeśli na przykład nasz fragment zawiera podgląd widoku aparatu fotograficznego, uruchomimy go prawdopodobnie w metodzie onResume() tego fragmentu. Dotarliśmy w końcu do punktu, w którym nasza aplikacja — ku radości użytkownika — szczęśliwie rozpoczyna działanie. Po pewnym czasie użytkownik zechce zakończyć pracę z programem albo poprzez wciśnięcie przycisku cofania, albo przycisku ekranu startowego, albo ewentualnie poprzez uruchomienie innej aplikacji. Podobnie jak w przypadku aktywności, następuje tu sekwencja zdarzeń postępujących w kierunku przeciwnym do omówionej powyżej.

Metoda zwrotna onPause() Pierwszą metodą zwrotną anulującą fragment jest onPause(). Jest ona powiązana z metodą onPause() aktywności; podobnie jak ma to miejsce w przypadku aktywności, jeżeli fragment zawiera odtwarzacz multimediów lub jakiś inny współdzielony obiekt, możemy wstrzymać, zatrzymać lub odebrać wyniki jego działania właśnie poprzez tę metodę. Mamy tu do czynienia z takimi samymi zasadami „dobrego wychowania” co w przypadku aktywności: muzyka nie powinna być odtwarzana, kiedy użytkownik rozmawia przez telefon. Istnieje możliwość przejścia fragmentu od metody onPause() z powrotem do metody onResume().

Metoda zwrotna onStop() Kolejna metoda zwrotna anulująca fragment to onStop(). Jest ona powiązana z metodą onStop() aktywności i pełni podobną funkcję. Zatrzymany fragment może przejść wprost do metody onStart(), skąd wiedzie bezpośrednia droga do metody onResume().

Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

1033

Metoda zwrotna onDestroyView() Jeżeli nasz fragment ma zostać zamknięty lub jeśli trzeba zapisać jego stan, następną metodą zwrotną występującą na ścieżce jego anulowania jest onDestroyView(). Zostanie ona wywołana po odłączeniu hierarchii widoków z danego fragmentu, utworzonej w metodzie onCreateView().

Metoda zwrotna onDestroy() Następnie mamy do czynienia z metodą onDestroy(). Zostaje ona wywołana, w przypadku gdy fragment przestaje być potrzebny. Zwróćmy uwagę, że jest on nadal dołączony do aktywności i ciągle można go „odnaleźć”, na niewiele się już jednak przyda.

Metoda zwrotna onDetach() Ostatnią metodą zwrotną cyklu życia fragmentu jest onDetach(). Po jej wywołaniu fragment przestaje być powiązany z aktywnością, nie posiada już hierarchii widoków i wszystkie związane z nim zasoby zostają zwolnione.

Stosowanie metody setRetainInstance() Być może Czytelnik zwrócił uwagę na połączenia oznaczone przerywaną linią na rysunku 29.1. Jedną z bardziej interesujących cech fragmentu jest możliwość zadeklarowania, że nie chcemy całkowicie pozbywać się go w przypadku odtwarzania aktywności, dzięki czemu nasze fragmenty mogą się pojawiać później. Taki fragment pojawia się więc wraz z wywołaniem metody setRetainInstance(), która przyjmuje wartość logiczną. Możemy sobie obrazowo przedstawić, że wartość ta może znaczyć: „Tak, ten fragment ma istnieć w trakcie odtwarzania aktywności” lub „Nie, ten fragment zostanie usunięty i utworzymy od początku nowy fragment”. Najlepszym miejscem na wywołanie tej metody jest wnętrze metody onCreate() fragmentu. Jeśli parametr przyjmie wartość true, oznacza to, że chcemy przechować obiekt fragmentu w pamięci i nie mamy zamiaru tworzyć nowego obiektu od podstaw. Jeżeli jednak aktywność zostaje usunięta i odtworzona, musimy odłączyć fragment od starego obiektu i podłączyć go do nowej aktywności. Wniosek z tego taki, że jeżeli wartość przechowywanego wystąpienia wynosi true, w rzeczywistości nie będziemy całkowicie usuwać instancji fragmentu, zatem nie będziemy musieli tworzyć nowego fragmentu. Jednak wszystkie pozostałe metody zwrotne zostaną wywołane. Połączenia zaznaczone przerywaną linią oznaczają, że możemy podczas wychodzenia pominąć metodę onDestroy() oraz — na etapie ponownego podłączania fragmentu do aktywności — metodę onCreate(). Ponieważ aktywność jest najprawdopodobniej odtwarzana z powodu zmian konfiguracyjnych, metody zwrotne fragmentu powinny się opierać na założeniu, że takie zmiany nastąpiły i należy podjąć odpowiednie działania. Takim działaniem może być na przykład rozwinięcie układu graficznego tworzącego nową hierarchię widoków w metodzie onCreateView(). Do tego może posłużyć przykładowo kod zamieszczony na listingu 29.2. Jeżeli postanowimy skorzystać z funkcji przechowywania instancji, być może powinniśmy pominąć wstawienie części logiki inicjalizacji w metodzie onCreate(), ponieważ nie będzie ona wywoływana zawsze w taki sam sposób jak pozostałe metody zwrotne.

Przykładowa aplikacja ukazująca cykl życia fragmentu Nic nie pozwala bardziej docenić omawianej koncepcji, jak zademonstrowanie jej na działającym przykładzie. Utworzymy aplikację prezentującą w działaniu wszystkie omówione powyżej metody zwrotne. Jeden fragment będzie zawierał listę dzieł Szekspira; po kliknięciu któregoś

1034 Android 3. Tworzenie aplikacji tytułu zostanie wyświetlony obszerny cytat z tej sztuki w osobnym fragmencie. Aplikacja ta działa na tablecie zarówno w trybie portretowym, jak i krajobrazowym. Skonfigurujemy następnie ten przykład w taki sposób, aby działał na mniejszym ekranie, dzięki czemu Czytelnik nauczy się rozdzielać fragmenty tekstowe na aktywności. Rozpoczniemy od układu graficznego aktywności w trybie krajobrazowym, zaprezentowanym na listingu 29.3 i zilustrowanym na rysunku 29.2. Na końcu rozdziału zamieściliśmy adres URL, pod którym znajdziemy listę projektów utworzonych na potrzeby rozdziału. Możemy je zaimportować bezpośrednio do środowiska Eclipse. Listing 29.3. Układ graficzny aktywności w trybie krajobrazowym

Rysunek 29.2. Interfejs użytkownika przykładowej aplikacji ukazującej zastosowanie fragmentów

Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

1035

Powyższy układ graficzny nie różni się zbytnio od układów graficznych prezentowanych w poprzednich rozdziałach: jest on ułożony poziomo, z dwoma elementami umieszczonymi z lewej i z prawej strony. Mamy jednak do czynienia z nowym znacznikiem —

, w którym znajdziemy nowy atrybut noszący nazwę class. Pamiętajmy, że fragment nie jest widokiem, zatem układ graficzny fragmentu nieco się różni od innych układów graficznych. Należy również pamiętać, że znacznik ten stanowi jedynie wypełniacz w tym układzie graficznym. Pozostałe atrybuty fragmentu wyglądają podobnie jak atrybuty widoku i pełnią analogiczne funkcje. Atrybut class znacznika fragmentu definiuje rozszerzoną klasę dla listy tytułów. Oznacza to, że musimy rozszerzyć klasę Fragment w celu zaimplementowania logiki, a w znaczniku musi się znaleźć nazwa tej rozszerzonej klasy. Fragment posiada własną hierarchię widoków, która zostanie w późniejszym okresie samoistnie utworzona. Następnym znacznikiem jest FrameLayout, a nie kolejny znacznik . Dlaczego? Wyjaśnimy to dokładniej w dalszej części rozdziału, na razie jednak Czytelnik powinien wiedzieć, że będziemy wprowadzać pewne modyfikacje tekstu i wymieniać fragmenty między sobą. Znacznik FrameLayout służy jako pojemnik widoku przechowujący bieżący fragment tekstu. W przypadku fragmentu przechowującego tytuły istnieje jeden (i tylko jeden) fragment, o który trzeba zadbać; nie ma żadnego zamieniania miejscami i żadnych innych modyfikacji. W przypadku obszaru wyświetlającego szekspirowski poemat mamy do czynienia z kilkoma fragmentami. Kod klasy MainActivity został umieszczony na listingu 29.4. Listing 29.4. Kod źródłowy klasy MainActivity // Jest to plik MainActivity.java import import import import import import import import import

android.app.Activity; android.app.Fragment; android.app.FragmentManager; android.app.FragmentTransaction; android.content.Intent; android.content.res.Configuration; android.os.Bundle; android.os.Environment; android.util.Log;

public class MainActivity extends Activity { public static final String TAG = "Szekspir"; @Override public void onCreate(Bundle savedInstanceState) { Log.v(TAG, "w metodzie onCreate aktywnosci MainActivity"); super.onCreate(savedInstanceState); FragmentManager.enableDebugLogging(true); setContentView(R.layout.main); } @Override public void onAttachFragment(Fragment fragment) { Log.v(TAG, "w metodzie onAttachFragment aktywnosci MainActivity. Id fragmentu = " + fragment.getId()); super.onAttachFragment(fragment); } @Override

1036 Android 3. Tworzenie aplikacji public void onStart() { Log.v(TAG, "w metodzie onStart aktywnosci MainActivity"); super.onStart(); } @Override public void onResume() { Log.v(TAG, "w metodzie onResume aktywnosci MainActivity"); super.onResume(); } @Override public void onPause() { Log.v(TAG, "w metodzie onPause aktywnosci MainActivity"); super.onPause(); } @Override public void onStop() { Log.v(TAG, "w metodzie onStop aktywnosci MainActivity"); super.onStop(); } @Override public void onSaveInstanceState(Bundle outState) { Log.v(MainActivity.TAG, "w metodzie onSaveInstanceState aktywnosci ´MainActivity"); super.onSaveInstanceState(outState); } @Override public void onDestroy() { Log.v(TAG, "w metodzie onDestroy aktywnosci MainActivity"); super.onDestroy(); } public boolean isMultiPane() { return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; }

/** * Funkcja pomocnicza ukazująca szczegóły zaznaczonego elementu albo poprzez * wyświetlanie fragmentu znajdującego się w bieżącym interfejsie użytkownika, albo * poprzez rozpoczęcie nowej aktywności, w której będą one wyświetlane. */ public void showDetails(int index) { Log.v(TAG, "w metodzie showDetails(" + index + ") aktywnosci MainActivity"); if (isMultiPane()) {

// Sprawdza, który fragment jest wyświetlany, w razie potrzeby zamienia go. DetailsFragment details = (DetailsFragment) getFragmentManager().findFragmentById(R.id.details); if (details == null || details.getShownIndex() != index) {

// Tworzy nowy fragment w celu ukazania szczegółów zaznaczenia.

Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

1037

details = DetailsFragment.newInstance(index);

// Przeprowadza operację zastąpienia istniejącego // fragmentu fragmentem umieszczonym wewnątrz ramki. Log.v(TAG, "tuz przed uruchomieniem operacji FragmentTransaction..."); FragmentTransaction ft = getFragmentManager().beginTransaction(); ft.setTransition( FragmentTransaction.TRANSIT_FRAGMENT_FADE);

//ft.addToBackStack("details"); ft.replace(R.id.details, details); ft.commit(); } } else {

// W przeciwnym wypadku musimy uruchomić nową aktywność // wyświetlającą fragment zawierający okno dialogowe z zaznaczonym tekstem. Intent intent = new Intent(); intent.setClass(this, DetailsActivity.class); intent.putExtra("indeks", index); startActivity(intent); } } }

Mamy do czynienia z bardzo prostą aktywnością. Jedynym powodem umieszczenia wszystkich metod zwrotnych w kodzie źródłowym jest możliwość wyświetlania komunikatów w dzienniku. Gdyby nie ten fakt, jedyną wymaganą metodą byłaby onCreate(), z kolei metodami pomocniczymi są isMultiPane() oraz showDetails(). Trudno byłoby wprowadzić prostszą metodę od ukazanej tu onCreate(). Służy ona tutaj wyłącznie do uruchomienia trybu debugowania menedżera fragmentów oraz ustanowienia widoku treści w postaci układu graficznego z listingu 29.3. Aby zdefiniować tryb wielopanelowy (na przykład w celu umieszczania obok siebie wielu fragmentów), wykorzystujemy jedynie położenie wyświetlacza. Jeżeli wyświetlacz jest ułożony w trybie krajobrazowym, możemy korzystać z wielu paneli równocześnie; w trybie portretowym jest to niemożliwe. Na koniec zauważmy, że pomocnicza metoda showDetails() służy do zdefiniowania sposobu wyświetlania szczegółów zaznaczonego tekstu. Indeks definiuje w tym przypadku pozycję na liście tytułów. Jeżeli aplikacja pracuje w trybie wielopanelowym, wykorzystamy fragment do wyświetlenia tekstu. Fragment ten został nazwany DetailsFragment i do jego utworzenia (wraz z indeksem) stosujemy metodę fabrykującą. Kod klasy DetailsFragment został zaprezentowany na listingu 29.5. Później jeszcze powrócimy do metody showDetails(). Listing 29.5. Kod źródłowy klasy DetailsFragment import import import import import import import import import

android.app.Activity; android.app.Fragment; android.os.Bundle; android.util.AttributeSet; android.util.Log; android.view.LayoutInflater; android.view.View; android.view.ViewGroup; android.widget.TextView;

1038 Android 3. Tworzenie aplikacji public class DetailsFragment extends Fragment { private int mIndex = 0; public static DetailsFragment newInstance(int index) { Log.v(MainActivity.TAG, "w metodzie newInstance(" + index + ") klasy DetailsFragment"); DetailsFragment df = new DetailsFragment();

// Dostarcza indeks w postaci argumentu. Bundle args = new Bundle(); args.putInt("indeks", index); df.setArguments(args); return df; } public static DetailsFragment newInstance(Bundle bundle) { int index = bundle.getInt("indeks", 0); return newInstance(index); } @Override public void onInflate(AttributeSet attrs, Bundle savedInstanceState) { Log.v(MainActivity.TAG, "w metodzie onInflate klasy DetailsFragment. Interfejs AttributeSet zawiera:"); for(int i=0; i


Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

1039

} public int getShownIndex() { return mIndex; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Log.v(MainActivity.TAG, "w metodzie onCreateView klasy DetailsFragment. pojemnik = " + container);

// Nie wiążmy tego fragmentu z niczym za pomocą obiektu pompującego. // Android zajmuje się za nas przyłączaniem fragmentów. // Pojemnik jest jedynie przepuszczany, więc wiemy dzięki niemu, // dokąd trafi hierarchia widoków. View v = inflater.inflate(R.layout.details, container, false); TextView text1 = (TextView) v.findViewById(R.id.text1); text1.setText(Shakespeare.DIALOGUE[ mIndex ] ); return v; } @Override public void onActivityCreated(Bundle savedState) { Log.v(MainActivity.TAG, "w metodzie onActivityCreated klasy DetailsFragment. Klasa savedState zawiera:"); if(savedState != null) { for(String key : savedState.keySet()) { Log.v(MainActivity.TAG, " " + key); } } else { Log.v(MainActivity.TAG, " Klasa savedState jest pusta"); } super.onActivityCreated(savedState); } @Override public void onStart() { Log.v(MainActivity.TAG, "w metodzie onStart klasy DetailsFragment"); super.onStart(); } @Override public void onResume() { Log.v(MainActivity.TAG, "w metodzie onResume klasy DetailsFragment"); super.onResume(); } @Override public void onPause() { Log.v(MainActivity.TAG, "w metodzie onPause klasy DetailsFragment"); super.onPause(); }

1040 Android 3. Tworzenie aplikacji @Override public void onSaveInstanceState(Bundle outState) { Log.v(MainActivity.TAG, "w metodzie onSaveInstanceState klasy DetailsFragment"); super.onSaveInstanceState(outState); } @Override public void onStop() { Log.v(MainActivity.TAG, "w metodzie onStop klasy DetailsFragment"); super.onStop(); } @Override public void onDestroyView() { Log.v(MainActivity.TAG, "w metodzie onDestroyView klasy DetailsFragment, widok = " + getView()); super.onDestroyView(); } @Override public void onDestroy() { Log.v(MainActivity.TAG, "w metodzie onDestroy klasy DetailsFragment"); super.onDestroy(); } @Override public void onDetach() { Log.v(MainActivity.TAG, "w metodzie onDetach klasy DetailsFragment"); super.onDetach(); } }

Klasa DetailsFragment jest w istocie równie nieskomplikowana. Jedynym powodem dużych rozmiarów kodu jest wprowadzenie instrukcji służących do wyświetlania informacji w dzienniku. Gdyby nie były nam one potrzebne, w powyższym fragmencie pozostawilibyśmy jedynie metody newInstance(), getShownIndex(), onCreate() oraz onCreateView(). Teraz Czytelnik już wie, w jaki sposób utworzono wystąpienie tego fragmentu. Ważna jest informacja, że wystąpienie tego fragmentu jest tworzone w kodzie, ponieważ układ graficzny definiuje pojemnik ViewGroup (dokładniej — obiekt FrameLayout), do którego trafi fragment przechowujący szczegółowe informacje. Ponieważ w przeciwieństwie do fragmentu zawierającego tytuły omawiany fragment nie jest zdefiniowany w pliku układu graficznego aktywności, musimy tworzyć wystąpienia fragmentów za pomocą kodu. Aby utworzyć nowy fragment przechowujący szczegółowe informacje, stosujemy metodę Jak już wcześniej stwierdziliśmy, ta metoda fabrykująca przywołuje domyślny konstruktor, a następnie ustanawia pakiet argumentów za pomocą wartości indeksu. Po uruchomieniu metody newInstance() fragment przechowujący szczegóły może odczytać wartości indeksu w dowolnej metodzie zwrotnej poprzez odniesienie do pakietu argumentów za pomocą metody getArguments(). Dla naszej wygody możemy zapisać w metodzie onCreate() wartość indeksu pochodzącą z pakietu argumentów, dokładniej zaś w polu członkowskim klasy DetailsFragment. newInstance().

Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

1041

Możemy się zastanawiać, dlaczego po prostu nie wprowadziliśmy wartości mIndex w metodzie newInstance(). Wynika to z faktu, że system poza naszym wzrokiem odtworzy fragment za pomocą domyślnego konstruktora. Następnie zostanie wykorzystany pakiet argumentów do przywrócenia poprzedniego stanu. Android nie wykorzysta metody newInstance(), więc jedynym pewnym sposobem ustanowienia wartości zmiennej mIndex jest odczytanie jej z pakietu argumentów i wprowadzenie jej w metodzie onCreate(). Metoda złożona getShownIndex() odczytuje wartości tego indeksu. Do opisania pozostała nam już tylko metoda onCreateView(), której zrozumienie również nie stanowi wielkiego wyzwania. Zadaniem metody onCreateView() jest przekazywanie hierarchii widoków do fragmentu. Pamiętajmy, że na podstawie konfiguracji chcemy uzyskać wszystkie możliwe rodzaje układów graficznych dotyczące tego fragmentu, zatem najczęściej spotykaną czynnością jest spożytkowanie pliku układu graficznego tego fragmentu. W naszej przykładowej aplikacji takim plikiem jest details.xml, który definiujemy za pomocą zasobu R.layout.details. Zawartość pliku details.xml została umieszczona na listingu 29.6. Listing 29.6. Plik układu graficznego details.xml zdefiniowany dla fragmentu przechowującego szczegółowe informacje

W przypadku naszej przykładowej aplikacji możemy stosować ten sam układ graficzny dla trybu krajobrazowego i portretowego podczas wyświetlania szczegółowych informacji. Jest to układ przeznaczony nie dla aktywności, lecz wyłącznie do wyświetlania tekstu fragmentu. Ponieważ może być on uznany za domyślny układ graficzny, możemy umieścić go w katalogu res/layout, gdzie zostanie znaleziony i zastosowany, nawet jeśli wyświetlacz znajduje się w trybie krajobrazowym. Podczas wyszukiwania pliku układu graficznego służącego do wyświetlania szczegółów system sprawdza najpierw katalogi ściśle powiązane z konfiguracją urządzenia, powróci jednak do katalogu res/layout, jeśli nigdzie indziej nie znajdzie pliku details.xml. Oczywiście, jeżeli chcemy zaprojektować inny układ graficzny fragmentu dla trybu krajobrazowego, możemy zdefiniować osobny plik details.xml i umieścić go w katalogu /res/layout-land. Nic nie stoi na przeszkodzie, żeby eksperymentować z różnymi plikami details.xml. W momencie wywołania metody onCreate() fragmentu przechowującego szczegóły system wybierze i rozwinie układ graficzny z odpowiedniego pliku details.xml, do którego wstawi tekst z klasy Shakespeare. Nie zamieścimy tu całego kodu klasy Shakespeare, lecz jedynie jego część (listing 29.7), aby ułatwić Czytelnikowi zrozumienie jego działania. Pełny kod źródłowy znajdziemy w gotowym projekcie, do którego adres został umieszczony w podrozdziale „Odnośniki” na końcu rozdziału.

1042 Android 3. Tworzenie aplikacji Listing 29.7. Kod źródłowy klasy Shakespeare public class Shakespeare { public static String TITLES[] = { "Henryk IV (1) (J. Paszkowski)", "Henryk V (L. Ulrich)", "Ryszard II (S. Koźmian)", "Romeo i Julia (J. Paszkowski)", "Hamlet (J. Paszkowski)", "Kupiec wenecki (J. Paszkowski)", "Otello (J. Paszkowski)" }; public static String DIALOGUE[] = { "Po tylu troskach, po tylu wtrząśnieniach,1\n...

...i tak dalej...

Zatem obecnie nasza hierarchia widoków we fragmencie zawierającym szczegółowe informacje przechowuje tekst z wybranej sztuki. Fragment ten jest już przygotowany do wyświetlenia. Możemy teraz wrócić do metody showDetails(), aby zająć się omówieniem klasy FragmentTransaction.

Klasy FragmentTransaction i drugoplanowy stos fragmentów Kod w metodzie showDetails(), który służy do wstawiania nowego fragmentu przechowującego szczegółowe informacje (ukazany ponownie na listingu 29.8), wygląda dość prosto, wykonuje jednak mnóstwo zadań. Warto poświęcić trochę czasu na wyjaśnienie, co tu się dzieje i dlaczego tak jest. Jeżeli nasza aktywność działa w trybie wielopanelowym, chcemy zaprezentować fragment przechowujący szczegóły obok fragmentu zawierającego listę. Być może nasza aplikacja wyświetla już ten pierwszy fragment, co oznacza, że stał się widoczny dla użytkownika. W każdym przypadku identyfikator zasobu R.id.details jest przeznaczony dla pojemnika FrameLayout aktywności, co jest widoczne na listingu 29.3. Jeżeli fragment przechowujący szczegóły znajduje się wewnątrz układu graficznego, z powodu braku własnego identyfikatora otrzyma właśnie wspomniany identyfikator. Aby więc dowiedzieć się, czy w układzie graficznym znajduje się jakiś fragment, możemy wysłać zapytanie do metody findFragmentById() menedżera fragmentów. Zostanie przekazana albo wartość null, jeśli układ graficzny jest pusty, albo informacje na temat bieżącego fragmentu. Możemy wtedy zadecydować, że powinniśmy umieścić nowy fragment w układzie graficznym, ponieważ układ graficzny może być pusty lub może być w nim umieszczony układ graficzny reprezentujący szczegóły nieodpowiedniego tytułu. Po podjęciu decyzji o utworzeniu i wykorzystaniu nowego fragmentu wywołujemy w tym celu metodę fabrykującą. Możemy teraz wstawić nowy fragment, który zostanie zaprezentowany użytkownikowi. Listing 29.8. Przykład transakcji fragmentu public void showDetails(int index) { Log.v(TAG, "w metodzie showDetails(" + index + ") aktywnosci MainActivity"); if (isMultiPane()) { 1

Przekład J. Paszkowskiego — przyp. tłum.

Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

1043

// Sprawdza, który fragment jest wyświetlany, podmienia go w razie potrzeby. DetailsFragment details = (DetailsFragment) getFragmentManager().findFragmentById(R.id.details); if (details == null || details.getShownIndex() != index) {

// Tworzy nowy fragment, służący do wyświetlania szczegółów wybranego elementu. details = DetailsFragment.newInstance(index);

// Przeprowadza transakcję i zamienia dowolny // fragment na fragment umieszczony w ramce. Log.v(TAG, "tuz przed uruchomieniem operacji FragmentTransaction..."); FragmentTransaction ft = getFragmentManager().beginTransaction(); ft.setTransition( FragmentTransaction.TRANSIT_FRAGMENT_FADE);

//ft.addToBackStack("details"); ft.replace(R.id.details, details); ft.commit(); }

// Reszta została pominięta w celu zaoszczędzenia miejsca. }

Kluczową koncepcją, którą trzeba zrozumieć, jest to, że fragment musi się znaleźć w pojemniku widoków, zwanym również grupą widoków. Wynika to częściowo z faktu, że fragment sam w sobie nie jest widokiem. Klasa ViewGroup zawiera takie elementy, jak układy graficzne i ich pochodne klasy. To właśnie dlatego wybraliśmy klasę FrameLayout do pliku main.xml stanowiącego układ graficzny aktywności. Nasz fragment przechowujący szczegóły zostanie umieszczony w pojemniku FrameLayout. Gdybyśmy zamiast tego zdefiniowali jeszcze jeden węzeł

w pliku układu graficznego aktywności, nie moglibyśmy przeprowadzić wymaganej operacji zamiany. To właśnie za pomocą klasy przeprowadzamy zamianę fragmentów. W czasie transakcji zamieniamy miejscami dowolny fragment umieszczony w ramce z nowym fragmentem przechowującym szczegóły. Moglibyśmy rozwiązać to w inny sposób, mianowicie lokalizując identyfikator zasobu — kontrolki TextView, która przechowuje tekst szczegółów, i wprowadzając w niej nowy tekst dla nowej wybranej sztuki Szekspira. Istnieje jednak jeszcze jeden fakt dotyczący fragmentów, przemawiający za korzystaniem z klasy FragmentTransaction. Jak wiemy, aktywności są poukładane na stosie i w trakcie zagłębiania się w aplikację okazuje się, że często na stosie znajduje się kilka jednocześnie uruchomionych aktywności. Po wciśnięciu przycisku cofania aktywność znajdująca się na wierzchu stosu jest z niego usuwana, a jej miejsce zajmuje następna w kolejce aktywność, która zostaje wznowiona. Proces ten jest przeprowadzany wzdłuż całego stosu aż do poziomu ekranu startowego. Stanowiło to znakomite rozwiązanie w przypadku prostych aktywności, teraz jednak omawiamy takie, które zawierają po kilka jednocześnie działających fragmentów. Ponadto, skoro możemy coraz bardziej zagłębiać się w aplikację bez konieczności opuszczania aktywności znajdującej się na wierzchu stosu, trzeba było rozwinąć koncepcję korzystania z przycisku cofania w taki sposób, aby uwzględnić także fragmenty. W istocie fragmenty wymagają zastosowania tej koncepcji nawet bardziej niż proste aktywności. Gdy mamy do czynienia z kilkoma fragmentami oddziałującymi ze sobą równocześnie wewnątrz aktywności i następuje jednoczesne przejście do nowej treści, które dotyczy wszystkich tych fragmentów, to wciśnięcie przycisku cofania powinno sprawić, że fragmenty te razem zostaną cofnięte o jeden etap. Aby zapewnić, że to cofnięcie obejmie wszystkie fragmenty w ramach danej aktywności, utworzono klasę FragmentTransaction — zapewnia ona koordynację tego procesu.

1044 Android 3. Tworzenie aplikacji Należy pamiętać, że drugoplanowy stos fragmentów nie jest wymagany we wnętrzu aktywności. Możemy zaprogramować działanie przycisku cofania w taki sposób, żeby obejmował on wyłącznie aktywność, a nie fragmenty. Jeżeli nie zdefiniujemy drugoplanowego stosu fragmentów, wciśnięcie przycisku cofania spowoduje usunięcie bieżącej aktywności ze stosu i powrót do wcześniejszej aktywności. Jeśli zaś Czytelnik postanowi wykorzystać możliwości oferowane przez stos fragmentów, powinien na listingu 29.8 usunąć znaki komentarza z wiersza ft.addToBackStack("details"). W tym konkretnym przypadku zamieściliśmy w kodzie parametr znacznika w postaci ciągu znaków details. Znacznik ten powinien stanowić ciąg znaków symbolizujący stan fragmentów w momencie przeprowadzania transakcji. Możemy w kodzie modyfikować stos drugoplanowy za pomocą wartości znacznika, co umożliwia usunięcie pewnych wpisów czy też uniknięcie innych. Powinniśmy nadawać przemyślane nazwy znacznikom transakcji, aby móc je później łatwiej znajdować.

Przejścia i animacje zachodzące podczas transakcji fragmentu Jedną z wyjątkowo eleganckich cech transakcji fragmentu jest możliwość zilustrowania zamiany starego fragmentu na nowy za pomocą przejść i animacji. Nie mamy tu do czynienia z animacjami omawianymi w rozdziałach 16. i 20. Animacje przedstawione w tym rozdziale są o wiele prostsze i nie wymagają zaawansowanej wiedzy o grafice. Warto wykorzystać jedno z przejść pozwalających na dodanie efektów specjalnych podczas zmiany starego fragmentu na nowy. W ten sposób nasza aplikacja stanie się bardziej elegancka, a zmiany fragmentów nabiorą płynności. Jedną z pozwalających na to metod jest widoczna na listingu 29.8 setTransition(). Mamy jednak do dyspozycji również kilka innych przejść. W naszym przykładzie skorzystaliśmy z efektu zanikania, możemy jednak wprowadzić metodę setCustomAnimations() do opisania innych efektów specjalnych, na przykład wsuwania się jednego fragmentu z prawej strony ekranu, a drugiego — z lewej. Niestandardowe animacje wykorzystują nowe definicje obiektów animacji, a nie stare. Stare pliki animacji zawierają takie znaczniki, jak

, podczas gdy w nowych stosowane są znaczniki typu . Stare pliki animacji znajdują się w katalogu /data/res/anim w odpowiednim miejscu platformy Android SDK (na przykład platforms/android-11 dla systemu Honeycomb). Znajdziemy tu również nowe pliki umieszczone w katalogu /data/res/animator. Kod przejścia może wyglądać następująco: ft.setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out);

Powoduje on, że stary fragment zanika, podczas gdy nowy stopniowo się pojawia. Pierwszy parametr odnosi się do nowego fragmentu, a drugi do fragmentu usuwanego. Warto przejrzeć katalog animator, aby zapoznać się z innymi domyślnymi animacjami. Jeżeli chcemy utworzyć własną animację, w dalszej części rozdziału znajdziemy sekcję poświęconą animatorowi obiektów. Bardzo ważna jest również informacja, że wywołanie przejścia musi nastąpić przed wywołaniem metody replace(), w przeciwnym wypadku zostanie ono zignorowane. Używanie animatora obiektów do programowania efektów specjalnych widocznych podczas zmiany fragmentów może być zabawne. W klasie FragmentTransaction znajdziemy jeszcze dwie metody, z którymi powinniśmy się zapoznać: hide() i show(). Parametrem w przypadku obydwu tych metod jest fragment. Zadania, jakie metody te realizują, wynikają z ich nazw. W przypadku fragmentu powiązanego z kontenerem widoków powodują one jego ukrywanie lub wyświetlanie w interfejsie użytkownika. W ten sposób fragment nie zostaje usunięty z menedżera fragmentów, musi być jednak związany z kontenerem widoków, aby metody miały wpływ na jego widoczność. Jeżeli fragment nie zawiera hierarchii widoków lub hierarchia ta nie jest powiązana z hierarchią wyświetlanych widoków, metody te okażą się bezużyteczne.

Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

1045

Po zdefiniowaniu efektów specjalnych występujących w czasie transakcji fragmentu musimy określić również główną czynność. W naszym przypadku zamieniamy fragment znajdujący się w ramce na nowy fragment przechowujący szczegóły. Posłużymy się tutaj metodą replace(). Stanowi ona ekwiwalent wywołania metody remove() wobec dowolnych fragmentów znajdujących się w ramce, a następnie metody add() wobec nowego fragmentu zawierającego szczegóły, co oznacza, że w razie potrzeby możemy po prostu wywoływać zamiast niej metody remove() i add(). Ostatnim działaniem, jakie musimy podjąć podczas transakcji fragmentu, jest jej zatwierdzenie. Metoda commit() nie powoduje natychmiastowego wykonania czynności, lecz raczej ustanowienie jej harmonogramu na czas, gdy wątek interfejsu użytkownika zostanie przygotowany. Teraz już Czytelnik powinien rozumieć, dlaczego zmiana treści w pojedynczym fragmencie jest taka kłopotliwa. Zmieniamy tu nie tylko tekst; możemy w trakcie przejścia wstawić specjalne efekty graficzne. Istnieje również możliwość zapisania szczegółów przejścia w transakcji fragmentu, dzięki czemu proces ten może zostać później odwrócony. Ostatnie zdanie może wydawać się nieco niezrozumiałe, dlatego Czytelnikowi należy się wyjaśnienie. Nie mamy tu do czynienia z transakcją w dosłownym tego słowa znaczeniu. Gdy usuwamy transakcje fragmentów ze stosu drugoplanowego, nie cofamy zmian w danych, które mogły zostać wprowadzone. Jeżeli zmiany zostały wprowadzone w obrębie aktywności, na przykład w trakcie tworzenia transakcji fragmentów w stosie drugoplanowym, wciśnięcie przycisku cofania nie sprawi, że zmienione wartości danych zostaną przywrócone do stanu początkowego. Cofamy się jedynie poprzez widoki interfejsu użytkownika, podobnie jak miało do miejsce w przypadku aktywności, teraz jednak dotyczy to fragmentów. Ponieważ fragmenty są zapisywane i odczytywane w taki, a nie inny sposób, wewnętrzny stan fragmentu odczytanego z atrybutów zachowanego stanu będzie zależeć od wartości zachowanych we fragmencie oraz od sposobu ich odczytania. Zatem fragmenty mogą wyglądać tak jak wcześniej, nie można jednak będzie tego powiedzieć o aktywności, chyba że w trakcie odczytywania stanu fragmentów będziemy odczytywać również stan aktywności. W naszym przykładzie pracujemy tylko z jednym pojemnikiem widoków i wprowadzamy tylko jeden fragment przechowujący szczegóły. W przypadku bardziej złożonych interfejsów użytkownika możemy manipulować pozostałymi fragmentami za pomocą transakcji. W rzeczywistości zajmujemy się tyko rozpoczęciem transakcji, co oznacza, że zamieniamy stary fragment przechowujący szczegóły w ramce na nowy fragment, określamy animację przejścia oraz zatwierdzamy przeprowadzenie tego procesu. Oznaczyliśmy jako komentarz część kodu, w której transakcja jest dodawana do stosu drugoplanowego, można jednak usunąć z tego fragmentu znaki komentarza i tym samym dołączyć ją do stosu.

Klasa FragmentManager Klasa FragmentManager jest składnikiem obsługującym fragmenty przechowywane w aktywności. Zaliczają się do nich również fragmenty przechowywane w stosie drugoplanowym oraz niepodłączone fragmenty. Wyjaśnijmy to. Fragmenty powinny być tworzone wyłącznie w kontekście aktywności. Dzieje się to albo poprzez rozwinięcie układu graficznego aktywności, albo poprzez bezpośrednie utworzenie wystąpienia obiektu, co zostało ukazane na listingu 29.1. W tym drugim przypadku fragment zostaje zazwyczaj dołączony do aktywności za pomocą transakcji, natomiast w każdym wypadku uzyskujemy dostęp i zarządzamy fragmentami poprzez klasę FragmentManager.

1046 Android 3. Tworzenie aplikacji Metodę getFragmentManager() wykorzystujemy wobec aktywności lub wobec przyłączonego fragmentu, aby uruchomić menedżer fragmentów. Z listingu 29.8 wiemy, że to właśnie z poziomu menedżera fragmentów uzyskujemy dostęp do transakcji. Poza tym za pomocą menedżera możemy odczytać identyfikator fragmentu, jego znacznik lub kombinację pakiet atrybutów – klucz, by w ten sposób znaleźć dany fragment. W tym celu mamy do dyspozycji metody pobierające findFragmentById(), findFragment ´ByTag() oraz getFragment(). Ta ostatnia może zostać wykorzystana razem z metodą put ´Fragment(), która również pobiera pakiet atrybutów, klucz oraz wstawiany fragment. Będziemy mieli najprawdopodobniej do czynienia z pakietem savedState, a metoda putFragment() będzie wstawiona w metodzie zwrotnej onSaveInstanceState(), dzięki czemu zostanie zachowany stan bieżącej aktywności (lub innego fragmentu). Metoda getFragment() może prawdopodobnie zostać wywołana w metodzie onCreate(), aby miała związek z metodą putFragment(), chociaż — jak już zostało wcześniej omówione — w przypadku fragmentu pakiet atrybutów jest również dostępny dla pozostałych metod zwrotnych. Oczywiście, nie możemy stosować metody getFragmentManager() wobec fragmentów, które nie zostały podłączone do aktywności. Prawdziwe jest jednak również stwierdzenie, że możemy dołączyć fragment do aktywności w taki sposób, że nie będzie jeszcze widoczny dla użytkownika. Jeżeli zdecydujemy się na to rozwiązanie, naprawdę powinniśmy dołączyć znacznik zawierający określony ciąg znaków do fragmentu, dzięki czemu nie będziemy mieli później problemów z jego wyszukaniem. Prawdopodobnie wykorzystamy w tym celu następującą metodę klasy FragmentTransaction: public FragmentTransaction add (Fragment fragment, String tag)

W rzeczywistości możemy otrzymać fragment, który nie eksponuje hierarchii widoków. Może się to okazać przydatne, jeśli zechcemy zawrzeć określoną logikę w taki sposób, aby móc ją dołączyć do aktywności, lecz jednocześnie pozostawić jej pewną dozę autonomii, odgradzającą ją od cyklu życia aktywności i pozostałych fragmentów. Gdy aktywność przechodzi przez cykl odtwarzania wynikający ze zmiany konfiguracji urządzenia, taki fragment niebędący częścią interfejsu użytkownika może pozostawać w dużej części nietknięty, podczas gdy sama aktywność zostaje usunięta i na jej miejsce wkracza nowa. Takie rozwiązanie jest interesującą opcją dla metody setRetainInstance(). Menedżer fragmentów obsługuje również stos drugoplanowy. Podczas transakcji fragmentów system umieszcza fragmenty na tym stosie, podczas gdy menedżer fragmentów może je stamtąd usuwać. Zazwyczaj dokonujemy tego przy użyciu identyfikatora lub znacznika fragmentu, równie dobrze możemy jednak w tym celu wprowadzić pozycję w stosie lub po prostu usunąć fragment znajdujący się na wierzchu. Na koniec należy stwierdzić, że menedżer fragmentów zawiera metody ułatwiające proces debugowania, w tym takie jak umożliwiające umieszczanie komunikatów w oknie LogCat (metoda enableDebugLogging()) lub umieszczanie bieżącego stanu menedżera fragmentów w strumieniu (metoda dump()). Zwróćmy uwagę, że na listingu 29.4 włączyliśmy tryb debugowania menedżera fragmentów w metodzie onCreate().

Ostrzeżenie dotyczące stosowania odniesień do fragmentów Nadszedł czas, aby powrócić do dyskusji na temat cyklu życia fragmentu, argumentów i pakietów z zachowanymi stanami. System może zachować jeden z fragmentów w wielu różnych sytuacjach. Oznacza to, że w momencie, gdy trzeba będzie odczytać dany fragment, może go nie

Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

1047

być w pamięci. Z tego właśnie powodu ostrzegamy przed uznaniem zmiennego odniesienia do fragmentu za obiekt, który zachowuje ważność przez długi czas. Jeżeli fragmenty są wymieniane w pojemniku widoków za pomocą transakcji, każde odniesienie do poprzedniego fragmentu będzie teraz wskazywać fragment, który prawdopodobnie znajduje się w stosie drugoplanowym. Ewentualnie fragment może zostać odłączony od hierarchii widoków aktywności w trakcie zmiany konfiguracji urządzenia, na przykład w momencie zmiany pozycji urządzenia. Bądźmy ostrożni. Jeżeli Czytelnik zamierza przechowywać odniesienie do fragmentu, powinien wiedzieć, kiedy może ono zostać zachowane. W przypadku konieczności jego znalezienia należy wykorzystać jedną z metod pobierania, zawartą w menedżerze fragmentów. Jeżeli chcemy koniecznie zatrzymać odniesienie do fragmentu, na przykład podczas odtwarzania aktywności w trakcie zmiany konfiguracji, możemy zastosować metodę putFragment() wraz z odpowiednim pakietem atrybutów. W przypadku zarówno aktywności, jak i fragmentów takim pakietem jest savedState wykorzystywany w metodzie onSaveInstanceState() oraz ponownie pojawiający się w metodzie onCreate() (lub innych wcześnie występujących metodach zwrotnych cyklu życia fragmentu). Prawdopodobnie Czytelnik nigdy nie będzie bezpośrednio przechowywać odniesienia do fragmentu w pakiecie argumentów; jeżeli ktoś poczuje jednak taką pokusę, powinien to bardzo dokładnie przemyśleć. Innym mechanizmem pozwalającym na uzyskanie dostępu do określonego fragmentu jest wysłanie zapytania zawierającego jego identyfikator lub znacznik. Uprzednio omówione metody pobierające pozwalają na odczytanie w ten sposób fragmentów z menedżera, co oznacza, że posiadamy możliwość zachowania takiego znacznika lub identyfikatora, dzięki czemu możemy za ich pomocą uzyskać dostęp do danego fragmentu, co jest alternatywą metod putFragment() i getFragment().

Klasa ListFragment i węzeł

Aby nasza aplikacja zyskała pełną funkcjonalność, musimy zająć się jeszcze kilkoma zagadnieniami. Pierwszym z nich jest klasa TitlesFragment. To właśnie ona zostaje utworzona za pomocą pliku layout.xml naszej aktywności. Znacznik gra rolę wypełniacza, w którym zostanie umieszczony omawiany fragment. Nie ma tu zdefiniowanej hierarchii widoków tego fragmentu. Kod klasy TitlesFragment został umieszczony na listingu 29.9. Klasa ta służy do wyświetlania listy tytułów. Listing 29.9. Kod klasy TitlesFragment import import import import import import import import import import

android.app.Activity; android.app.ListFragment; android.os.Bundle; android.util.AttributeSet; android.util.Log; android.view.LayoutInflater; android.view.View; android.view.ViewGroup; android.widget.ArrayAdapter; android.widget.ListView;

public class TitlesFragment extends ListFragment { private MainActivity myActivity = null; int mCurCheckPosition = 0;

1048 Android 3. Tworzenie aplikacji @Override public void onInflate(AttributeSet attrs, Bundle savedInstanceState) { Log.v(MainActivity.TAG, "w metodzie onInflate klasy TitlesFragment. Obiekt AttributeSet zawiera:"); for(int i=0; i


Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

1049

super.onActivityCreated(savedState);

// Zapełnia listę tytułami pochodzącymi ze statycznej tabeli. setListAdapter(new ArrayAdapter

(getActivity(), android.R.layout.simple_list_item_1, Shakespeare.TITLES)); if (savedState != null) {

// Odczytuje ostatni stan sprawdzanej pozycji. mCurCheckPosition = savedState.getInt("curChoice", 0); }

// Uzyskuje dostęp do widoku ListView klasy ListFragment i aktualizuje go. ListView lv = getListView(); lv.setChoiceMode(ListView.CHOICE_MODE_SINGLE); lv.setSelection(mCurCheckPosition);

// Aktywność zostaje utworzona, fragmenty stają się dostępne. // Fragment przechowujący szczegóły zostaje zapełniony. myActivity.showDetails(mCurCheckPosition); } @Override public void onStart() { Log.v(MainActivity.TAG, "w metodzie onStart klasy TitlesFragment"); super.onStart(); } @Override public void onResume() { Log.v(MainActivity.TAG, "w metodzie onResume klasy TitlesFragment"); super.onResume(); } @Override public void onPause() { Log.v(MainActivity.TAG, "w metodzie onPause klasy TitlesFragment"); super.onPause(); } @Override public void onSaveInstanceState(Bundle outState) { Log.v(MainActivity.TAG, "w metodzie onSaveInstanceState klasy TitlesFragment"); super.onSaveInstanceState(outState); outState.putInt("curChoice", mCurCheckPosition); } @Override public void onListItemClick(ListView l, View v, int pos, long id) { Log.v(MainActivity.TAG, "w metodzie onListItemClick klasy TitlesFragment. pozycja = " + pos); myActivity.showDetails(pos); mCurCheckPosition = pos; }

1050 Android 3. Tworzenie aplikacji @Override public void onStop() { Log.v(MainActivity.TAG, "w metodzie onStop klasy TitlesFragment"); super.onStop(); } @Override public void onDestroyView() { Log.v(MainActivity.TAG, "w metodzie onDestroyView klasy TitlesFragment"); super.onDestroyView(); } @Override public void onDestroy() { Log.v(MainActivity.TAG, "w metodzie onDestroy klasy TitlesFragment"); super.onDestroy(); } @Override public void onDetach() { Log.v(MainActivity.TAG, "w metodzie onDetach klasy TitlesFragment"); super.onDetach(); myActivity = null; } }

Podobnie jak wcześniej większość zamieszczonego tu kodu jest zbędna z punktu widzenia działania aplikacji i służy jedynie do przechowania instrukcji wyświetlających informacje w dzienniku, dzięki czemu będzie wiadomo, kiedy fragment przechodzi do określonego etapu cyklu życia. W przeciwieństwie do klasy DetailsFragment, w tym fragmencie metoda onCreateView() nie posiada specjalnego przeznaczenia. Wynika to z faktu, że rozszerzamy klasę ListFragment, która już zawiera widok ListView. Domyślne ustawienia metody onCreateView() w klasie ListView powodują przekazanie widoku kontrolki ListView. Właściwe operacje są przeprowadzane dopiero na etapie wywołania metody onActivityCreated(). Do tego czasu możemy być pewni, że zostanie utworzona hierarchia widoków aktywności wraz z hierarchią aktywności fragmentu. Identyfikatorem zasobu dla kontrolki ListView jest android.R.id.list1. Żeby jednak uzyskać do niej odniesienie, należy wywołać metodę getListView() wewnątrz metody onActivityCreated(). Ponieważ jednak klasa ListFragment nie jest tym samym co kontrolka ListView, nie podłączamy adaptera bezpośrednio do widoku ListView. Musimy zamiast tego użyć metody setListAdapter() klasy ListFragment. Ponieważ została skonfigurowana hierarchia widoków aktywności, możemy bezpiecznie powrócić do aktywności, aby wywołać metodę showDetails(). Na tym etapie cyklu życia aktywności dodaliśmy adapter do widoku listy, odczytaliśmy bieżącą pozycję (jeżeli powróciliśmy z etapu przywracania, spowodowanego na przykład zmianą położenia urządzenia) oraz zaprogramowaliśmy aktywność (w metodzie showDetails()), aby wprowadziła tekst związany z odpowiednim tytułem sztuki szekspirowskiej. Klasa TitlesFragment również posiada obiekt nasłuchujący listy, zatem gdy użytkownik zaznaczy inny tytuł, system wywoła metodę zwrotną onListItemClick() i zmieni tekst na odpowiadający danej sztuce, znowu za pomocą metody showDetails().

Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

1051

Kolejnym elementem różniącym ten fragment od wcześniej omawianego fragmentu przechowującego szczegóły jest zapisywanie stanu w pakiecie (wartości wskazującej bieżącą pozycję na liście) oraz odczytywanie jej w metodzie onCreate() podczas zamykania i odtwarzania fragmentu. W przeciwieństwie do fragmentu przechowującego szczegóły, który jest wymieniany w kontrolce FrameLayout układu graficznego aktywności, mamy tu do czynienia z tylko jednym fragmentem przechowującym tytuły. Jeśli więc następuje zmiana konfiguracji i nasz fragment przechodzi przez operację zapisywania i odczytywania, chcemy zapamiętać bieżącą pozycję. W przypadku fragmentów przechowujących szczegóły odtwarzamy je bez konieczności zapamiętywania poprzedniego stanu.

Wywoływanie odrębnej aktywności w razie potrzeby Istnieje część kodu, o której jeszcze nie wspominaliśmy — chodzi o fragment metody show Kod ten przydaje się wtedy, gdy urządzenie znajdujące się w trybie portretowym wyświetla fragment przechowujący szczegóły, który wymiarami nie odpowiada fragmentowi przechowującemu tytuły. Potraktujmy to jako problem, chociaż w przypadku tabletów nie musimy się tym martwić. Ponieważ jednak technika fragmentów staje się dostępna również w starszych wersjach Androida, będziemy mogli korzystać z nich zarówno w telefonach, jak i w tabletach. Oznacza to, że całkiem często będziemy się spotykać z tym, że parametry ekranu uniemożliwią dogodne wyświetlenie fragmentu, który w normalnej sytuacji byłby umieszczony obok innych fragmentów. W takiej sytuacji musimy uruchomić oddzielną aktywność, służącą do wyświetlania tego fragmentu. W naszym przykładzie postanowiliśmy zaimplementować w ten sposób aktywność przechowującą szczegóły; jej kod znajdziemy na listingu 29.10.

´Details().

Listing 29.10. Wyświetlanie nowej aktywności, jeśli określony fragment nie mieści się na ekranie // Jest to plik DetailsActivity.java import import import import

android.app.Activity; android.content.res.Configuration; android.os.Bundle; android.util.Log;

public class DetailsActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { Log.v(MainActivity.TAG, "w metodzie onCreate klasy DetailsActivity"); super.onCreate(savedInstanceState); if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {

// Jeżeli ekran znajduje się w trybie krajobrazowym, oznacza to, // że klasa MainActivity wyświetla zarówno // tytuły, jak i tekst, zatem niniejsza aktywność jest // już niepotrzebna. Zignorujmy ją i pozwólmy klasie MainActivity // zająć się wszystkimi zadaniami. finish(); return; } if(getIntent() != null) {

// Jest to inny sposób utworzenia wystąpienia fragmentu

1052 Android 3. Tworzenie aplikacji // przechowującego szczegóły. DetailsFragment details = DetailsFragment.newInstance(getIntent().getExtras()); getFragmentManager().beginTransaction() .add(android.R.id.content, details) .commit(); } } }

Warto przyjrzeć się kilku interesującym aspektom tego kodu. Przede wszystkim jest on naprawdę prosty do implementacji. Dokonujemy prostego określenia trybu orientacji urządzenia i jeżeli znajduje się ono w trybie portretowym, wstawiamy fragment przechowujący szczegóły do oddzielnej aktywności. W trybie krajobrazowym aktywność MainActivity może wyświetlać obydwa fragmenty obok siebie, zatem nie ma potrzeby pokazywania dodatkowej aktywności. Czytelnik może się zastanawiać, po co chcielibyśmy w ogóle tworzyć tę aktywność w trybie krajobrazowym. Odpowiedź jest prosta: nie chcemy. Jeżeli jednak aktywność przechowująca szczegóły została utworzona w trybie portretowym i użytkownik obróci urządzenie do trybu krajobrazowego, zostanie ona uruchomiona ponownie z powodu zmiany konfiguracji. Otrzymaliśmy więc dodatkową aktywność w trybie krajobrazowym. W tym momencie jedynym rozsądnym rozwiązaniem okazuje się zakończenie tej aktywności i przekazanie klasie MainActivity wszystkich zadań. Kolejnym interesującym aspektem aktywności przechowującej szczegóły jest brak możliwości ustawienia głównego widoku treści za pomocą metody setContentView(). W jaki więc sposób zostaje utworzony interfejs użytkownika? Jeżeli przyjrzymy się uważnie wywołaniu metody add() w transakcji fragmentu, zauważymy, że pojemnik widoków, do którego dodajemy dany fragment, został określony jako zasób android.R.id.content. Jest to główny pojemnik widoków aktywności, zatem jeśli dołączamy do niego hierarchię widoków fragmentów, oznacza to, że hierarchia ta staje się jedyną hierarchią widoków w aktywności. Do utworzenia nowego fragmentu (na przykład przyjmującego pakiet w postaci argumentu) wykorzystaliśmy tutaj dokładnie taką samą klasę DetailsFragment co wcześniej, lecz wprowadziliśmy inną metodę newInstance(), a następnie dołączyliśmy go po prostu do głównego poziomu hierarchii widoków aktywności. W ten sposób fragment zostaje wyświetlony we wnętrzu tej aktywności. Z punktu widzenia użytkownika ogląda on teraz jedynie widok zawierający fragment, w którym jest przechowywany tekst sztuki Szekspira. Jeżeli będzie chciał przejść do innego tytułu, musi wcisnąć przycisk cofania, co spowoduje powrót do głównej aktywności (przechowującej wyłącznie fragment z tytułami). Alternatywnym rozwiązaniem jest obrót urządzenia i przejście do trybu krajobrazowego. Wtedy w aktywności przechowującej szczegóły zostanie wywołana metoda finish(), co spowoduje jej zamknięcie i pojawienie się odtworzonej aktywności głównej. Kiedy urządzenie znajduje się w trybie portretowym i jeśli w głównej aktywności nie wyświetlamy fragmentu przechowującego szczegóły, należy utworzyć osobny plik układu graficznego main.xml, którego zawartość została zaprezentowana na listingu 29.11. Listing 29.11. Układ graficzny aktywności dla trybu portretowego


Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

1053

android:layout_width="match_parent" android:layout_height="match_parent">

Oczywiście, ten układ graficzny można zmodyfikować w dowolny sposób. W celach demonstracyjnych służy on jedynie do wyświetlania fragmentu przechowującego tytuły. Bardzo dobrze się stało, że klasa tego fragmentu nie wymaga dużej ilości kodu do przetwarzania zmian konfiguracji urządzenia. Ostatnim elementem, jaki chcemy dołączyć w tym przykładzie, jest plik AndroidManifest.xml, zaprezentowany na listingu 29.12. Listing 29.12. Plik AndroidManifest.xml

Jest to standardowy plik manifest. Widzimy główną aktywność zawierającą kategorię LAUNCHER, dzięki czemu zostanie ona umieszczona na liście aplikacji urządzenia. Widzimy następnie oddzielną aktywność DetailsActivity ze zdefiniowaną kategorią DEFAULT. W ten sposób możemy uruchomić tę aktywność za pomocą kodu, nie zostanie ona jednak umieszczona na liście aplikacji.

1054 Android 3. Tworzenie aplikacji

Trwałość fragmentów W trakcie testowania omawianej aplikacji nie należy zapominać o obracaniu urządzenia (w emulatorze dokonamy tego za pomocą skrótu klawiaturowego Ctrl+F11). Zauważymy, że wraz z obrotem urządzenia obracać się będą również fragmenty. Jeżeli Czytelnik będzie śledzić komunikaty w oknie LogCat, zauważy, że aplikacja wygeneruje ich bardzo dużo. W szczególności należy zwrócić uwagę na te komunikaty wyświetlane w momencie obracania urządzenia, które dotyczą fragmentów; usuwana i odtwarzana jest nie tylko aktywność, lecz również fragmenty. Dotychczas napisaliśmy jedynie niewielką ilość kodu do obsługi fragmentu przechowującego szczegóły. Kod ten służy do zachowywania bieżącej pozycji na liście tytułów w przypadku ponownego uruchomienia aktywności. W przypadku fragmentów przechowujących szczegóły nie musieliśmy wprowadzać kodu obsługującego zmiany konfiguracji, ponieważ nie ma takiej potrzeby. Android sam obsłuży przechowywanie fragmentów znajdujących się w menedżerze, ich zachowywanie, a następnie odczytywanie w przypadku odtwarzania stanu aktywności. Czytelnik powinien mieć już świadomość, że fragmenty otrzymywane po zmianie konfiguracji najprawdopodobniej nie są tymi samymi fragmentami, które wcześniej znajdowały się w pamięci. Fragmenty te zostały zrekonstruowane. System zachował pakiet argumentów oraz informacje o typie fragmentu, a w przypadku każdego fragmentu przechowującego zapisane informacje o stanie zachował również pakiet z atrybutami zachowanego stanu, służące do późniejszego ich odtworzenia. Komunikaty wyświetlane w oknie LogCat informują nas o fragmentach przechodzących przez cykl życia w synchronizacji z cyklem życia aktywności. Zauważymy, że fragment przechowujący szczegóły zostaje odtworzony, lecz system nie wywołuje ponownie metody newInstance(). Zamiast tego Android korzysta po prostu z domyślnego konstruktora, następnie dołącza do niego pakiet argumentów i rozpoczyna wywoływanie metod zwrotnych danego fragmentu. Dlatego tak ważne jest, aby nie umieszczać żadnego wymyślnego kodu w metodzie newInstance(), ponieważ w momencie odtwarzania fragmentu metoda ta zostanie pominięta. Czytelnik powinien już także docenić możliwość wielokrotnego użytkowania fragmentów w różnych miejscach. Fragment przechowujący tytuły jest wykorzystywany w dwóch różnych układach graficznych, jeśli jednak przyjrzymy się jego kodowi, zauważymy, że atrybuty umieszczone w tych plikach nie mają większego znaczenia. Moglibyśmy utworzyć dwa zupełnie różniące się od siebie układy graficzne, a kod tego fragmentu wyglądałby dokładnie tak samo. To samo można powiedzieć o fragmencie przechowującym szczegóły. Został on wprowadzony do głównego układu graficznego trybu krajobrazowego oraz w aktywności przechowującej szczegóły. Także i w tym przypadku układy graficzne mogą się od siebie znacznie różnić, a kod fragmentu przechowującego szczegóły nie uległby zmianom. Również kod aktywności przechowującej szczegóły był bardzo prosty. Do tej pory analizowaliśmy dwa typy fragmentów: klasę bazową Fragment oraz jej podklasę ListFragment. Przejdziemy teraz do kolejnego elementu klasy Fragment, jakim jest klasa podrzędna DialogFragment.

Fragmenty wyświetlające okna dialogowe W rozdziale 8. omówiliśmy mechanizm okien dialogowych w wersjach systemu starszych od 3.0. Wraz z wersją 3.0 Androida wprowadzono nowy sposób pracy z oknami dialogowymi, oparty na fragmentach. Spodziewamy się, że omówiony w rozdziale 8. protokół zarządzanych okien dialogowych zostanie wyparty przez rozwiązanie omówione poniżej.

Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

1055

W tym podrozdziale Czytelnik dowie się, w jaki sposób wykorzystywać fragmenty do wyświetlania prostego okna dialogowego oraz niestandardowego okna dialogowego, ukazującego tekst zachęty.

Podstawowe informacje o klasie DialogFragment Zanim zademonstrujemy przykładowe aplikacje wyświetlające okno dialogowe zachęty oraz alert, chcielibyśmy najpierw zapoznać Czytelnika z teoretycznymi podstawami stosowania fragmentów wyświetlających okna dialogowe. Funkcjonalności związane z oknami dialogowymi w Androidzie 3.0 zostały zawarte w klasie DialogFragment. Stanowi ona podelement klasy Fragment i w istocie posiada właściwości fragmentu. Będzie to zatem nasza klasa bazowa, służąca do obsługi okien dialogowych. Gdy już utworzymy okno dialogowe pochodzące z tej klasy, na przykład: public class MyDialogFragment extends DialogFragment { ... }

możemy wyświetlić taki fragment MyDialogFragment w postaci okna dialogowego przy użyciu transakcji fragmentu. Na listingu 29.13 umieściliśmy pseudokod obrazujący ten proces. Listing 29.13. Wyświetlanie fragmentu przechowującego dialog JakasAktywnosc {

//...pozostałe funkcje aktywności public void showDialog() {

//konstruuje klasę MyDialogFragment MyDialogFragment mdf = MyDialogFragment.newInstance(arg1,arg2); FragmentManager fm = getFragmentManager(); FragmentTransaction ft = fm.beginTransaction(); mdf.show(ft,"znacznik-mojego-dialogu"); }

//...pozostałe funkcje aktywności }

Zgodnie z listingiem 29.13, aby wyświetlić fragment przechowujący okna dialogowe, należy wykonać następujące czynności: 1. Utworzenie fragmentu wyświetlającego okna dialogowe. 2. Uruchomienie transakcji fragmentu. 3. Wyświetlenie okna dialogowego za pomocą transakcji utworzonej na etapie 2. Przyjrzyjmy się każdemu z wymienionych etapów.

Utworzenie fragmentu wyświetlającego okna dialogowe Podczas tworzenia fragmentu wyświetlającego okna dialogowe kierujemy się takimi zasadami jak w przypadku pozostałych typów fragmentów. Zalecanym wzorcem jest stosowanie metody fabrykującej, takiej jak newInstance(). W jej wnętrzu powinniśmy wykorzystać domyślny konstruktor, a następnie dodać pakiet z argumentami, zawierający przekazywane parametry. Metoda ta nie powinna służyć do innych zadań, ponieważ chcemy mieć pewność, że niczego nie pominiemy w procesie odtwarzania fragmentu z zachowanego stanu. W takim przypadku Android wywoła domyślny konstruktor i odtworzy za jego pomocą pakiet zawierający argumenty.

1056 Android 3. Tworzenie aplikacji Przesłanianie metody onCreateView Podczas dziedziczenia fragmentu wyświetlającego okna dialogowe musimy przesłonić jedną lub dwie metody w celu wprowadzenia hierarchii widoków tego okna dialogowego. Pierwszą możliwością jest przesłonięcie metody onCreateView() i uzyskanie widoku. Drugą opcję stanowi przesłonięcie metody onCreateDialog() i otrzymanie obiektu Dialog (takiego, jaki był tworzony przez klasę AlertDialog.Builder). Na listingu 29.14 zaprezentowaliśmy przykład przesłaniania metody onCreateView(). Listing 29.14. Przesłanianie metody onCreateView() klasy DialogFragment MyDialogFragment {

...inne funkcje public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

//Tworzy widok poprzez rozwinięcie wybranego układu graficznego View v = inflater.inflate(R.layout.prompt_dialog,container,false);

//Możemy zlokalizować widok i wprowadzić wartości TextView tv = (TextView)v.findViewById(R.id.promptmessage); tv.setText(this.getPrompt());

//Możemy wprowadzić metody zwrotne dla przycisków Button dismissBtn = (Button)v.findViewById(R.id.btn_dismiss); dismissBtn.setOnClickListener(this); Button saveBtn = (Button)v.findViewById( R.id.btn_save); saveBtn.setOnClickListener(this); return v; }

...inne funkcje }

W kodzie z listingu 29.14 wczytujemy widok zdefiniowany przez układ graficzny. Następnie wyszukujemy dwa przyciski i określamy dla nich metody zwrotne. W bardzo podobny sposób tworzyliśmy wcześniej fragment przechowujący szczegóły. W przeciwieństwie jednak do prezentowanych wcześniej fragmentów, omawiany typ zawiera jeszcze jeden mechanizm pozwalający na utworzenie hierarchii widoków. Przesłanianie metody onCreateDialog Alternatywą dla umieszczenia widoku w metodzie onCreateView() jest przesłonięcie metody onCreateDialog() i dostarczenie wystąpienia okna dialogowego. Na listingu 29.15 został zaprezentowany przykładowy kod takiego rozwiązania.

Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

1057

Listing 29.15. Przesłonięcie metody onCreateDialog() klasy DialogFragment MyDialogFragment {

...inne funkcje @Override public Dialog onCreateDialog(Bundle icicle) { AlertDialog.Builder b = new AlertDialog.Builder(getActivity()); b.setTitle("Tytuł mojego okna dialogowego"); b.setPositiveButton("OK", this); b.setNegativeButton("Anuluj", this); b.setMessage(this.getMessage()); return b.create(); }

...inne funkcje }

W tym przykładzie wykorzystujemy konstruktor alertów do utworzenia przekazywanego okna dialogowego. Jest to skuteczne rozwiązanie w przypadku prostych okien dialogowych. Pierwsze rozwiązanie, polegające na przesłonięciu metody onCreateView(), jest równie proste i zapewnia o wiele większą swobodę.

Wyświetlanie fragmentu przechowującego okna dialogowe Po utworzeniu fragmentu wyświetlającego okno dialogowe wymagane będzie wykorzystanie transakcji fragmentu. Podobnie jak w przypadku pozostałych typów fragmentów, także i tutaj operacje przeprowadzane są za pośrednictwem transakcji. Metoda show() przyjmuje transakcję w postaci parametru wejściowego. Widzimy to na listingu 29.13. Za pomocą transakcji metoda ta dodaje okno dialogowe do aktywności, a następnie zatwierdza tę transakcję. Metoda show() nie dodaje jednak transakcji do stosu drugoplanowego. Jeżeli chcemy, możemy najpierw dodać transakcję do stosu, a następnie przekazać ją metodzie show(). Metoda ta w przypadku fragmentu wyświetlającego okna dialogowego posiada następujące sygnatury: public int show(FragmentTransaction transaction, String tag) public int show(FragmentManager manager, String tag)

Pierwsza metoda show() wyświetla okno dialogowe poprzez dodanie tego fragmentu do przekazanej transakcji wraz z określonym znacznikiem. Przekazuje ona następnie identyfikator przeprowadzanej transakcji. Druga metoda show() automatyzuje proces otrzymywania transakcji z menedżera transakcji. Jest to metoda skrótowa. Jednak w przypadku korzystania z niej tracimy możliwość umieszczenia transakcji w stosie drugoplanowym. Jeżeli chcemy uzyskać kontrolę nad tym aspektem, musimy zastosować metodę o pierwszej z wymienionych sygnatur. Druga metoda okazuje się przydatna, gdy chcemy wyświetlić jedynie okno dialogowe i nie mamy innego powodu, aby w danym momencie przeprowadzać transakcję fragmentu.

1058 Android 3. Tworzenie aplikacji Bardzo miłą implikacją okna dialogowego w postaci fragmentu jest fakt, iż menedżer fragmentów zarządza stanami w podstawowym zakresie. Jeśli na przykład urządzenie zostanie obrócone w chwili wyświetlania okna dialogowego, zostanie ono odtworzone całkowicie bez naszego udziału. Fragment wyświetlający okna dialogowe zawiera również metody pozwalające na kontrolowanie ramki, w której jest wyświetlany widok okna dialogowego, w tym takie właściwości, jak jej tytuł lub wygląd. Więcej opcji znajdziemy w dokumentacji klasy DialogFragment; łącze do tej dokumentacji zamieszczono na końcu rozdziału.

Odwołanie fragmentu wyświetlającego okna dialogowe Fragment wyświetlający okna dialogowe można odwołać na dwa sposoby. Pierwszym z nich jest jawne wywołanie metody dismiss() w odpowiedzi na wciśnięcie przycisku lub jakieś działanie na widoku okna dialogowego, co zostało ukazane na listingu 29.16. Listing 29.16. Wywołanie metody dismiss() if (someview.getId() == R.id.btn_dismiss) {

//Wykorzystajmy jakieś metody zwrotne powiadamiające klientów //tego okna dialogowego o jego odwołaniu, //a następnie wywołajmy omawianą metodę. dismiss(); return; }

Metoda dismiss() usunie ten fragment z menedżera fragmentów, a następnie przeprowadzi odpowiednią transakcję. Jeżeli dany fragment znajduje się na stosie drugoplanowym, metoda ta spowoduje po prostu jego wycofanie, a na jego miejsce wejdzie poprzedni stan transakcji fragmentu. Bez względu na to, czy dostępny jest stos drugoplanowy, czy nie, wywołanie metody dismiss() poskutkuje wywołaniem standardowych metod zwrotnych usuwających fragment wyświetlający okna dialogowe, w tym również onDismiss(). Należy zauważyć, że obecność metody onDismiss() wcale nie musi oznaczać wywołania metody dismiss(). Wynika to z faktu, iż metoda onDismiss() jest wywoływana również podczas zmiany konfiguracji urządzenia i z tego powodu nie nadaje się do określania czynności, jakie użytkownik przeprowadził na oknie dialogowym. Jeżeli okno dialogowe jest wyświetlane w trakcie zmiany trybu wyświetlania obrazu, we fragmencie zostanie wywołana metoda onDismiss(), nawet jeśli użytkownik nie wcisnął żadnego przycisku w tym oknie dialogowym. Zamiast tego powinniśmy prawdopodobnie zawsze polegać na jawnych zdarzeniach kliknięć przycisku znajdującego się w widoku okna dialogowego. Jeżeli użytkownik wciśnie przycisk cofania w trakcie wyświetlania fragmentu zawierającego okno dialogowe, spowoduje to wywołanie metody zwrotnej onCancel() w tym fragmencie. Domyślnie system pozbędzie się fragmentu i nie będzie trzeba samodzielnie wywoływać metody dismiss(). Jeżeli jednak chcemy, aby aktywność wywołująca została powiadomiona o anulowaniu okna dialogowego, będziemy musieli w tym celu wprowadzić odpowiednią logikę do metody onCancel(). Na tym polega różnica pomiędzy metodami onDismiss() i onCancel()

Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

1059

we fragmentach wyświetlających okna dialogowe. W przypadku metody onDismiss() cały czas nie będziemy mieli pewności, co spowodowało jej wywołanie. Mogliśmy również odnotować fakt, że fragment ten nie posiada metody cancel(), jedynie dismiss(), lecz — jak już stwierdziliśmy — w momencie wciśnięcia przycisku cofania system samodzielnie zajmuje się procesem jego anulowania czy też wycofania. Alternatywnym sposobem wycofania fragmentu wyświetlającego okno dialogowe jest wprowadzenie innego fragmentu tego typu. Mechanizm wycofania starego fragmentu i wprowadzenia nowego różni się nieco od zwyczajnego wycofania bieżącego fragmentu. Na listingu 29.17 zamieściliśmy stosowny przykład. Listing 29.17. Konfigurowanie dialogu przeznaczonego do stosu drugoplanowego if (someview.getId() == R.id.btn_invoke_another_dialog) { Activity act = getActivity(); FragmentManager fm = act.getFragmentManager(); FragmentTransaction ft = fm.beginTransaction(); ft.remove(this); ft.addToBackStack(null);

//Wartość null symbolizuje brak nazwy dla transakcji przeprowadzanej w stosie drugoplanowym HelpDialogFragment hdf = HelpDialogFragment.newInstance(R.string.helptext); hdf.show(ft, "NA POMOC"); return; }

W obrębie jednej transakcji usuwamy bieżący fragment i dodajemy nowy fragment wyświetlający okna dialogowe. Wizualnie poprzednie okno dialogowe znika, a w jego miejscu pojawia się nowe. Jeżeli użytkownik wciśnie przycisk cofania, nowe okno dialogowe zostanie wycofane i zostanie wyświetlone poprzednie okno dialogowe, ponieważ zachowaliśmy tę transakcję w stosie drugoplanowym. Jest to bardzo przydatny sposób wyświetlania na przykład okna dialogowego pomocy.

Skutki wycofania okna dialogowego Gdy dodajemy dowolny fragment do menedżera fragmentów, menedżer ten będzie zarządzał jego stanami. Oznacza to, że w trakcie zmiany konfiguracji urządzenia (wynikającego na przykład ze zmiany orientacji wyświetlacza) aktywność, wraz z fragmentami, zostanie uruchomiona ponownie. Zostało to zademonstrowane na przykładzie zawierającym cytaty z dzieł Szekspira. Zmiana konfiguracji urządzenia nie wpływa na okna dialogowe, ponieważ są one również zarządzane przez menedżer fragmentów. Jednak niejawne zachowanie metod show() i dismiss() sprawia, że jeśli nie zachowamy ostrożności, dość łatwo możemy zgubić dany fragment wyświetlający okna dialogowe. Metoda show() automatycznie dodaje fragment do menedżera; z kolei metoda dismiss() automatycznie go stamtąd usuwa. Być może uzyskamy bezpośredni wskaźnik fragmentu przed jego wyświetleniem, nie będziemy jednak mogli dodać później tego fragmentu do menedżera za pomocą metody show(), ponieważ fragment może zostać dodany

1060 Android 3. Tworzenie aplikacji do tego menedżera tylko jednorazowo. Może zaistnieć potrzeba odczytania tego wskaźnika poprzez odtworzenie aktywności. Jeżeli jednak chcemy pokazać, a następnie wycofać okno dialogowe, fragment zostanie niejawnie usunięty z menedżera fragmentów, co jest jednoznaczne z uniemożliwieniem jego odtworzenia i ponownego wskazania (ponieważ menedżer nie będzie posiadał informacji, że fragment ten istnieje). Jeżeli chcemy utrzymać stan okna dialogowego po jego wycofaniu, będziemy musieli go w jakiś sposób przechować na zewnątrz — albo w nadrzędnej aktywności, albo we fragmencie niezwiązanym z oknami dialogowymi, który nie zostanie szybko usunięty.

Przykładowa aplikacja wykorzystująca klasę DialogFragment Utworzymy teraz przykładową aplikację, za pomocą której przedstawimy trzy koncepcje fragmentu wyświetlającego okna dialogowe. Przyjrzymy się również mechanizmowi komunikacji pomiędzy fragmentem a przechowującą go aktywnością. Aby tego dokonać, będziemy potrzebować pięciu plików: „ MainActivity.java jest główną aktywnością aplikacji. Będzie ona wyświetlała prosty widok zawierający tekst pomocniczy oraz menu, za pomocą którego będą uruchamiane okna dialogowe. „ PromptDialogFragment.java stanowi przykład fragmentu wyświetlającego okna dialogowe, w którym zostaje zdefiniowany osobny plik układu graficznego. Jest tu możliwa interakcja z użytkownikiem. Dostępne są trzy przyciski: Zachowaj, Wycofaj (na przykład służący do anulowania) oraz Pomoc. „ AlertDialogFragment.java to przykładowy fragment wyświetlający okna dialogowe, który wykorzystuje klasę AlertBuilder do utworzenia okna dialogowego wewnątrz tego fragmentu. Mamy tu do czynienia z klasyczną metodą tworzenia okna dialogowego; możemy wykorzystać tu wiedzę nabytą podczas tworzenia zwykłych okien dialogowych. „ HelpDialogFragment.java jest bardzo prostym fragmentem wyświetlającym komunikat pomocy, umieszczony w zasobach aplikacji. Dana wiadomość zostaje określona w momencie tworzenia okna dialogowego. Obiekt ten jest wyświetlany zarówno z poziomu głównej aktywności, jak i fragmentu wyświetlającego okno zachęty. „ OnDialogDoneListener.java zawiera w sobie interfejs wymagany przez aktywność w celu otrzymywania komunikatów pochodzących z fragmentów. Stosując ten interfejs, stwierdzamy, że fragmenty nie muszą posiadać wielu informacji na temat aktywności wywołującej, wystarczy im informacja, że aktywność ta implementuje omawiany interfejs. W ten sposób wszystkie funkcje mogą się znajdować na swoich miejscach. Z punktu widzenia aktywności jest to bardzo popularny sposób otrzymywania komunikatów z fragmentów bez posiadania zbyt wielu informacji na ich temat. Nasza przykładowa aplikacja zawiera trzy układy graficzne: dla głównej aktywności, dla fragmentu wyświetlającego okno zachęty oraz dla fragmentu wyświetlającego okno pomocy. Zauważmy, że nie potrzebujemy układu graficznego dla fragmentu wyświetlającego alert, ponieważ jego utworzeniem zajmie się klasa AlertBuilder. Po utworzeniu i uruchomieniu aplikacji zobaczymy ekran widoczny na rysunku 29.3.

Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

1061

Rysunek 29.3. Interfejs użytkownika przykładowej aplikacji — fragment przechowujący okno dialogowe

Przykładowe okno dialogowe — klasa MainActivity Przejdźmy do kodu źródłowego. Na listingu 29.18 znajduje się kod głównej aktywności. Listing 29.18. Główna aktywność fragmentu wyświetlającego okna dialogowe // Jest to plik MainActivity.java import import import import import import import import import

android.app.Activity; android.app.FragmentManager; android.app.FragmentTransaction; android.os.Bundle; android.util.Log; android.view.Menu; android.view.MenuInflater; android.view.MenuItem; android.widget.Toast;

public class MainActivity extends Activity implements OnDialogDoneListener { public static final String LOGTAG = "DialogFragmentDemo"; public static final String ALERT_DIALOG_TAG = "ALERT_DIALOG_TAG"; public static final String HELP_DIALOG_TAG = "HELP_DIALOG_TAG"; public static final String PROMPT_DIALOG_TAG = "PROMPT_DIALOG_TAG"; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); FragmentManager.enableDebugLogging(true); }

1062 Android 3. Tworzenie aplikacji @Override public boolean onCreateOptionsMenu(Menu menu){ super.onCreateOptionsMenu(menu); MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == R.id.menu_show_alert_dialog) { this.testAlertDialog(); return true; } if (item.getItemId() == R.id.menu_show_prompt_dialog) { this.testPromptDialog(); return true; } if (item.getItemId() == R.id.menu_help) { this.testHelpDialog(); return true; } return true; } private void testPromptDialog() { FragmentTransaction ft = getFragmentManager().beginTransaction(); PromptDialogFragment pdf = PromptDialogFragment.newInstance("Wprowadź jakieś dane"); pdf.show(ft, PROMPT_DIALOG_TAG); } private void testAlertDialog() { FragmentTransaction ft = getFragmentManager().beginTransaction(); AlertDialogFragment adf = AlertDialogFragment.newInstance("Komunikat alertu"); adf.show(ft, ALERT_DIALOG_TAG); } private void testHelpDialog() { FragmentTransaction ft = getFragmentManager().beginTransaction(); HelpDialogFragment hdf = HelpDialogFragment.newInstance(R.string.help_text);

Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

1063

hdf.show(ft, HELP_DIALOG_TAG); } public void onDialogDone(String tag, boolean cancelled, CharSequence message) { String s = tag + " reaguje na: " + message; if(cancelled) s = tag + " zostało anulowane przez użytkownika"; Toast.makeText(this, s, Toast.LENGTH_LONG).show(); Log.v(LOGTAG, s); } }

Kod głównej aktywności jest niezwykle prosty. W metodzie onCreate() ustanawiamy widok treści i włączamy debugowanie menedżera fragmentów. Widzimy następnie dwie metody związane z konfigurowaniem opcji menu. Wybór poszczególnych opcji menu powoduje wywołanie różnych metod o prostej budowie. Każda z tych metod wykonuje praktycznie takie samo zadanie: pozyskuje transakcję fragmentu, a następnie tworzy i wyświetla dany fragment. Zwróćmy uwagę, że każdy fragment posiada niepowtarzalny znacznik, który zostaje dostarczony metodzie show(). Znacznik ten zostaje powiązany z fragmentem w menedżerze, dzięki czemu możemy później lokalizować fragmenty. Fragmenty mogą również same określać wartość znacznika za pomocą metody getTag() klasy Fragment. Ostatnią definicją metody w naszej głównej aktywności jest onDialogDone(), która jest częścią implementowanego interfejsu OnDialogDoneListener. Jak widać, omawiana metoda zwrotna zawiera znacznik wywołującego fragmentu, wartość logiczną wskazującą, czy fragment został anulowany, oraz komunikat. Dla naszych celów wystarczy wiedza, że komunikat zostaje wyświetlony w oknie LogCat; jest on również prezentowany użytkownikowi za pomocą kontrolki Toast.

Przykładowe okno dialogowe — interfejs OnDialogDoneListener Skoro pokazaliśmy, w jaki sposób ustalić moment zniknięcia okna dialogowego, utworzymy interfejs obiektu nasłuchującego implementowany przez obiekty, które wywołują okna dialogowe. Kod tego interfejsu został zaprezentowany na listingu 29.19. Listing 29.19. Interfejs obiektu nasłuchującego // Jest to plik OnDialogDoneListener.java /* * Interfejs standardowo implementowany przez aktywność, * dzięki czemu okno dialogowe może przesyłać komunikaty * o zdarzeniach. */ public interface OnDialogDoneListener { public void onDialogDone(String tag, boolean cancelled, CharSequence message); }

Jak widać, mamy do czynienia z bardzo prostym interfejsem. Wybraliśmy tylko jedną metodę zwrotną dla tego interfejsu. Metoda ta koniecznie musi być zaimplementowana przez aktywność. Nasze fragmenty nie muszą posiadać informacji o szczegółach aktywności wywołującej, a jedynie o tym, że aktywność ta musi implementować interfejs OnDialogDoneListener.

1064 Android 3. Tworzenie aplikacji Fragmenty mogą więc za pomocą tej metody zwrotnej komunikować się z aktywnością wywołującą. W zależności od przeznaczenia fragmentu w interfejsie tym może się znajdować wiele metod zwrotnych. Nasza przykładowa aplikacja rozdziela interfejs od definicji klas fragmentów. Aby ułatwić sobie zarządzanie kodem, możemy zamieścić interfejs obiektu nasłuchującego fragment wewnątrz samej definicji klasy fragmentu, a tym samym zapewnić sobie większą kontrolę nad synchronizacją obiektu nasłuchującego z fragmentem.

Przykładowe okno dialogowe — klasa PromptDialogFragment Przyjrzyjmy się teraz pierwszemu fragmentowi — PromptDialogFragment, którego układ graficzny oraz kod Java zostały razem umieszczone na listingu 29.20. Listing 29.20. Układ graficzny i kod Java klasy PromptDialogFragment


Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

android:text="Zachowaj">

// Jest to plik PromptDialogFragment.java import import import import import import import import import import import import

android.app.Activity; android.app.DialogFragment; android.app.FragmentTransaction; android.content.DialogInterface; android.os.Bundle; android.util.Log; android.view.LayoutInflater; android.view.View; android.view.ViewGroup; android.widget.Button; android.widget.EditText; android.widget.TextView;

public class PromptDialogFragment extends DialogFragment implements View.OnClickListener { private EditText et; public static PromptDialogFragment newInstance(String prompt) { PromptDialogFragment pdf = new PromptDialogFragment(); Bundle bundle = new Bundle(); bundle.putString("zachęta",prompt); pdf.setArguments(bundle); return pdf; } @Override public void onAttach(Activity act) {

// Jeżeli aktywność, do której dołączyliśmy, nie // posiada zaimplementowanego interfejsu OnDialogDoneListener,

1065

1066 Android 3. Tworzenie aplikacji // poniższy wiersz spowoduje wyświetlenie wyjątku // ClassCastException. Jest to najwcześniejszy etap, // w którym możemy przetestować zachowanie aktywności. OnDialogDoneListener test = (OnDialogDoneListener)act; super.onAttach(act); } @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); this.setCancelable(true); int style = DialogFragment.STYLE_NORMAL, theme = 0; setStyle(style,theme); } public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle icicle) { View v = inflater.inflate(R.layout.prompt_dialog, container, false); TextView tv = (TextView)v.findViewById(R.id.promptmessage); tv.setText(getArguments().getString("zachęta")); Button dismissBtn = (Button)v.findViewById(R.id.btn_dismiss); dismissBtn.setOnClickListener(this); Button saveBtn = (Button)v.findViewById(R.id.btn_save); saveBtn.setOnClickListener(this); Button helpBtn = (Button)v.findViewById(R.id.btn_help); helpBtn.setOnClickListener(this); et = (EditText)v.findViewById(R.id.inputtext); if(icicle != null) et.setText(icicle.getCharSequence("wejście")); return v; } @Override public void onSaveInstanceState(Bundle icicle) { icicle.putCharSequence("wejście", et.getText()); super.onPause(); } @Override public void onCancel(DialogInterface di) { Log.v(MainActivity.LOGTAG, "w metodzie onCancel () fragmentu PDF"); super.onCancel (di); } @Override public void onDismiss(DialogInterface di) { Log.v(MainActivity.LOGTAG, "w metodzie onDismiss() fragmentu PDF"); super.onDismiss(di);

Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

1067

} public void onClick(View v) { OnDialogDoneListener act = (OnDialogDoneListener)getActivity(); if (v.getId() == R.id.btn_save) { TextView tv = (TextView)getView().findViewById(R.id.inputtext); act.onDialogDone(this.getTag(), false, tv.getText()); dismiss(); return; } if (v.getId() == R.id.btn_dismiss) { act.onDialogDone(this.getTag(), true, null); dismiss(); return; } if (v.getId() == R.id.btn_help) { FragmentTransaction ft = getFragmentManager().beginTransaction(); ft.remove(this);

// W tym przypadku chcemy wyświetlić tekst pomocy // i po zakończeniu powrócić do poprzedniego okna dialogowego. ft.addToBackStack(null);

//Wartość null oznacza brak nazwy dla transakcji stosu drugoplanowego.

} }

HelpDialogFragment hdf = HelpDialogFragment.newInstance(R.string.help1); hdf.show(ft, MainActivity.HELP_DIALOG_TAG); return; }

Układ graficzny okna dialogowego zachęty nie różni się od wcześniej tworzonych układów graficznych. Widzimy w nim kontrolkę TextView zawierającą tekst zachęty, kontrolkę EditText, w której użytkownik wprowadza własne dane, oraz trzy przyciski, zapewniające obsługę, kolejno, zachowania danych wejściowych, wycofania (na przykład anulowania) fragmentu wyświetlającego okna dialogowe oraz wyświetlania okna dialogowego pomocy. Kod klasy PromptDialogFragment na początku niczym się nie różni od innych wcześniej utworzonych fragmentów. Znalazła się tu statyczna metoda newInstance() służąca do tworzenia nowych obiektów, a w jej wnętrzu wywołujemy domyślny konstruktor i generujemy pakiet argumentów, który zostaje następnie dołączony do tego obiektu. Następnie Czytelnik zapewne spostrzegł coś nowego w metodzie onAttach(). Chodzi o to, aby się upewnić, że aktywność dołączająca posiada zaimplementowany interfejs OnDialogDoneListener. Aby to sprawdzić, rzutujemy aktywność przekazywaną do interfejsu OnDialogDoneListener. Jeżeli interfejs nie jest zaimplementowany w aktywności, zostanie wyświetlony wyjątek ClassCastException. Moglibyśmy spróbować obejść ten wyjątek i wprowadzić bardziej eleganckie rozwiązanie, zależy nam jednak na utrzymaniu jak najmniejszej złożoności kodu.

1068 Android 3. Tworzenie aplikacji Następnie umieściliśmy metodę zwrotną onCreate(). Zgodnie z powszechnym trendem związanym z pracą z fragmentami nie tworzymy tutaj interfejsu użytkownika, lecz możemy zdefiniować styl okna dialogowego. Jest to rozwiązanie specyficzne dla fragmentów wyświetlających okna dialogowe. Możemy samodzielnie ustalić styl i motyw lub określić jedynie styl i wprowadzić motyw o wartości 0 (zero), aby pozostawić systemowi swobodę w kwestii jego doboru. W metodzie onCreateView() tworzymy hierarchię widoków danego fragmentu wyświetlającego okno dialogowe. Podobnie jak w przypadku pozostałych typów fragmentów, nie dołączamy hierarchii widoków do przekazywanego pojemnika widoków (na przykład poprzez ustawienie wartości false w parametrze attachToRoot). Następnie konfigurujemy metody zwrotne przycisków i wstawiamy tekst zachęty do okna dialogowego, które zostało pierwotnie przekazane do metody newInstance(). Na końcu sprawdzamy, czy poprzez pakiet zachowanych stanów nie są przekazywane wartości. Wynikałoby z tego, że fragment został odtworzony, najprawdopodobniej w wyniku zmiany konfiguracji, oraz że użytkownik mógł już wprowadzić jakiś tekst. Jeśli tak jest, musimy zapełnić kontrolkę EditText informacjami wprowadzonymi przez użytkownika. Pamiętajmy, że z powodu zmiany konfiguracji mamy do czynienia z innym obiektem widoku w pamięci, zatem musimy go zlokalizować i ustawić odpowiedni tekst. Kolejną metodą zwrotną jest onSaveInstanceState(); to właśnie w niej zapisujemy w pakiecie dowolny bieżący tekst wprowadzony przez użytkownika. Metody zwrotne onCancel() i onDismiss() zaprezentowaliśmy jedynie z powodu możliwości zapisywania informacji w oknie dziennika, więc bez problemu zauważymy, kiedy zostaną one uruchomione w trakcie cyklu życia fragmentu. Ostatnia metoda we fragmencie wyświetlającym okno zachęty jest przeznaczona do obsługi przycisków. Po raz kolejny uzyskujemy odniesienie do otaczającej aktywności i rzutujemy ją na interfejs, który jest przez nią implementowany. Jeżeli użytkownik wcisnął przycisk Zapisz, pobieramy wpisany tekst i wywołujemy metodę zwrotną interfejsu onDialogDone(). Jak zostało wcześniej ukazane, metoda ta pobiera nazwę znacznika fragmentu, wartość logiczną wskazującą, czy dany fragment został anulowany, oraz komunikat, który w tym przypadku jest tekstem wprowadzonym przez użytkownika. Następnie wywołujemy metodę dismiss(), aby usunąć fragment wyświetlający okna dialogowe. Pamiętajmy, że metoda ta nie tylko wizualnie usuwa fragment sprzed oczu użytkownika, lecz również wycofuje go z menedżera fragmentów, więc fragment ten staje się zupełnie niedostępny. Jeżeli zostanie wciśnięty przycisk Wycofaj, ponownie wywołujemy metodę zwrotną interfejsu, tym razem niezawierającą komunikatu, i wywołujemy metodę dismiss(). Z kolei jeśli użytkownik wciśnie przycisk Pomoc, nie chcemy w rzeczywistości utracić fragmentu wyświetlającego okno dialogowe, więc przeprowadzamy nieco odmienną operację. Została ona wcześniej omówiona. Aby zapamiętać okno zachęty, do którego będzie można wrócić później, musimy utworzyć transakcję fragmentu usuwającą to okno i dodającą okno pomocy za pomocą metody show(); powinniśmy je umieścić w stosie drugoplanowym. Zwróćmy także uwagę na sposób utworzenia fragmentu wyświetlającego okno pomocy za pomocą odniesienia do identyfikatora zasobu. Oznacza to, że fragment ten może być użyty wraz z dowolnym tekstem umieszczonym w aplikacji.

Przykładowe okno dialogowe — klasa HelpDialogFragment Niebawem zaprezentujemy kod fragmentu wyświetlającego okno pomocy, najpierw jednak opiszemy mechanizm jego działania. Utworzyliśmy transakcję fragmentu powodującą przejście od fragmentu wyświetlającego okno zachęty do fragmentu przechowującego okno pomocy

Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

1069

i umieściliśmy ją na stosie drugoplanowym. W wyniku tego fragment wyświetlający okno zachęty znika z pojemnika widoków, ciągle jest jednak dostępny w menedżerze fragmentów oraz z poziomu stosu drugoplanowego. Na jego miejscu pojawia się nowy fragment wyświetlający okno pomocy, w którym zawarto widoczny dla użytkownika tekst pomocy. W momencie wycofania omawianego fragmentu jego wpis zostanie usunięty ze stosu, w wyniku czego zniknie (zarówno sprzed oczu użytkownika, jak również z poziomu menedżera fragmentów) i zostanie przywrócony fragment wyświetlający okno zachęty. Cała operacja jest w rzeczywistości banalna do przeprowadzenia. Kod zamieszczony na listingu 29.21 jest niezwykle prosty, a jednocześnie niesamowicie skuteczny; działa bezbłędnie nawet wtedy, gdy podczas wyświetlania okna dialogowego zmieni się tryb wyświetlania. Listing 29.21. Układ graficzny i kod Java klasy HelpDialogFragment

// Jest to plik HelpDialogFragment.java import import import import import import import

android.app.DialogFragment; android.os.Bundle; android.view.LayoutInflater; android.view.View; android.view.ViewGroup; android.widget.Button; android.widget.TextView;

public class HelpDialogFragment extends DialogFragment implements View.OnClickListener {

1070 Android 3. Tworzenie aplikacji public static HelpDialogFragment newInstance(int helpResId) { HelpDialogFragment hdf = new HelpDialogFragment(); Bundle bundle = new Bundle(); bundle.putInt("zasób pomocy", helpResId); hdf.setArguments(bundle); return hdf; } @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); this.setCancelable(true); int style = DialogFragment.STYLE_NORMAL, theme = 0; setStyle(style,theme); } public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle icicle) { View v = inflater.inflate(R.layout.help_dialog, container, false); TextView tv = (TextView)v.findViewById(R.id.helpmessage); tv.setText(getActivity().getResources() .getText(getArguments().getInt("zasób pomocy"))); Button closeBtn = (Button)v.findViewById(R.id.btn_close); closeBtn.setOnClickListener(this); return v; } public void onClick(View v) { dismiss(); } }

Mamy tu do czynienia z kolejnym fragmentem wyświetlającym okno dialogowe, jeszcze prostszym od prezentowanego wcześniej. Zadaniem tego fragmentu jest wyświetlanie tekstu pomocy. Na układ graficzny składa się kontrolka TextView i przycisk Zamknij. Kod Java powinien wyglądać już znajomo dla Czytelnika. Znajdziemy w nim metody newInstance() służącą do utworzenia nowego fragmentu, który wyświetla okno pomocy, onCreate() pozwalającą na zdefiniowanie stylu i motywu okna dialogowego, a także onCreateView() generującą hierarchię widoków. W naszym przypadku potrzebny jest nam zasób typu string, którym posłużymy się do zapełnienia kontrolki TextView, zatem uzyskujemy dostęp do zasobów z poziomu aktywności i wybieramy identyfikator zasobu przekazany w metodzie newInstance(). Na końcu metoda onCreateView() ustanawia procedurę obsługi kliknięcia przycisku Zamknij. W tym przypadku w czasie zamykania okna system nie wykonuje niczego nadzwyczajnego.

Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

1071

Istnieją dwa sposoby wywoływania tego fragmentu: z poziomu aktywności oraz poprzez fragment wyświetlający okno zachęty. Jeżeli wybierzemy pierwsze rozwiązanie, późniejsze wycofanie tego fragmentu poskutkuje jego usunięciem ze szczytu stosu i wyświetleniem głównej aktywności znajdującej się pod spodem. Jeżeli wyświetlimy ten fragment z poziomu fragmentu wyświetlającego okno zachęty, jego wycofanie spowoduje wycofanie transakcji fragmentu (ponieważ fragment ten był częścią transakcji umieszczonej w stosie drugoplanowym) i usunięcie okna pomocy wraz z jednoczesnym przywróceniem fragmentu wyświetlającego okno zachęty. W efekcie użytkownik ponownie ujrzy okno zachęty.

Przykładowe okno dialogowe — klasa AlertDialogFragment Pozostał nam do zaprezentowania ostatni fragment wyświetlający okno dialogowe, mianowicie okno dialogowe alertu. Chociaż możemy utworzyć go podobnie jak w przypadku wcześniej zaprezentowanego fragmentu wyświetlającego okno pomocy, istnieje alternatywne rozwiązanie, pozwalające na wykorzystanie starszej struktury klasy AlertBuilder. Rozwiązanie to okazywało się skuteczne w wielu poprzednich wersjach systemu Android. Listing 29.22 zawiera kod źródłowy fragmentu wyświetlającego okno alertu. Listing 29.22. Kod Java klasy AlertDialogFragment import import import import import

android.app.AlertDialog; android.app.Dialog; android.app.DialogFragment; android.content.DialogInterface; android.os.Bundle;

public class AlertDialogFragment extends DialogFragment implements DialogInterface.OnClickListener { public static AlertDialogFragment newInstance(String message) { AlertDialogFragment adf = new AlertDialogFragment(); Bundle bundle = new Bundle(); bundle.putString("komunikat alertu",message); adf.setArguments(bundle); return adf; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.setCancelable(true); int style = DialogFragment.STYLE_NORMAL, theme = 0; setStyle(style,theme); } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { AlertDialog.Builder b =

1072 Android 3. Tworzenie aplikacji new AlertDialog.Builder(getActivity()); b.setTitle("Uwaga!!"); b.setPositiveButton("OK", this); b.setNegativeButton("Anuluj", this); b.setMessage(this.getArguments().getString("komunikat alertu")); return b.create(); } public void onClick(DialogInterface dialog, int which) { OnDialogDoneListener act = (OnDialogDoneListener) getActivity(); boolean cancelled = false; if (which == AlertDialog.BUTTON_NEGATIVE) { cancelled = true; } act.onDialogDone(getTag(), cancelled, "Alert odwołany"); } }

W przypadku tego fragmentu nie potrzebujemy układu graficznego, ponieważ klasa AlertBuilder zapewnia jego utworzenie. Czytelnik zauważy, że ten fragment jest tworzony tak jak pozostałe, jednak zamiast metody zwrotnej onCreateView() stosujemy w tym przypadku metodę onCreateDialog(). Implementujemy albo metodę onCreateView(), albo onCreateDialog(), nigdy obydwie naraz. Element przekazywany przez metodę onCreateDialog() nie jest widokiem, lecz oknem dialogowym. Od tego miejsca możemy zacząć wykorzystywać informacje przedstawione w rozdziale 8., aby utworzyć okno dialogowe w standardowy sposób. Różnica polega na tym, że w celu uzyskania dostępu do parametrów okna dialogowego powinniśmy wziąć pod uwagę pakiet parametrów. W naszej przykładowej aplikacji wykorzystujemy go do zaprezentowania komunikatu alertu, nie ma jednak przeszkód, aby uzyskać dostęp do innych parametrów pakietu. Zwróćmy także uwagę, że w przypadku tego typu fragmentu wyświetlającego okno dialogowe wymagana jest implementacja interfejsu DialogInterface.OnClickListener w klasie fragmentu, co oznacza, że nasz fragment musi implementować metodę zwrotną onClick(). Metoda ta zostanie wywołana, jeżeli użytkownik zacznie w jakiś sposób wpływać na okno dialogowe. Po raz wtóry uzyskujemy odniesienie do uruchomionego okna dialogowego oraz wskazanie, który przycisk został wciśnięty. Podobnie jak poprzednio, nie możemy polegać na metodzie onDismiss(), ponieważ może ona zostać wywołana wskutek zmiany konfiguracji urządzenia.

Przykładowe okno dialogowe — główny układ graficzny Aby nasza aplikacja była kompletna, musimy wstawić również układ graficzny głównej aktywności. Odpowiedni kod został zaprezentowany na listingu 29.23. Listing 29.23. Główny układ graficzny


Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

1073

android:layout_height=" match_parent" android:gravity="fill" >

Po uruchomieniu aplikacji należy przetestować wszystkie opcje w różnorodnych trybach wyświetlania obrazu. Spróbujmy obrócić urządzenie w momencie wyświetlania fragmentów. Czytelnikowi powinno się spodobać, że okna dialogowe obracają się wraz z resztą obrazu oraz że nie trzeba zbytnio przejmować się kodem odpowiedzialnym za zachowywanie i odczytywanie fragmentów w trakcie zmian konfiguracji. Mamy nadzieję, że kolejnym elementem, jaki Czytelnik doceni, jest łatwość nawiązywania komunikacji pomiędzy fragmentami a aktywnością. Oczywiście, aktywność zawiera odniesienia (lub może je uzyskać) do wszystkich istniejących fragmentów, posiada więc możliwość uzyskania dostępu do metod eksponowanych przez te fragmenty. Nie jest to jedyny sposób komunikowania się fragmentów z aktywnością. Możemy zawsze zastosować metody pobierające wobec menedżera fragmentów w celu odczytania wystąpienia zarządzanego fragmentu, a następnie odpowiednio rzutować takie odniesienie i bezpośrednio wywołać metodę wobec tego fragmentu. Stopień odizolowania fragmentów od siebie za pomocą interfejsów oraz poprzez aktywności lub utworzenia sieci zależności pomiędzy fragmentami zależy od złożoności aplikacji oraz stopnia jej wykorzystania.

Inne formy komunikowania się z fragmentami Zademonstrowaliśmy elegancki sposób komunikowania się fragmentów pomiędzy sobą, polegający na definiowaniu i wykorzystywaniu interfejsu służącego do implementacji metod zwrotnych z fragmentów wprost w aktywności wywołującej. Nie jest to jedyny mechanizm komunikowania się fragmentów ze sobą. Ponieważ menedżer fragmentów ma dostęp do informacji na temat wszystkich fragmentów dołączonych do bieżącej aktywności, aktywność ta lub zawarty w niej fragment mogą zażądać takich informacji dotyczących dowolnego innego fragmentu przy użyciu uprzednio opisanych metod pobierających. Po uzyskaniu odniesienia do fragmentu aktywność lub jej fragment mogą go w odpowiedni sposób rzutować, a następnie spowodować bezpośrednie wywołanie metod wobec tej aktywności lub jej fragmentu. W ten sposób fragmenty mogą uzyskać więcej informacji na temat innych fragmentów, niż byłoby to wymagane w zwykłych przypadkach. Nie należy jednak zapominać, że w tym przypadku aplikacja jest uruchomiona na urządzeniu mobilnym, zatem niekiedy zastosowanie pewnych uproszczeń jest uzasadnione. Wycinek kodu z listingu 29.24 ukazuje nam bezpośredni sposób komunikacji pomiędzy dwoma fragmentami.

1074 Android 3. Tworzenie aplikacji Listing 29.24. Bezpośrednia komunikacja pomiędzy fragmentami FragmentOther fragOther = (FragmentOther)getFragmentManager().findFragmentByTag("other"); fragOther.callCustomMethod( arg1, arg2 );

Na listingu 29.24 nie wprowadziliśmy żadnego interfejsu. Omawiany fragment bezpośrednio posiada informacje na temat klasy oraz dostępnych metod drugiego fragmentu. Takie rozwiązanie nie jest kłopotliwe, ponieważ fragmenty te mogą być częścią tej samej aplikacji. Poza tym fakt, że niektóre fragmenty mają dostęp do informacji na temat innych fragmentów, może być łatwiejszy do zaakceptowania.

Stosowanie metod startActivity() i setTargetFragment() Wspólną cechą fragmentów i aktywności jest możliwość uruchomienia aktywności za pomocą fragmentu. Fragment zawiera metody startActivity() oraz startActivityForResult(). Działają one podobnie jak w przypadku aktywności: w momencie przekazania wyniku zostanie wywołana metoda onActivityResult() wobec fragmentu uruchamiającego daną aktywność. Istnieje jeszcze jeden mechanizm komunikacji, z którym Czytelnik powinien się zapoznać. Gdy dany fragment ma zostać uruchomiony przez inny fragment, wywołujący fragment może zostać powiązany z fragmentem wywoływanym. Zostało to zademonstrowane na listingu 29.25. Listing 29.25. Konfiguracja mechanizmu komunikacji fragmentu wywołującego z docelowym fragmentem mCalledFragment = new CalledFragment(); mCalledFragment.setTargetFragment(this, 0); fm.beginTransaction().add(mCalledFragment, "work").commit();

Za pomocą tych kilku wierszy utworzyliśmy nowy obiekt CalledFragment, ustawiliśmy w bieżącym fragmencie wywoływany fragment jako fragment docelowy oraz za pomocą mechanizmu transakcji dodaliśmy ten wywoływany fragment do menedżera fragmentów oraz do aktywności. Po uruchomieniu wywoływanego fragmentu będzie mógł on wywołać metodę getTargetFragment(), która przekaże odniesienie do fragmentu wywołującego. Za pomocą tej metody wywoływany fragment może korzystać z metod fragmentu wywołującego, a nawet uzyskać bezpośredni dostęp do jego składowych widoku. Na listingu 29.26 zademonstrowaliśmy przykładowy kod, w którym wywoływany fragment wprowadza tekst bezpośrednio do interfejsu użytkownika znajdującego się we fragmencie wywołującym. Listing 29.26. Komunikacja fragmentu docelowego z fragmentem wywołującym TextView tv = (TextView) getTargetFragment().getView().findViewById(R.id.text1); tv.setText("Ustawiony z poziomu wywoływanego fragmentu");

Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

1075

Tworzenie niestandardowych animacji za pomocą klasy ObjectAnimator We wcześniejszej części rozdziału zaprezentowaliśmy w zarysie sposób niestandardowego animowania fragmentów. Pokazaliśmy kod niestandardowej animacji, dzięki której fragment wyświetlający szczegóły zniknął, a na jego miejscu pojawił się nowy fragment wyświetlający szczegóły. Stwierdziliśmy również, że w pakiecie Android SDK do dyspozycji pozostaje tylko kilka standardowych animacji, jednak nie wszystkie działają zgodnie z oczekiwaniami. W tym podrozdziale pokażemy, w jaki sposób można tworzyć własne, niestandardowe animacje, dzięki czemu Czytelnik będzie mógł wstawiać interesujące przejścia pomiędzy fragmentami. Mechanizm implementowania niestandardowych animacji fragmentów został umieszczony w klasie ObjectAnimator. W rzeczywistości jest to ogólna funkcja Androida, która może być stosowana nie tylko w przypadku fragmentów, lecz również obiektów klasy View. W tym podrozdziale będziemy zajmować się wyłącznie animacją fragmentów, lecz omawiane tu zasady w równym stopniu stosuje się również do innych rodzajów obiektów. Animator obiektu pobiera obiekt i animuje go od stanu początkowego do stanu końcowego w określonym przedziale czasowym. Przedział ten jest definiowany w milisekundach. Istnieją procedury określające zachowanie obiektu w tym czasie. Procedury te noszą nazwę interpolatorów. Jeżeli wyobrazimy sobie przejście od stanu początkowego do stanu końcowego jako prostą, interpolator będzie wyznaczał „położenie” animacji na tej prostej w dowolnym momencie tego danego czasu. Jedną z najprostszych procedur jest interpolator liniowy (ang. linear interpolator); dzieli on nasz odcinek na równe części i „przeskakuje” po nich w takich samych przedziałach czasowych. W wyniku tego obiekt porusza się z punktu początkowego do punktu końcowego ze stałą prędkością, bez początkowego przyśpieszenia i końcowego hamowania. Domyślnym interpolatorem jest accelerate_decelerate, który wprowadza na początku animacji płynne przyśpieszenie, a na jej końcu — płynne hamowanie. Najciekawsza jest informacja, że interpolator może przekroczyć punkt końcowy na naszej prostej, a następnie cofnąć się. Na tym polega działanie interpolatora przekraczającego (ang. overshoot interpolator). Mamy również do czynienia z interpolatorem drgającym (ang. bogunce interpolator), który porusza się z punktu początkowego do punktu końcowego, jednak po dotarciu do punktu końcowego wraca kilkakrotnie do punktu początkowego, aby ostatecznie zatrzymać się w punkcie końcowym. Interpolator wpływa na wymiary obiektu. W przypadku stosowanych wcześniej animatorów oraz fade_out, tym wymiarem był parametr alfa fragmentu (tj. przezroczystość obiektu). Animator fade_in zmieniał wartość parametru alfa fragmentu od wartości (0) do (1), z kolei animator fade_out modyfikował wartość parametru alfa drugiego fragmentu od wartości (1) do (0). Jeden fragment przechodził ze stanu całkowitej przezroczystości do stanu zupełnej widoczności, podczas gdy w drugim fragmencie następował odwrotny proces. fade_in

Poza wzrokiem użytkownika animator obiektu znajduje główny widok fragmentu i regularnie wywołuje metodę setAlpha(), za każdym razem nieznacznie zmieniając wartość parametru. Częstotliwość powtórzeń wywołań zależy od interpolatora. Interpolator liniowy wprowadza wywołania w równych odstępach czasowych. Interpolator accelerate_decelerate najpierw ustanawia mniejsze wartości określonego parametru na jednostkę czasu i stopniowo je zwiększa, co daje wrażenie przyśpieszenia, natomiast pod koniec odwraca ten proces, przez co wydaje się, że następuje spowolnienie animacji danego wymiaru obiektu.

1076 Android 3. Tworzenie aplikacji Wymiarami może być wiele spośród wartości, które można pobrać i ustawić wewnątrz klasy View. W rzeczywistości do pracy z przetwarzanym widokiem animator obiektu wykorzystuje mechanizm refleksji. Jeżeli zechcemy zdefiniować animację obrotu, Android wywoła metodę setRotation() wobec danego obiektu (lub jego widoku). Animator pobiera wartości początkową i końcową, a następnie wykorzystuje je do przetworzenia obiektu w danych granicach. Jeżeli nie zostanie zdefiniowana wartość początkowa, zostanie wprowadzona metoda pobierająca, mająca na celu uzyskanie bieżącej wartości obiektu. Zobaczmy, jak to się ma do naszych fragmentów. FragmentTransaction definiującą niestandardową animację setCustomAnimations(), która pobiera dwa parametry — identyfikatory zasobów:

Jedyną metodą w klasie „

„

jest

Pierwszy parametr określa zasób animatora dla fragmentu wprowadzanego do pojemnika widoków. Drugi parametr definiuje zasób animatora dla fragmentu opuszczającego pojemnik widoków.

Obydwa animatory nie muszą być ze sobą nawet powiązane, najlepiej jednak by było, gdyby wizualnie do siebie pasowały. Innymi słowy, jeżeli jeden fragment zanika, drugi mógłby stopniowo pojawić się na miejscu poprzednika. Jeżeli jeden fragment wysuwa się z prawego brzegu ekranu, drugi może wsunąć się z lewej strony. Zasoby animatora można znaleźć w folderze zestawu SDK, w katalogu związanym z właściwą platformą, a dalej w podkatalogu /data/res/animator. To właśnie tu znajdziemy używane wcześniej pliki fade_in.xml i fade_out.xml. Możemy utworzyć również własne zasoby. Jeżeli zdecydujemy się na ten krok, najlepiej umieścić taki plik w katalogu /res/animator naszego projektu. Plik ten w razie potrzeby można dodać ręcznie. Przyjrzyjmy się przykładowi umieszczonemu na listingu 29.27, gdzie widzimy prosty lokalny plik animatora (slide_in_left.xml). Listing 29.27. Niestandardowy animator powodujący wysuwanie się obiektu z lewej strony ekranu

W tym pliku zasobów wykorzystano nowy znacznik, objectAnimator, wprowadzony w wersji 3.0 Androida. Podstawowa struktura pliku powinna jednak wyglądać znajomo dla Czytelnika. Mamy tu do czynienia z grupą atrybutów typu android: określających czynność, jaką chcemy wykonać. W przypadku animatora obiektów musimy zdefiniować kilka elementów. Pierwszym z nich jest interpolator. Lista dostępnych typów interpolatorów została umieszczona w zasobie android.R.interpolator. Dzięki wiedzy o nazwach zasobów zorientujemy się, że atrybut interpolatora stanowi odpowiednik pliku umieszczonego w katalogu zestawu SDK, w folderze właściwej platformy, dokładniej zaś w katalogu /data/res/interpolator, a nazwa tego pliku to accelerate_decelerate.xml. Atrybut android:propertyName definiuje wymiar, jaki stanie się podstawą animacji. W tym przypadku zamierzamy przeprowadzić animację w kierunku osi X. Jeżeli przyjrzymy się metodzie setX() klasy View, zauważymy, że wprowadzanym parametrem jest wartość zmienno-

Rozdział 29 „ Koncepcja fragmentów oraz inne pojęcia dotyczące tabletów

1077

przecinkowa. Z tego właśnie powodu atrybut android:valueType posiada zdefiniowaną wartość floatType. Wartość atrybutu android:duration wynosi 2000, czyli 2 sekundy. Prawdopodobnie jest to zbyt długi czas dla standardowej aplikacji, w tym przypadku jednak chcemy zdążyć zobaczyć, co się dzieje w trakcie animacji. Dwa niewymienione jeszcze atrybuty, android: ´valueFrom oraz android:valueTo, posiadają wartości odpowiednio: -1280 i 0. Zostały one określone w taki sposób, ponieważ dany fragment ma się znajdować w punkcie 0 na końcu animacji. Oznacza to, że po zakończeniu animacji lewa krawędź fragmentu będzie się znajdować przy lewej krawędzi pojemnika widoku. Ponieważ chcemy, aby nasz fragment wysunął się z lewej strony ekranu, musimy zadeklarować rozpoczęcie animacji od tej strony, a wartość -1280 wydaje się wystarczająco duża. Jak zapewne Czytelnik się domyśla, animator obiektu wysuwającego się z prawej strony wyglądałby niemal identycznie jak ten zaprezentowany na listingu 29.27, z tą różnicą, że atrybut android:valueFrom przyjąłby wartość 0, a w atrybucie android:valueTo wprowadzilibyśmy bardzo dużą wartość dodatnią, na przykład 1280. Podczas korzystania z animatora obiektów zauważymy, że wartości większości wymiarów posiadają typ floatType, chociaż czasami będziemy wybierać również typ intType. Wystarczy spojrzeć na typ wartości parametru wymaganego przez metodę ustawiającą. To właśnie dzięki niej animator obiektu posiada tak wielki potencjał. Tak naprawdę nie jest ważne, skąd pochodzi metoda ustawiająca. Oznacza to, że możemy dodać do obiektu własny wymiar, a jego animację obsłuży klasa animatora. Zadaniem programisty jest jedynie dostarczenie metody ustawiającej i wprowadzenie atrybutów do pliku zasobu. Resztę pracy wykona animator. Jedyną wadą tego systemu jest to, że w przypadku pominięcia atrybutu valueFrom w pliku XML animator obiektu wykorzysta metodę pobierającą do określenia wartości początkowej obiektu. Metoda pobierająca musi wtedy przekazać właściwy typ wartości danego wymiaru. Być może Czytelnika zainteresuje również możliwość jednoczesnego animowania kilku wymiarów. W tym celu wykorzystujemy znacznik

wokół większej liczby znaczników

Android 3 Tworzenie aplikacji - PDF Free Download (2024)

References

Top Articles
Latest Posts
Article information

Author: Reed Wilderman

Last Updated:

Views: 6067

Rating: 4.1 / 5 (52 voted)

Reviews: 83% of readers found this page helpful

Author information

Name: Reed Wilderman

Birthday: 1992-06-14

Address: 998 Estell Village, Lake Oscarberg, SD 48713-6877

Phone: +21813267449721

Job: Technology Engineer

Hobby: Swimming, Do it yourself, Beekeeping, Lapidary, Cosplaying, Hiking, Graffiti

Introduction: My name is Reed Wilderman, I am a faithful, bright, lucky, adventurous, lively, rich, vast person who loves writing and wants to share my knowledge and understanding with you.