13. JAVA W KOMUNIKACJI BEZPRZEWODOWEJ 


Komunikacja bezprzewodowa obejmuje bardzo szeroki zakres technologii i związanych z nimi urządzeń. Naturalną tendencją w takiej różnorodności jest wprowadzanie pewnych standardów obejmujących w miarę szeroki wachlarz urządzeń i oprogramowania. Również Java wychodzi naprzeciw takim tendencjom.

Platforma Javy jest doskonałym środowiskiem do budowania oprogramowania dla celów komunikacji bezprzewodowej.

Główną platformą Javy dla technologii bezprzewodowej jest platforma J2ME(Java2 Micro Edition).


13.1. WPROWADZENIE

J2ME jest zestawem technologii i specyfikacji projektowanych dla różnych typów urządzeń. Podzielona jest na konfiguracje i profile.

Konfiguracje są specyfikacjami które opisują język programowania, maszynę wirtualną i bazowy zestaw funkcji API które mogą być używane w urządzeniach określonej klasy (wielkość pamięci, szybkość procesora).

Profil bazując na danej konfiguracji  dodaje do niej specyficzne API tak aby utworzyć kompletne środowisko do budowania aplikacji. Profile zwykle dołączają funkcje stosowane w cyklu życia aplikacji, w interfejsach użytkownika i w operowaniu na pamięci trwałej.

Elementy stosu programistycznego danego urządzenia podaje tabela:

Pakiety opcjonalne
 Profile
Konfiguracja
Maszyna wirtualna
System operacyjny urządzenia 

Na platformie J2ME określono dwie podstawowe konfiguracje: CDLC i CDC przeznaczone dla urządzeń różnej klasy.

                                                                           Źródło:http://wireless.java.sun.com/midp/articles/api/

Konfiguracja CDLC (Connected Limited Device Configuration) specyfikuje maszynę wirtualną Javy KVM  (Kilobyte Virtual Machine) i zbiór podstawowych klas bibliotecznych . Przeznaczona jest dla małych urządzeń bezprzewodowych (telefony komórkowe, dwukierunkowe pagery i cyfrowe asystentki osobiste) z uproszczonym interfejsem użytkownika i pamięcią rzędu kilkuset  kilobajtów.

Konfiguracja CDC (Connected Device Configuration) opiera się na specyfikacji klasycznej maszyny wirtualnej mającej funkcjonalność komputera biurkowego. Przeznaczona jest dla większych  urządzeń bezprzewodowych z pamięcią rzędu kilku megabajtów (nawigacyjne systemy samochodowe)..

W wykładzie omówimy konfigurację  CDLC (wersja 1.0)  i oparty o nią profil MIDP (wersja 2.0).

Profil MIDP (Mobile Information Device Profile) definiuje model aplikacji (midlet) pozwalający w współdzielenie ograniczonych zasobów urządzenia przez kilka aplikacji, w jakiej postaci  midlet powinien  być dostarczony do urządzenia, jakie środowisko uruchomieniowe jest dla niego dostępne i jak powinien być zarządzany przez system operacyjny urządzenia.


13.2.  ZESTAW BIBLIOTEK CLDC I  MIDP

API platformy J2SE wymagają kilka megabajtów pamięci dlatego nie są właściwe dla małych urządzeń o ograniczonych zasobach. W projektowaniu API ustalono minimalny zbór bibliotek użytecznych dla szerokiej gamy  urządzeń bezprzewodowych.

Klasy biblioteczne można podzielić na dwie kategorie:

Podzbiór J2SE opisuje poniższa tabela:

Pakiety włączone z platformy J2SE
java.io Klasy strumieniowe wejścia i wyjścia włączone z J2SE
java.lang Klasy jądra włączone z  J2SE.
java.util Klasy pomocnicze m.in. kolekcje włączone z J2SE
 

Podpakiety specyficzne dla MIDP:

Podstawowy pakiet dla aplikacji MIDP 
javax.microedition.midlet Definiuje aplikację MIDP(midlet)  i jej współdziałanie z otoczeniem w którym się wykonuje

Pakiet GUI dla aplikacji MIDP
javax.microedition.lcdui Zestaw klas opisujących komponenty GUI dla aplikacji MIDP
javax.microedition.lcdui.game Zestaw klas przeznaczonych specjalnie dla gier w urządzeniach bezprzewodowych

Pakiet dla pamięci trwałej
javax.microedition.rms Klasy pozwalające na  zapis i odczyt danych do i z pamięci trwałej.

Pakiet do połączeń sieciowych
javax.microedition.io Klasy do obsługi połączeń sieciowych oparte o szkielet CDLC.

 

Pakiet klucza publicznego
javax.microedition.pki Autoryzowanie informacji w  bezpiecznych połączeniach  sieciowych..

 

Pakiety audio do obsługi funkcji głosowych ( implementacja podzioru specyfikacji MMAPI )
javax.microedition.media Częściowa implementacja Mobile Media API(JSR-135) w zakresie funkcji głosowych i video.
javax.microedition.media.control Klasy opisujące obiekty  współdziałające z obiektami typu Player.


13.3. PAKIETY WŁĄCZONE Z PLATFORMY  J2SE

Te pakiety to java.lang,java.io,java.util.

Należy zdawać sobie sprawę że generalnie klasy te nie posiadają pełnej funkcjonalności klas z platformy J2SE - ich funkcjonalność jest podzbiorem funkcjonalności ich odpowiedników z platformy standardowej.

Pakiet java.lang

Klasy zwykłe  Boolean, Byte, Character, Class, Integer, Long, Math, Object, Runnable, Runtime, Short, String, StringBuffer, System, Thread, Throwable
Klasy wyjątków  ArithmeticException, ArrayIndexOutOfBoundException, ArrayStoreException, ClassCastException, ClassNotFoundException, Error, Exception, IllegalAccessException, IllegalArgumentException, IllegalMonitorStateException, IllegalThreadStateException, IndexOutOfBoundException, InstantiationException, InterruptedException, OutOfMemoryError, NegativeArraySizeException, NumberFormatException, NullPointerException, RuntimeException, SecurityException, StringIndexOutOfBoundException, VirtualMachineError

Pakiet java.io

Klasy zwykłe ByteArrayInputStream, ByteArrayOutputStream, DataInput, DataOutput, DataInputStream, DataOutputStream, InputStream, OutputStream, InputStreamReader, OutputStreamWriter, PrintStream, Reader, Writer
Klasy wyjątków EOFException, IOException, InterruptedException, UnsupportedEncodingException, UTFDataFormatException

Pakiet java.util

Klasy zwykłe  Calendar, Date, Enumeration, Hashtable, Random, Stack, TimeZone, Vector,Timer,TimerTask
Klasy wyjątków EmptyStackException, NoSuchElementException

 Pakiet java.util warto omówić bardziej szczegółowo ze względu na jego praktyczne zastosowanie.
 

Jedyny interfejs pakietu java.util
Enumeration    deklaruje funkcjonalność  iteratora do przeglądania elementów kolekcji..
 

Nie wszystkie klasy pakietu java.util z platformy J2SE zostały włączone do midletów.Warto zatem spojrzeć na listę klas przeznaczonych dla midletów.

 
Klasy pakietu java.util dla midletów
Calendar  klasa abstrakcyjna do ustawiania i pobierania daty reprezentowanej na kilku polach : m.in.  YEAR, MONTH, DAY.
Date  reprezentacja daty podawanej w milisekundach od określonego momentu przeszłości
Hashtable mapa odwzorowująca klucze na wartości
Random generator liczb pseudolosowych.
Stack  reprezentuje stos obiektów  ( struktura danych typu LIFO) .
Timer  do uruchamiania zadań typu TimerTask 
TimerTask zadanie (wątek) wykonywane jednokrotnie lub wielokrotnie przez Timer.
TimeZone  reprezentuje strefę czasową.
Vector  rozszerzalna automatycznie tablica obiektów.

Na szczególną uwagę w tej tabeli zasługują klasy TimerTask i Timer.

TimerTask implementuje Runnable stanowi  więc jest podstawę do utworzenia wątku. Jedyny konstruktor tej klasy jest chroniony i może być wykorzystany w podklasie tej klasy.

Konstruktor klasy TimerTask
protected TimerTask()
tworzy nowe zadanie timera.
 

Najważniejsza metoda tej klasy to run() - implementacja interfejsu Runnable.

Metody klasy TimerTask
 boolean cancel()
 kasuje zadanie timera.
abstract  void run()
 metoda ta gromadzi instrukcje do wykonania w tym zadaniu.
 long scheduledExecutionTime()
 zwraca przewidywany czas wykonania najbardziej aktualnej egzekucji tego zadania.          
 

Timer jest przeznaczony do szeregowania zadań  typu TimerTask do późniejszej egzekucji w wątku działającym w tle. Zadania mogą podlegać jednokrotnej lub wielokrotnej egzekucji z określoną częstością.

Każdemu obiektowi klasy Timer towarzyszy pojedynczy wątek  wykonujący sekwencyjnie wszystkie włączone zadania .Zadania dla timera powinny być krótkie tak by nie blokowały wątku wykonującego. 

Wątek wykonujący zadania domyślnie nie jest wątkiem demonowym i po zakończeniu wszystkich zadań kończy swoje działanie w zwykły sposób i może ulec odśmieceniu po jakimś czasie. Jeżeli chcemy zakończyć watek wykonujący bezzwłocznie musimy użyć metody cancel() z klasy Timer.

Klasa Timer jest watkowo bezpieczna - pojedynczy obiekt tej klasy może być współdzielony przez wiele wątków bez potrzeby zewnętrznej synchronizacji.

Jedyny konstruktor klasy Timer
Timer()
 Tworzy obiekt timera.
 
Metody klasy Timer
 void cancel()
kończy działanie timera usuwając włączone aktualnie zadania.
 void schedule(TimerTask task, Date time)
szereguje podane zadanie task do wykonania w określonym czasie time.
 void schedule(TimerTask task, Date firstTime, long period)
szereguje podane zadanie task do periodycznej egzekucji ze stałym opóźnieniem - pierwsze wykonanie nastąpi w czasie firstTime; pozostałe będą wykonywane co okres period.
 void schedule( TimerTask task, long delay)
szereguje  zadanie task do egzekucji po czasie delay.
 void schedule( TimerTask task, long delay, long period)
szereguje zadanie task do periodycznej egzekucji ze stałym opóźnieniem - pierwsza egzekucja po czasie delay; pozostałe będą wykonywane co okres period.
 void schedule( TimerTask task, Date firstTime, long period)
szereguje  zadanie do periodycznej egzekucji ze stałą prędkością - pierwsze wykonanie w momencie  firstTime ; pozostałe co okres period.
 void scheduleAtFixedRate( TimerTask task, long delay, long period)
szereguje zadanie do periodycznej egzekucji ze stałą prędkością - pierwsze wykonanie w momencie  po czasie delay  ; pozostałe co okres period.         

Egzekucja ze stałym opóźnieniem jest właściwa dla czynności w których ważne jest utrzymanie stałej częstości w krótkim czasie - np. większość zadań animacyjnych.

Egzekucja ze stałą prędkością jest właściwa dla czynności dla których ważne jest utrzymanie stałej częstości w długim czasie ; istotny jest czas bezwzględny wykonania czynności ( bicie zegara o każdej godzinie), istotny jest czas całkowity wykonania ustalonej liczby operacji  ( timer odmierzający sekundy), istotna jest wzajemna synchronizacja  powtarzających się zadań.

 


Ponieważ w poniższym programie będziemy używać interfejsu graficznego typu TextBox mówimy go tutaj awansem. Omówienie pozostałych komponentów GUI zamieszczamy w punkcie 4.

Komponent TextBox jest chyba najczęściej używanym komponentem MIDP GUI i pozwala użytkownikowi na wprowadzanie, wyprowadzanie i edycję tekstu. Komponent ten ma maksymalny rozmiar - ilość znaków które mogą być zgromadzone  w obiekcie tego typu (pojemność). Ilość zgromadzonych znaków  nie ma związku z ilością i ułożeniem  znaków które mogą być wyświetlone w danym momencie (zależy ona od urządzenia)

Jedyny konstruktor tej klasy pozwala ustalić ten maksymalny rozmiar oraz dodatkowo tytuł ,początkową zawartość tekstową i ograniczenia na format wprowadzanych danych.

 
Konstruktor klasy TextBox
TextBox(String title, String text, int maxSize, int constraints)
Tworzy nowy obiekt z tytułem, początkowym tekstem ,maksymalnym rozmiarem i ograniczeniami formatu.
 

Implementacja może narzucić górną granicę  pojemności mniejszą niż podajemy w konstruktorze. Aktualnie osiąagana wartość może być pobrana za pomocą metody getMaxSize() i uwzględniać tę wartość w programie. Nie cały zawarty tekst jest zawsze widoczny na ekranie - w takich przypadkach implementacja stosuje przewijanie tekstu.

Ograniczenia podaje się jako stałe statyczne klasy TextField. Te stałe to : ANY,EMAILADDR,NUMERIC,PHONENUMBER,URL,DECIMAL. W konstruktorze można podawać również dodatkowe modyfikatory PASSWORD, UNEDITABLE, SENSITIVE, NON_PREDICTIVE, INITIAL_CAPS_WORD,INITIAL_CAPS_SENTENCE  które też są stałymi statycznymi klasy TextField.

Ze względu na użyteczność tego komponentu podajemy tu większość  jego metod.

Niektóre metody klasy TextBox
 void delete(int offset, int length)
usuwa znaki z obiektu
int getCaretPosition()
dostarcza bieżącą pozycję wstawiania.
 int getChars(char[] data)
kopiuje zawartość TextBox do podanej tablicy .  
 int getConstraints()
dostarcza aktualne ograniczenia formatu
 int getMaxSize()
podaje maksymalny rozmiar tekstu który może być umieszczony w obiekcie         
String getString()
dostarcza zawartość obiektu          
 void insert(char[] data, int offset, int length, int position)
wstawia podtablicę tablicy data do obiektu  TekstBox   na daną pozycję     
 void insert(String src, int position)
wstawia łańcuch na daną pozycję        
 void setChars(char[] data, int offset, int length)
zamienia zawartość obiektu znakami z podanej podtablicy
 void setConstraints(int constraints)
ustala ograniczenia formatu wejściowego
 int setMaxSize(int maxSize)
ustala maksymalny rozmiar  
 void setString(String text)
 ustala tekst zawarty w komponencie    
 void setTitle(String s)
 ustala tytuł komponentu      
 int size()
podaje liczbę znaków aktualnie zgromadzonych w obiekcie    

 


Poniższy program wykorzystuje obiekt Timer i TimerTask do obliczania średniego czasu działania określonej liczby cykli pętli for :

import java.lang.*;
import java.util.*;

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;

public class LoopTime 
  extends MIDlet 
  implements CommandListener {
	
  public Display display;
  public TextBox tb;
  public Command exitCom ;
  public String str;
  boolean cancel;
  static int licz;    
  long avTime;

    public LoopTime() {                
    
      display = Display.getDisplay(this);
        
      exitCom = new Command("Exit", Command.EXIT, 1);
                                                         
      tb = new TextBox("Czas wykonania pętli","",256,0);
      tb.addCommand(exitCom);        
      tb.setCommandListener(this);               
      display.setCurrent(tb);
      cancel=false;
                                        
    }
    
    public void startApp() {                                       
                          
      Timer timer = new Timer();
      Task task = new Task();
      licz = 0;
      timer.schedule(task,1000,2000);
      
      try {Thread.sleep(60000);}
      catch(InterruptedException e){}
      
      cancel = true;
      
      task.cancel();
      timer.cancel();
      avTime = avTime/licz;
      tb.setString("czas sredni= " + avTime);
    }

    public void pauseApp() {
    	
    }
  
    public void destroyApp(boolean unconditional) {
      
    }

    public void commandAction(Command c, Displayable s) {                                       
      if(c==exitCom){        	                  	
        destroyApp(true);notifyDestroyed();
      }
    }
    
    class Task extends TimerTask{
    
      public void run() {
      	      	      	       	      	   	                      
        long start = System.currentTimeMillis();              
        
        for (int i = 0; i < 1000000 ; i++){ 
          
        };                                 
        
        long stop = System.currentTimeMillis();                                     
        long millis = stop - start;         
        licz++;        
        avTime=avTime+millis;     	        
        if(!cancel)tb.setString("time= "+millis);    	
       }//run()
    	
    }//class Task	
                 
}//class Midlet1

Ograniczenia bibliotek CLDC w porównaniu do  J2SE

Rozdział ten podsumujemy listą cech platformy J2SE które zostały wyeliminowane w maszynie wirtualnej obsługującej  CDLC  ze  względu na zbyt duży koszt implementacji lub ze względów bezpieczeństwa.:

1 - brak obsługi arytmetyki zmiennoprzecinkowej (float, double): głównie ze względu na ograniczenia sprzętowe urządzeń

2 - brak finalizacji: brak metody finalize() w klasie Object tak że nie można dokonać zwolnienia zasobów przed odśmieceniem.

3 - ograniczona obsługa błędów: błędy czasu przebiegu są obsługiwane w sposób specyficzny dla implementacji.
     CLDC definiuje tylko trzy klasy błędów:
     java.lang.Error,java.lang.outOfMemoryError oraz java.lang.VirtualMachineError
     Inne błędy są obsługiwane w sposób zależny od urządzenia włączając w to zakończenie aplikacji lub resetowanie      urządzenia

4 - brak bibliotek natywnych (Java Native Interface -JNI): względów bezpieczeństwa i ograniczoność pamięci.

5 - brak możliwości przedefiniowania wbudowanego ładowacza klas: ze względów bezpieczeństwa 

6 - brak obsługi refleksj, RMI i serializacji 

7 - brak obsługi grup wątków i wątków demonów

8 - brak słabych referencji

Brak obsługi liczb typu float lub double można rozwiazać w arytmetyce całkowitej przez przyjęcie konwencji że argumenty operacji arytmetycznych są kodowane z określonym miejscem po kropce. Np.  przyjmując dwa miejsca po kropce liczba 123 będzie traktowana przez program jako 1.23, liczba 1 jako 0.01 a liczba 12 jako 0.12. 

W poniższym programie zdefiniowano klasę Float wykonującą cztery podstawowe działania arytmetyczne(+,-,*,/) na liczbach zmiennoprzecinkowych. Argumentami metod dod(), odejm(), mnoz(), dziel() są liczby typu int które reprezentują zakodowane liczby zmiennoprzecinkowe. Kodowanie polega na założeniu że liczby całkowite reprezentują liczby rzeczywiste z kropką. Stała prec  klasy Float podaje ile cyfr występuje po kropce  w reprezentacji całkowitej  liczb rzeczywistych. 

Oprócz metod wykonujących cztery podstawowe operacje arytmetyczne mamy w klasie Float dwie pomocnicze metody. Metoda przes(String str,int poz) wstawia do łańcucha  str kropkę na odpowiednią pozycję. Metoda power(int x,int n) podnosi liczbę całkowitą x do potęgi n.

 

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;

  public class FloatConv extends MIDlet 
  implements CommandListener {
	
    public Display display;
    public String str;
    int liczOK;TextBox tb;
    public Command exitCom,okCom ;
    Float f;
   
    int x,y;

    public FloatConv() {                

        str="";

        display = Display.getDisplay(this);
        
        exitCom = new Command("Exit", Command.EXIT, 1);
        okCom = new Command("OK", Command.OK, 2);                                   
              
        tb = new TextBox("Wprowadź pierwszą liczbę->OK","",256,2);
        tb.addCommand(exitCom);
        tb.addCommand(okCom);
        tb.setCommandListener(this);
                                        
    }
    
    public void startApp() {   
        f = new Float(2);                                     
        display.setCurrent(tb);                 
    }

    public void pauseApp() {
    	
    }
  
    public void destroyApp(boolean unconditional) {
    	
    }

    public void commandAction(Command c, Displayable s) {         
      liczOK+=1;
        
      if(liczOK==1){
        	
        x = Integer.parseInt(tb.getString());
        tb.setTitle("Wprowadź drugą liczbę->OK");
        tb.setString("");
      }	
      if(liczOK==2){
        	
       	y = Integer.parseInt(tb.getString());              	
       	tb.setString("");        
        tb.setTitle("Wynik sumowania");
        tb.setConstraints(0);        	        	
        tb.setString("x="+x+"\n"+"y="+y+"\n"); 
        	
       	str="x+y="+f.dod(x,y)+"\n";
       	str=str+"x-y="+f.odejm(x,y)+"\n";
       	str=str+"x*y="+f.mnoz(x,y)+"\n";
       	str=str+"x/y="+f.dziel(x,y)+"\n";
        	
        tb.insert(str,tb.getCaretPosition());
      }
        
      if(c==exitCom){destroyApp(true);notifyDestroyed();}
    }
                 
}//class ioMidlet

class Float {
	
  int prec;

  public Float(int prec) { 	
    this.prec = prec;		
  }
	
  public String dod(int a,int b){
  
    int wynik = a+b;
    String wynikStr = String.valueOf(wynik);  
    return przes(wynikStr,prec); 
  
  }//dod()
  
  public String odejm(int a,int b){  
   
    String wynikStr;
    int wynik = a - b;             
    wynikStr = String.valueOf(wynik);      
    wynikStr = przes(wynikStr,prec);        
    return wynikStr; 
  
  }//odejm()
  public String mnoz(int a,int b){
  
    int wynik = a*b;  
    String wynikStr = String.valueOf(wynik);  
    return przes(wynikStr,prec*prec); 
  
  }//mnoz()
  public String dziel(int a,int b){
  
    String strA = String.valueOf(a);
    String strB = String.valueOf(b);
    
    int diff = strA.length()-strB.length();
    int wykl = 0;
    
    if(diff < 0)wykl = Math.abs(diff);
    a = a*power(10,wykl+prec);
    int wynik = a/b;
    String wynikStr = String.valueOf(wynik);    
    return przes(wynikStr,wykl+prec); 
  
  }//dziel()
  
  public String przes(String str,int oilepoz){
  	
  	int diff;
  	String temp;  	
  	diff = str.length()-oilepoz;  	
  	StringBuffer sb = new StringBuffer(str);
  	 	
  	if(diff > 0)sb.insert(diff,'.');
  	
  	if(diff == 0){
  	  str = "0." + str;
  	  sb=new StringBuffer(str);
  	}	
  	
  	if(diff < 0){  		
  	  temp = "0.";	
  	  for(int i=1; i <= Math.abs(diff); i++)temp = temp + "0";	
  	  temp=temp+str;
  	  sb = new StringBuffer(temp);  		
    }
  	
    return sb.toString();  	  	
  	
  }//przes()    

  public int power(int x,int n){
  	
  	int iloczyn = 1;  	
  	for(int i=1; i<=n; i++)iloczyn = iloczyn*x;  	
  	return iloczyn;  	
  }
	
}//class Float

 


13.4. CYKL ŻYCIA MIDLETU

MIDP definiuje określony model aplikacji pozwalający na korzystanie z ograniczonych zasobów urządzenia bezprzewodowego.

Każde urządzenie bezprzewodowe obsługujące MIDP powinno posiadać oprogramowanie podstawowe pozwalające na instalację, selekcję, uruchamianie i usuwanie aplikacji MIDP (midletów). Oprogramowanie to nazywa się oprogramowaniem zarządzającym aplikacjamu (skrót ang. AMS). AMS dostarcza środowiska JRE w którym midlet  jest instalowany, uruchamiany, zatrzymywany i odinstalowywany. AMS jest również odpowiedzialny za obsługę błędów w czasie instalacji, wykonywania i usuwania aplikacji oraz w razie konieczności za interakcję z użytkownikiem.

Instalacja aplikacji odbywa się na podstawie dwóch plików, które muszą być umieszczone w danym urządzeniu.

Jeden z nich ma rozszerzenie JAR i stanowi zarchiwizowane pliki ze skompilowanymi klasami aplikacji,

Drugi plik z rozszerzeniem JAD jest deskryptorem aplikacji i w połączeniu z plikiem manifestowym JAR jest używany prze AMS do zarządzania midletami w ramach aplikacji jak również jest używany przez same midlety do konfiguracji specyficznych atrybutów.

Oba pliki  mogą być ściągnięte z serwera  w sposób podany przez producenta urządzenia bądź za pomocą przeglądarki WAP zainstalowanej w urządzeniu.

W jednym pliku  JAR może być umieszczonych kilka midletów (pakiet midletów), stanowiących jednostkę aplikacji i współdziałających w ramach tej jednostki. Każdy midlet składa się z klasy która rozszerza klasę Midlet oraz z innych klas potrzebnych midletowi. Po uruchomieniu apilkacji AMS tworzy nowy obiekt  klasy Midlet, który jest używany w cyklu życia midletu do rozpoczęcia ,przerywania i zakończenia działania midletu.

Współdzielenie danych i informacji między midletami w ramach jednostki aplikacji jest kontrolowane przez indywidualne funkcje i ich implementacje.

AMS po zainicjowaniu aplikacji udostępnia programowi następujące zasoby:

CLDC i JVM dostarcza środowiska dla egzekucji kodu bajtowego, wielowątkowości ,blokowania i synchronizacji . 

MIDlet możę  ładować i wywoływać metody dowolnej klasy z pliku  JAR , klas MIDP, klas CLDC. Każda biblioteka specyfikuje jak dana klasa obsługuje współdzielenie zasobów i jak powinien midlet ich używać w środowisku wielowątkowym..

Pliki z archiwum JAR które nie są klasami są dostępne w aplikacji poprzez wywołanie funkcji  getResourceAsStream.() z klasy java.lang.Class. Metoda zwraca obiekt typu InputStream. Konkretnymi podklasami klasy abstrakcyjnej InputStreamByteArrayInputStream i DataInputStream.

Zawartość deskryptora JAD jest dostępna aplikacji poprzez wywołanie metody getAppProperty()z klasy javax.microedition.midlet.MIDlet, która zwraca obiekt typu String.  

Stany midletu

Każdy midlet musi rozszerzać klasę Midlet -jedyny konstruktor tej klasy jest chroniony (protected) oraz bezparametrowy . 

Uwaga: midlet nie może zawierać metody public static void main() 

Konstruktor klasy Midlet
protected Midlet()
konstruktor chroniony do stosowania w klasach rozszerzających.
  

Kiedy midlet jest uruchamiany, przy pomocy tego konstruktora tworzona jest instancja tej klasy i wywoływane są metody tej klasy mogące wprowadzać midlet w trzy stany : paused,active i destroyed lub też powiadamiać AMS o zmianach tych stanów.

W cyklu życia midletu stany paused i active mogą być osiągane wielokrotnie a stan destroyed tylko jednokrotnie.

Jeżeli midletu nie można wprowadzić w żądany stan to zostanie wygenerowany wyjątek MidletStateException.

Metody  klasy Midlet stanowią podstawowe API wykorzystywane w cyklu życia midletu.

Metody klasy Midlet
 int checkPermission(String permission)
dostarcza statusu podanego pozwolenia
protected abstract  void destroyApp(boolean unconditional)
 zatrzymuje midlet i wprowadza w stan destroyed
String getAppProperty(String key)
dostarcza nazwy właściwości  pobranej z AMS
 void notifyDestroyed()
powiadamia AMS o osiągnięciu stanu destroyed
 void notifyPaused()
powiadamia AMS o przejściu w stan paused (nieaktywny).
protected  abstract  void  pauseApp()
wprowadza midlet w stan paused
 boolean platformRequest(String URL)
żąda obsługi (wyświetlenia lub instalacji) zasobu znajdującego się pod  adresem URL
 void resumeRequest()
wskazuje na zamiar osiągnięcia stanu active
protected abstract  void startApp()
wprowadza midlet w stan active
 

Midlet nie powinien wywoływać metody System.exit(). Jeżeli to zrobi wygenerowany zostanie wyjątek SecurityException.

Poniższa tabela przedstawia stany midletu i sposoby ich osiągania.

 

Nazwa stanu Sposób osiągania stanu

   paused

 

 

 

 

 

  • po utworzeniu midletu porzez instrukcję  new Midlet().     

          Jeżeli wystąpi wyjątek aplikacja osiąga stan destroyed i kończy działanie.

  • po wywołaniu w stanie active metody pauseApp() 

  • po wywołaniu w stanie  acive metody  notifyPaused()  

  • po wywołaniu w stanie stanu active metody  startApp() i wygenerowaniu wyjątku MIDletStateChangeException

  active

  •  bezpośrednio po wywołaniu metody startApp() method.

 destroyed

 

 

  • po wywołaniu w stanie active lub paused metody destroyApp(true).

  • Jeżeli wywołana jest metoda destroyApp(false) może zostać wygenerowany wyjątek MidletStateChangeException sygnalizując że tym razem midlet nie będzie zakończony.

  • Po wywołaniu metody notifyDestroyed().


 Podstawowy szkielet midletu pokazano poniżej.

 

import javax.microedition.midlet.*;      

//importy innych pakietów potrzebnych do realizacji aplikacji

public class MyMidlet extends MIDlet {      

    //pola klasy 
   
    public void startApp() {      
      //instrukcje wykonywane w stanie aktywnym   
    }      
      
    public void pauseApp() {      
      //instrukcje wykonywane w stanie przerwanym      
    }      
      
    public void destroyApp(boolean unconditional) {      
      //instrukcje wykonywane przed zakończeniem midletu - zwolnienie zasobów;      
    }      

    //metody własne klasy MyMidlet

       //klasy wewnętrzne klasy MyMidlet 
      
    
} //class Midlet    

//klasy zewnetrzne

 

Poniższy program Hello wykorzystuje szkielet midletu do interakcji z użytkownikiem w celu wprowadzenia przez użytkownika imienia oraz wyprowadzeniu wprowadzonego imienia za pomocą komponentu TextBox.

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;

public class Hello extends MIDlet 
implements CommandListener {
	
    public Display display;
    public TextBox tb;
    public Command exitCom,okCom ;
    public String str;   
    
    public Hello() {        
      display = Display.getDisplay(this);
      exitCom = new Command("Exit", Command.EXIT, 1);
      okCom = new Command("OK", Command.OK, 1);
      
      tb=new TextBox("Podaj swoje imię->OK","",256,0);
      tb.addCommand(exitCom);
      tb.addCommand(okCom);
      tb.setCommandListener(this);                                  
    }
    
    public void startApp() {                               
      display.setCurrent(tb);         
    }
    public void pauseApp() {
    	
    } 
    public void destroyApp(boolean unconditional) {
    	
    }

    public void commandAction(Command c, Displayable s) {   
      if(c==okCom){
       	str=tb.getString();
       	tb.removeCommand(okCom);
       	tb.setString("\n"+"Hello " + str);   
      }
      if(c==exitCom){ destroyApp(true);notifyDestroyed();}
    }
                 
}//class Hello

 


13.5. KOMPONENTY  GRAFICZNEGO INTERFEJSU UŻYTKOWNIKA (GUI)

Ponieważ urządzenia bezprzewodowe różnią się pod wieloma względami od komputerów biurkowych (w szczególności sposobem interakcji z użytkownikiem) również GUI dla MIDP różni się od GUI dla tych komputerów - nie jest prostym podzbiorem komponentów AWT. Wynika to z kilku cech tych urządzeń :

Interfejsy i klasy komponentów GUI zawarte są w podpakiecie  javax.microedition.lcdui.
Interfejsy pakietu javax.microedtion.lcdui
Choice implementowany przez komponenty dokonujące wyboru z określonej liczby możliwości        Klasy implementujące to: ChoiceGroup i List.
CommandListener  nasłuch komend - wysokopoziomowych zdarzeń pochodzących od implementacji. 
ItemCommandListener      nasłuch komend pochodzących od obiektów typu Item.
ItemStateListener  nasłuch zdarzeń wskazujących na zmiany stanu interaktywnych elementów zawartych w obiekcie typu Form.
 

 
Klasy pakietu javax.microedtion.lcdui
Alert Alarm-ekran pokazujący dane, czekający przez pewien okres czasu i przechodzący do następnego obiektu typu Displayable..
AlertType  Klasa informująca o przyczynie alarmu.
Canvas Klasa bazowa dla aplikacji stosującej niskopoziomowe zdarzenia i rysowanie na wyświetlaczu.    
ChoiceGroup  Grupa wybieralnych elementów umieszczana w obiekcie typu Form; rozszerza Item, implementuje Choice.
Command  Komenda-klasa zawierająca informacje o semantyce akcji.
CustomItem Klasa rozszerzana dla uzyskania nowych wizualnie i interaktywnie elementów .
DateField   Edytowalny element dla prezentacji daty i czasu.
Display Reprezentuje zarządcę wyświetlacza i systemowych urządzeń wejściowych.
Displayable  Klasa obiektu zdolnego do wyświetlania na wyświetlaczu. Podklasy to Canvas i Screen.
Font Reprezentuje font i metrykę fontu.
Form  Ekran zawierający zestaw komponentów typu  Item, Image i String
Gauge Element- wykres słupkowy prezentujący bieżącą wartość typu int z przedziału <min, max>.
Graphics  Wykreślacz grafiki 2D.
Image Klasa której obiekty przechowują obraz.
ImageItem  Element mogący zawierać obraz..
Item Element-nadklasa wszystkich komponentów dodawanych do Form. Elementami są: ChoiceGroup, CustomItem, DateField, Gauge, ImageItem, StringItem,Spacer,TextField
List Ekran zawierający listę wyborów. Implementuje Choice
Screen Ekran-nadklasa wszystkich wysokopoziomowych klas GUI: Alert,  List, TextBox, Form.
Spacer Element-spacja : nie jest interaktywny .
StringItem  Element mogący zawierać łańcuch znaków.
TextBox  Ekran pozwalający na wprowadzanie i edycję tekstu. Ma trzy obszary:  tytułu, tekstu i komendy
TextField  Element-edytowalny komponent tekstowy umieszczany w obiekcie typu Form.
Ticker Reprezentuje  tekst taśmowy  przesuwający się  w sposób ciągły przez ekran.
 

Struktura bibliotek GUI

W klasach GUI MIDP można wyróżnić logicznie  dwa poziomy: 

- niskopoziomowe API : zrealizowane na niskim poziomie abstrakcji dla aplikacji które potrzebują precyzyjnie sterować elementami graficznymi jak również mieć dostęp do niskopoziomowych zdarzeń wejściowych oraz specyficznych cech urządzenia. Przykładem zastosowań są gry. Stosując niskopoziomowe APi możemy mieć pełną kontrolę nad tym co jest rysowane na wyświetlaczu, prowadzić nasłuch zdarzeń pochodzących od klawiatury, uzyskiwać dostęp do konkretnych klawiszy i innych urządzeń wejścia. Klasy dostarczające tego rodzaju API to : Canvas i Graphics.

- wysokopoziomowe APIs : przeznaczone dla aplikacji biznesowych, gdzie ważna jest przenośność aplikacji między   urządzeniami. Zrealizowane są na odpowiednio wysokim poziomie abstrakcji poprzez fakt że aplikacja nie definiuje wizualnego wyglądu komponentów oraz ani nie jest świadoma prymitywnych interakcji t.jak przewijanie, nawigacja itp. Aplikacja również nie ma dostępu do konkretnych urządzeń wejścia np. do poszczególnych klawiszy. Klasy dostarczające tego rodzaju API są podklasami klasy Screen.

Programy oparte o niskopoziomowe API mogą nie być przenośne, zatem powinno się programować na poziomie  niezależnym od platformy - aplikacja nie powinna używać innych klawiszy niż są zdefiniowane w klasie Canvas i nie powinna opierać się na konkretnym rozmiarze ekranu.

Przegląd  najważniejszych klas

Komponenty GUI MIDP jak już wspomniano mają swoją specyfikę ale w gruncie rzeczy wykazują dużo analogii do komponentów AWT, które znamy z platformy J2SE dlatego też omówimy tylko najważniejsze klasy w tym pakiecie, pozostałe będą omawiane przy okazji użycia ich w programach.

Podstawową klasą w tym pakiecie jest Display - reprezentuje zarządcę wyświetlacza i urządzeń wejścia. Najważniejsze jej metody podano w tabeli poniżej:

Niektóre metody klasy Display
void  callSerially(Runnable r)
wstawia asynchronicznie do strumienia zdarzeń metodę run() obiektu typu Runnable .
Displayable getCurrent()
dostarcza aktualny obiekt typu Displayable dla midletu.
  Display getDisplay(Midlet m)
dostarcza  obiekt typu Display unikalny dla midletu.
 void setCurrent(Alert alert, Displayable nextDisplayable)
ustala aktualny obiekt Alert i ustala obiekt Displayable który stanie się aktualny po zniknięciu Alert.
 void setCurrent(Displayable nextDisplayable)
ustala następny aktualny obiekt typu Dispalyable do wyświetlenia..
 void setCurrentItem (Item item)
ustala obiekt Displayable zawierający element item jako aktualny do wyświetlenia, przewija ten obiekt tak aby element item stał się widoczny i ewentualnie przydziela mu fokus.

Dla każdego aktywnego midletu tworzona jest tylko jedna instancja klasy Display którą można pobrać za metodą getDisplay(). Obiekt ten będzie pozostawał taki sam w czasie działania midletu.

Mając dostarczony obiekt Display możemy ustalać aktualny komponent Displayable podlegający wyświetleniu za pomocą metody setCurrent(Displayable) , setCurrent(Alert,Displayable) lub setCurrentItem(Item).

Metoda setCurrent() generalnie  może być wywoływana  w następujących fazach cyklu życia midletu 

Wszystkie komponenty GUI pokazane na wyświetlaczu są zawarte w jakimś obiekcie Displayable.W dowolnym momencie działania aplikacja może mieć co najwyżej jeden aktualny taki obiekt.

Aplikacja może zmianiać aktualny obiekt Displayable w dowolnym momencie - najczęściej na skutek interakcji użytkownika w wątku zdarzeniowym. Ale również zmiana ta może być dokonana w innym wątku na skutek spełnienia określonego warunku.

Ustalony aktualnie Dispalyable może nie być pokazany na wyświetlaczu w danym momencie na skutek działania innego midletu. Mówimy wówczas że midlet działa w tle a nie na pierwszym planie. Aplikacja która nie jest na pierwszym planie traci dostęp nie tylko do wyświetlacza ale również i  do urządzeń wejścia.

Klasa Displayable stoi na szczycie hierarchii komponentów GUI . Reprezentuje ona obiekty zdolne do pojawienia się na wyświetlaczu.

Metody klasy Displayable
 void addCommand(Command cmd)
dodaje komendę cmd.
 int getHeight()
dostarcza wysokość dostępnego obszaru  wyświetlacza.       
 Ticker getTicker()
dostarcza obiekt tekstu taśmowego.
String getTitle()
dostarcza tytuł.
 int getWidth()
dostarcza szerokość dostępnego obszaru wyświetlacza.
 boolean isShown()
sprawdza czy obiekt jest widoczny aktualnie na wyświetlaczu.
 void removeCommand(Command cmd)
usuwa komendę cmd.
 void setCommandListener(CommandListener l)
ustala nowy obiekt CommmandListener zamieniając poprzedni.
 void setTicker(Ticker ticker)
ustala tekst taśmowy zamieniając poprzedni.
 void setTitle(String s)
ustala tytuł; może zawierać znaki przejścia do nowej linii
protected  void sizeChanged(int w, int h)
wołana przez implementację przy zmianie rozmiarów dostępnego obszaru 

Charakterystyczne dla obiektu Displayable jest możliwość dodawania komend oraz ustalania tytułu, tekstu taśmowego i słuchacza komend .

Domyślne ustawienia obiektu Displayable są następujące:

Poniższy program Suma wykorzystuje komponent TextBox typu Displayable do wprowadzenia dwóch liczb całkowitych i wyprowadzenia wyniku sumowania.

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;

public class Suma extends MIDlet 
implements CommandListener {
	
    public Display display;
    public String str;
    int liczOK;TextBox tb;
    public Command exitCom,okCom ;
   
    int x,y;

    public Suma() {                
        display = Display.getDisplay(this);
        
        exitCom = new Command("Exit", Command.EXIT, 1);
        okCom = new Command("OK", Command.OK, 2);                                   
              
        tb=new TextBox("Wprowadź pierwszą liczbę->OK","",256,2);
        tb.addCommand(exitCom);
        tb.addCommand(okCom);
        tb.setCommandListener(this);
                                        
    }
    
    public void startApp() {                                       
        display.setCurrent(tb);                 
    }

    public void pauseApp() {
    	
    }
  
    public void destroyApp(boolean unconditional) {
    	
    }

    public void commandAction(Command c, Displayable s) 
    {         
        liczOK+=1;
        
        if(liczOK==1){
        	x=Integer.parseInt(tb.getString());
        	tb.setTitle("Wprowadż drugą liczbę->OK");
        	tb.setString("");
        }	
        if(liczOK==2){
        	y=Integer.parseInt(tb.getString());              	
        	tb.setString("");        
            tb.setTitle("Wynik sumowania");
            tb.setConstraints(0);        	        	
        	tb.setString("x="+x+"\n"+"y="+y+"\n"+"x+y="+(x+y)); 
        }
        
        if(c==exitCom){destroyApp(true);notifyDestroyed();}
    }
                 
}//class ioMidlet

 

Konkretnymi podklasami DisplayableCanvas oraz  Alert,List,TextBox,Form (typu Screen).Klasa Canvas zostanie omówiona przy okazji niskopoziomowych zdarzeń i grafiki natomiast teraz przedstawimy program wykorzystujący niektóre poznane wcześniej komponenty.

Wśród ekranów możemy wyróżnić te które posiadają predefiniowaną strukturę : Alert,List i TextBox oraz klasę Form której strukturę tworzy użytkownik w momencie gdy predefiniowane struktury są niewystarczające.

Klasa Form pozwala na tworzenie dowolnej kombinacji komponentów, programista powinien brać pod uwagę ograniczone możliwości wykreślacza i ograniczyć  ilość umieszczanych komponentów do kilku. Jeżeli komponenty nie mieszczą się na wyświetlaczu implementacja może zastosować przewijanie lub zrealizować pewne komponenty w trybie popup. 

Generalnie implementacja pewnych operacji na wysokopziomowych GUI może być różna na różnych urządzeniach . Przykłady takich operacji to: nawigacja między elementami List lub elementami Form, wybór elementu listy, ruch kursora wstawiania podczas edycji. komponentu tekstowego.

Wprawdzie w większości prezentowanych programach używać będziemy komponentów z predefiniowaną strukturą dla zorientowania się w możliwościach klasy Form i krótki opis konstruktorów i  metod  tej klasy zamieszczono poniżej.

Konstruktory klasy Form
Form(String  title)
tworzy pusty obiekt.
Form(String title,Item[] items)
tworzy obiekt z podanymi elementami .
 
Metody klasy Form
 int append(Image img)
dodaje obraz  img.
 int append(Item item)
dodaje element  item .
 int append(String str)
dodaje łańcuch znaków.
 void delete(int itemNum)
usuwa element o danym indeksie itemNum.
 void deleteAll()
usuwa wszystkie komponenty.
Item get(int itemNum)
dostarcza element o danym numerze.
 int getHeight()
dostarcza wysokości obszaru dostępnego dla komponentów.
 int getWidth()
dostarcza szerokości obszaru dostępnego dla komponentów. 
 void insert(int itemNum,Item item)
wstawia element item na pozycję itemNum zwiększając ilość elementów.
 void set(int itemNum,Item item)
zamienia element o indeksie itemNum elementem item..
 void setItemStateListener(ItemStateListener iListener)
ustala  obiekt ItemStateListener zamieniając poprzedni.
 int size()
podaje liczbę elementów .

 

Poniższy program wykorzystuje obiekt Form do utworzenia  formularza  składającego się pola daty pozwalającego na wprowadzenie daty, pola czasu pozwalającego na wprowadzenie czasu, pól tekstowych przeznaczonych do wprowadzenia imienia, nazwiska, e-maila, telefonu i hasła. Wykorzystuje również słuchacza elementów umieszczonych w obiekcie Form do akceptacji danych wprowadzonych do pól formularza oraz pobraniu tych danych. Wykorzystuje również słuchacza zmiany stanów elementów obiektu Form do wypisania czasowego komunikatu o aktualizacji danego elementu. .

import java.util.*;
import javax.microedition.lcdui.*;
import javax.microedition.midlet.MIDlet;

public class Formularz extends MIDlet 
implements CommandListener,ItemCommandListener,ItemStateListener {
	
    Display display;
    Command exit,okForm,okItem,backTb;                                                           
    Ticker ticker;
    
    Form form;
    DateField dfDate,dfTime;
    Date date;
    TextField tfName,tfSurname,tfPhone,tfEmail,tfPassword;
    TextBox tb;
    Calendar cal;
    String str;
    
    public Formularz() {
        
      exit = new Command("EXIT", Command.EXIT,1);
      okForm = new Command("OK-F",Command.EXIT,1); 
      okItem = new Command("OK-I",Command.EXIT,1); 
      backTb = new Command("BACK-TB",Command.EXIT,1); 

      tb = new TextBox("ELEMENT PO AKCEPTACJI","",1024,0);
      tb.addCommand(backTb);
      tb.setCommandListener(this);
      
      form = new Form("Formularz");
      
      ticker = new Ticker("Podaj datę,czas i swoje dane osobowe");
      form.setTicker(ticker);
      
      dfDate = new DateField("Date", DateField.DATE);
      dfDate.addCommand(okItem);         
      dfDate.setItemCommandListener(this);
      
      dfTime = new DateField("Time", DateField.TIME);
      dfTime.addCommand(okItem);      
      dfTime.setItemCommandListener(this);
      
      tfName = new TextField("Imię", "", 15, TextField.ANY);
      tfName.addCommand(okItem);         
      tfName.setItemCommandListener(this);
                
      tfSurname = new TextField("Nazwisko", "", 15, TextField.ANY);
      tfSurname.addCommand(okItem);         
      tfSurname.setItemCommandListener(this);
            
      tfPhone = new TextField("Telefon", "", 15, TextField.PHONENUMBER);
      tfPhone.addCommand(okItem);         
      tfPhone.setItemCommandListener(this);
      
      tfEmail = new TextField("E-Mail", "", 15, TextField.EMAILADDR);
      tfEmail.addCommand(okItem);         
      tfEmail.setItemCommandListener(this);
      
      tfPassword = new TextField("Hasło", "", 15, TextField.PASSWORD);
      tfPassword.addCommand(okItem);         
      tfPassword.setItemCommandListener(this);
            
      form.append(dfDate);
      form.append(dfTime);
      
      form.append(tfName);
      form.append(tfSurname);
      
      form.append(tfPhone);
      form.append(tfEmail);
      
      form.append(tfPassword);             
      
      form.addCommand(exit);
      form.addCommand(okForm);
      form.setCommandListener(this);
      form.setItemStateListener(this);
      
      display = Display.getDisplay(this);  
    }                                                        
                                                        
    protected void startApp() {    	                           
      display.setCurrent(form);
    }    

    protected void destroyApp(boolean unconditional) {
    }    

    protected void pauseApp() {
    }   
    
    public void commandAction(Command c,Displayable d) {
      if(c == okForm){
      	 tb.setString("Formularz zaakceptowany");
      	 display.setCurrent(tb);
      }	
      if(c==backTb){
        display.setCurrent(form);
      }
      
      if (c == exit) {
        destroyApp(true);
        notifyDestroyed();
      }
    }    
    public void commandAction(Command c,Item item) {
      
      if(c == okItem){
      	
      	if(item instanceof DateField){
      	  DateField dfRef = (DateField)item;	
      	  date = (dfRef).getDate();	
      	  cal = Calendar.getInstance();      	 
      	  cal.setTime(date);
      	  
      	  String str1,str2,str3;
      	  
      	  if(item == dfDate ){
      	    str1 = "" + cal.get(Calendar.DAY_OF_MONTH); 
      	    str2 = "" + cal.get(Calendar.MONTH);
      	    str3 = "" + cal.get(Calendar.YEAR);
      	    
      	    if(str1.length()==1)str1="0"+str1;
      	    if(str2.length()==1)str2="0"+str2;
      	    
      	    str = str1 +"." + str2 + "." + str3;
      	    
      	  }
      	    
      	  if(item  == dfTime){
      	    str1 = "" + cal.get(Calendar.HOUR);
      	    str2 = "" + cal.get(Calendar.MINUTE); 
      	    str3 = "" + cal.get(Calendar.SECOND);
      	    
      	    if(str1.length()==1)str1 = "0" + str1;
      	    if(str2.length()==1)str2 = "0" + str2;
      	    if(str3.length()==1)str3 = "0" + str3;
      	    
      	    str=str1+":"+str2+":"+str3;
      	  }  
      	               	           
          tb.setString(str);
          display.setCurrent(tb);
        }
        if(item instanceof TextField){
      	  str = ((TextField)item).getString();	
          tb.setString(str);
          display.setCurrent(tb);
        }
      }  	
      if (c == exit) {
        destroyApp(true);
        notifyDestroyed();
      }
    }    
    public void itemStateChanged(Item item ) {    	            
       Alert alert=
      	   new Alert("ALERT","AKTUALIZACJA ELEMENTU "+item.getLabel(),
      	              null,AlertType.CONFIRMATION);
       alert.setTimeout(3000);  
       display.setCurrent(alert);
    }
}

Generalnie aktualny ekran midletu pierwszego planu będzie widoczny na wyświetlaczu. Jednakże w pewnych sytuacjach AMS może utworzyć ekran systemowy który przykryje aktualny ekran midletu. Może to np. nastąpić jeżeli system potrzebuje pokazać menu komend lub jeżeli system wymaga od użytkownika edycji na osobnym ekranie zamiast w polu tekstowym obiektu Form. Również w tym kontekście pojęcie ekranu aktualnego pozostaje w mocy i metoda getCurrent() zwróci aktualny ekran aplikacji a nie systemowy  a metoda isShown() zwróci false.

W przypadku gdy ekran systemowy przykryje Canvas będzie wywołana jej metoda hideNotify() a kiedy ekran systemowy będzie usunięty wywołane będą metody showNotify() i paint().

Bardzo ważnym w zastosowaniach  komponentem jest  Image.

Obiekt klasy Image jest używany do przechowywania obrazów. Istnieje on niezależnie od wyświetlacza w pamięci nie związanej z pamięcią ekranu i nie będzie  wyświetlony dopóki nie zostanie wywołana komenda powodująca pojawienie się obrazu na wyświetlaczu (np. w metodzie paint() klasy Canvas) lub też obraz nie zostanie dodany do aktualnego obiektu Displayable .  

Obraz może być dodany do komponentów Alert,Choice,Form,ImageItem.

Metody klasy Image 
static Image createImage(byte[] imageData, int imageOffset, int imageLength)
Tworzy niemodyfikowalny obraz dekodowany z tablicy bajtów image Data od indeksu imageOffset .Ilość pobranych bajtów wynosi imageLength.
static Image createImage(Image source)
tworzy obraz niemodyfikowalny z obrazu źródłowego.
static Image createImage(Image image, int x, int y, int width, int height, int transform)
tworzy obraz niemodyfikowalny na bazie podanego obszaru pikselowego obrazu image dokonując transformacji transform.Transformacja podana jest poprzez stałą statyczną z klasy javax.microedition.lcdui.game.Sprite.
static Image createImage(InputStream stream)
tworzy niemodyfikowlny obraz na podstawie  danych pobranych ze strumienia InputStream.
static Image createImage(int width, int height)
tworzy nowy modyfikowalny obraz do wykreślania poza ekranem.
static Image createImage(String name)
tworzy niemodyfikowalny obraz pobrany z zasobu o nazwie name.
static Image createRGBImage(int[] rgb, int width, int height, boolean processAlpha)
tworzy obraz niemodyfikowalny na podstawie sekwencji wartości ARGB podawanych w postaci  0xAARRGGBB.Dane z tablicy rgb umieszczane są wierszami od lewej do prawej i od góry do dołu. Wartość piksla w wierszu i i kolumnie j jest dana poprzez formułę P(i,j) = rgb[i +j * width]. Jeżeli processAlpha ma wartość true bajt najbardziej znaczący danych rgb opisuje stopień nieprzezroczystości. Jeżeli ma wartość false wszystkie piksle traktowane są jako nieprzezroczyste.
Graphics getGraphics()
tworzy wykreślacz pozwalający na wykreślanie na tym obrazie.
 int getHeight()
podaje wysokość obrazu w pikslach.
 void getRGB(int[] rgbData, int offset, int scanlength, int x, int y, int width, int height)
gromadzi dane pikselowe pobrane ze wyspecyfikowanego obszaru obrazu w tablicy int  o nazwie rgbData.
 int getWidth()
podaje szerokość obrazu w pikslach.
 boolean isMutable()
sprawdza czy obraz jest modyfikowalny.

Obrazy mogą być  modyfikowalne lub niemodyfikowalne w zależności od tego w jaki sposób są tworzone. Jak widać z powyższej tabeli obrazy niemodyfikowalne są  tworzone poprzez ładowanie obrazu z plików, ze strumienia   lub innego obrazu. Jeżeli  utworzymy obraz niemodyfikowalny to nie będzie go można później modyfikować. Natomiast będzie można tworzyć jego modyfikowalną kopię w sposób podany niżej:

    Image source; // obraz do kopiowania
    source = Image.createImage(...);
    Image copy = Image.createImage(source.getWidth(),
    source.getHeight());
    Graphics g = copy.getGraphics();
    g.drawImage(source, 0, 0, TOP|LEFT);
  

Obrazy modyfikowalne są tworzone jako puste obrazy zawierające tylko białe piksle. Aplikacja może wykreślać na tym pustym obrazie za pomocą wykreślacza  uzyskanego metodą getGraphics().

Każdy piksel w obrazie modyfikowalnym jest w pełni nieprzezroczysty. Obrazy niemodyfikowalne mogą zawierać kombinację piksli całkowicie nieprzezroczystych i całkowicie przezroczystych dająć w wyniku piksel o określonym stopniu przeźroczystości.Implementacja musi zapewniać wyświetlanie piksli całkowicie nieprzezroczystych i całkowicie przezroczystych w obrazach niemodyfikowalnych. Wyświetlanie półprzezroczystych piksli zależy od tego czy implementacja obsługuje mieszanie piksli w czasie wykreślania. Jeżeli tak to półprzezroczysty piksel obrazu źródłowego będzie odwzorowany w półprzezroczysty piksel obrazu wynikowego odpowiednio do ilości poziomów przezroczystości obsługiwanych przez daną platformę (w klasie Display mamy metodę numAlphaLevels()). Jeżeli nie to półprzezroczyste piksele są odwzorowywane w całkowicie przezroczyste.

Jeżeli aplikacja modyfikuje zawartość obrazu musi potem odrysowywać komponent zawierający zmodyfikowany obraz np. poprzez wywołanie metody imageItem.setImage() ażeby zmiany zawartości komponentu stały się widoczne.

Implementacja powinna zapewnić obsługę obrazów w formacie PNG.Aplikacja MIDP musi być zgodna z dodatkowymi wymaganiami dotyczącymi obsługi obrazów PNG. Szczegóły wymagań można znaleźć w specyfikacji PNG i dokumentacji MIDP.


13.6. OBSŁUGA ZDARZEŃ 

Interakcja użytkownika powoduje generację zdarzeń. Implementacja powiadamia aplikację o tych zdarzeniach  poprzez wywołania wsteczne metod obsługi. Są generalnie cztery rodzaje wywołań wstecznych  wysoko i niskopoziomowych:

Wywołania wsteczne metod obsługi są szeregowane tak że nie będą nigdy wywołane równocześnie. Jednakże istnie jeden wyjątek od zasady szeregowania wywołań wstecznych - jest nim wywołanie metody serviceRepaints() z klasy Canvas. Metoda ta powoduje wywołanie metody paint() z klasy Canvas i oczekuje na zakończenie metody paint(). Wymuszone wywołanie metody paint() nastąpi nawet wewnątrz aktywnego wywołania innej metody obsługi.

Należy pamiętać że zdarzenia wywołania metody run() klasy TimerTask szeregowane przez obiekt Timer nie są wywołaniami wstecznymi GUI i mogą mieć konkurencyjny dostęp do określonych zasobów. Tak więc jeżeli aplikacja używa  obiektów Timer musi sama zapewnić synchronizację operacji na  współdzielonych zasobach dokonywanych przez wywołania wsteczne GUI i zadania obiektów Timer.

Wysokopoziomowa obsługa zdarzeń - komendy 

Komendy (obiekty klasy Command )  reprezentują  wysokopoziomowe GUI  i nie narzucają konkretnej techniki  interakcji  np. poprzez klawisz lub poprzez menu - zależy to od implementacji dla danego urządzenia.

Poniższa tabela podaje typy komend jako stałe statyczne klasy Command:

Pola klasy Command
static int BACK
komenda nawigacji która przywraca poprzedni ekran.
static int CANCEL
komenda skasowania dialogu bieżącego ekranu.
static int EXIT
komenda zakończenia aplikacji.
static int HELP
komenda żądania pomocy on-line .
static int ITEM
komenda specyficzna dla elementu zawartego w obiekcie typu Screen lub Choice.
static int OK
komenda akceptacji dialogu bieżącego ekranu.
static int SCREEN
komenda definiowana prze aplikację odnosząca się do bieżącego ekranu.
static int STOP
komenda zatrzymania bieżącej operacji, procesu itp..
 

Poszczególne typy komend mogą być mapowane na inne klawisze w zależności od  danego urządzenia, ale zazwyczaj implementacja umieszcza je w standardowych miejscach - np. komenda BACK jest przydzielana do lewego klawisz wyboru.

Aktualna semantyka typu komendy jest zawsze określana przez słuchacza komend (obiekt typu CommandListener) w metodzie commandAction(Command,Displayable).

Klasa Command ma dwa konstruktory pozwalające tworzyć komendy z podaną etykietą ,typem i priorytetem.

Konstruktory klasy Command
Command(String label, int commandType, int priority)
tworzy nową komendę o podanej etykiecie, typie i priorytecie.
Command(String shortLabel,String longLabel, int commandType, int priority)
tworzy nową komendę o podanych etykietach, typie i priorytecie.
  

Jak widać konstruktory te pozwalają określić następujące cechy komend:

Metody klasy Command pozwalają uzyskać informacje o cechach komendy: etykietach, typie i priorytecie.

Metody klasy Command
 int getCommandType()
dostarcza typu komendy         
String getLabel()
dostarcza krótkiej etykiety komendy.
String getLongLabel()
dostarcza długiej etykiety komendy.
 int getPriority()
dostarcza priorytetu komendy.
 

Komendy (obiekty Command) dołączane mogą być do obiektu Displayable za pomocą metody addCommand(Command) .

Podsumowując wysokopoziomową obsługę zdarzeń podamy schemat takiej obsługi  dla komponentu Displayable:

Poniższy program wykorzystuje mechanizm wysokopozomowej obsługi zdarzeń do wprowadzenia elementów tablicy o podanej przez użytkownika długości. Wprowadzone elementy są następnie wyświetlane na ekranie.

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;

public class Tablica extends MIDlet 
implements CommandListener {
	
    public Display display;
    public TextBox tb;
    public Command exitCom,okCom,itemCom ;
    public String str;
    int liczITEM;
    int[] tab;
    int size;
    boolean initSize;
    boolean initTab;
    
    public Tablica() {                
        display = Display.getDisplay(this);
        
        exitCom = new Command("Exit", Command.EXIT, 1);
        okCom = new Command("OK", Command.OK, 1);        
        itemCom = new Command("Item", Command.ITEM, 1);
        
        tb = new TextBox("Podaj długość tablicy->OK","",256,2);
        tb.addCommand(exitCom);
        tb.addCommand(okCom);
        tb.addCommand(itemCom);
        tb.setCommandListener(this);
        
        str = "";                     
               
    }
    
    public void startApp() {                               
      display.setCurrent(tb);                                
    }

    public void pauseApp() {
    	
    }
  
    public void destroyApp(boolean unconditional) {
    	
    }

    public void commandAction(Command c, Displayable s) {
                
      if(c==okCom && !initSize){
      	
      	try{      	     	  
      	  size=Integer.parseInt(tb.getString());
      	  initSize=true;
        }
        catch(NumberFormatException e){return;}
              	      	            
        tab=new int[size];
      	tb.setString("");
      	tb.setTitle("Wprowadź elem-"+liczITEM+"->ITEM");      	      	      	        	
      
      }
      if(c==itemCom && initSize){                              
        if(liczITEM<size){
          
          try {	         
            tab[liczITEM]=Integer.parseInt(tb.getString());                                   
          }
          catch(NumberFormatException e){return;}
          
          str+="tab["+liczITEM+"]="+tab[liczITEM]+"\n";        	
          tb.setString("");
          liczITEM++;
          tb.setTitle("Wprowadź elem-"+liczITEM+"->ITEM");
        }
        if(liczITEM>=size){          
          tb.setTitle("Zainicjowana tablica");
          tb.setConstraints(0);  
          tb.setString(str);                    	
        }	  
                
      }          
        
      if(c==exitCom){destroyApp(true);notifyDestroyed();}
    }
                 
}//class Tablica

 

Niskopoziomowa obsługa  zdarzeń - klasa Canvas

Abstrakcyjna klasa  Canvas jak każdy Displayable może dodawać komendy oraz ustalać słuchacza komend.
 
Jest również klasą bazową stosowaną w aplikacjach stosujących  niskopoziomową obsługę  zdarzeń oraz wykreślających obiekty graficzne na wyświetlaczu. Dostarcza ona pól i metod do obsługi  zdarzeń pochodzących od klawiatury, akcji zwiazanych z grami oraz zdarzeń pochodzących od urządzenia wskaźnikowego (jeżeli takie  istnieje).
Deklaruje  również metodę abstrakcyjną paint() która musi być zdefiniowana w podklasie rozszerzającej klasę  Canvas.
 
Jedyny konstruktor klasy Canvas jest chroniony co oznacza ze można go użyć w klasie rozszerzającej klasę Canvas.

W klasie Canvas wystepują dwie grupy stałych statycznych : pierwsza grupa zwiazana z kodami klawiszy standardowej klawiatury telefonicznej ITU-T a druga grupa związana z akcjami występującymi w czasie gry.

Zdarzenia klawiatury

MIDP definiuje 12 standardowych klawiszy używanych na klawiaturze telefonicznej:
Kody klawiszy klawiatury ITU-T
static int KEY_NUM0  (kod klawisza "0")
static int KEY_NUM1  (kod klawisza "1")
static int KEY_NUM2  (kod klawisza "2" )
static int KEY_NUM3  (kod klawisza "3")
static in  KEY_NUM4 (kod  klawisza "4")
static int KEY_NUM5  (kod klawisza "5" )
static int KEY_NUM6  (kod klawisza "6" )    
static int KEY_NUM7  (kod klawisza "7" )
static int KEY_NUM8  (kod klawisza "8")
static int KEY_NUM9  (kod klawisza "9" )
static int KEY_POUND (kod klawisza "#")
static int KEY_STAR   (kod klawisza "*" )

Wymienione kody klawiszy są kodami Unikodu znaków występujących na klawiszach.

Na klawiaturze konkretnego urządzenia mogą być obecne inne klawisze których kody są inne od wymienionych. Jednakże żeby zapewnić przenośność aplikacji powinno stosować się klawisze standardowe.

Powyższe kody klawiszy przekazywane są w momencie wywołania wstecznego metod obsługi.
Metody klasy Canvas związane z klawiaturą 
 int getGameAction(int keyCode)
dostarcza akcji gry skojarzonej z klawiszem o podanym kodzie.
 int getKeyCode(int gameAction)
dostarcza kod jednego z klawiszy skojarzonego z podaną akcją gry.
String getKeyName(int keyCode)
dostarcza nazwy klawisza       
 boolean hasRepeatEvents()
sprawdza czy platforma może generować zdarzenia powtarzania przy wciśsniętym klawiszu.
protected  void keyPressed(int keyCode)
wywoływana przy naciśnięciu klawisza
protected  void keyReleased(int keyCode)
wywoływana przy zwolnieniu klawisza.
protected  void keyRepeated(int keyCode)
wywoływana przy utrzymywaniu wciśsniętego klawisza.
 

Metody keyPressed(),keyReleased(),keyRepeated() wywoływane są przez system po zajściu odpowiedniego zdarzenia na klawiaturze. 

Akcje gry

Aplikacje wykorzystujące zdarzenia pochodzące od klawiszy strzałek  i zdarzenia związane powinny  używać tzw. akcji gry po to żeby były przenośne a nie bezpośredni kodów lub nazw klawiszy.

W poniższej tabeli pokazano zestaw stałych statycznych klasy Canvas reprezentujących akcje gry.

Pola klasy Canvas związane z akcjami gry
static int DOWN (stała akcji DOWN)
static int FIRE  (stała akcji FIRE)
static int GAME_A  (stała akcji ogólnego przeznaczenia "A")
static int GAME_B  (stała akcji ogólnego przeznaczenia  "B"  )
static int GAME_C  (stała akcji ogólnego przeznaczenia  "C" )
static int GAME_B  (stała akcji ogólnego przeznaczenia  "D" )
static int LEFT  (stała akcji LEFT)
static int RIGHT  (stała akcji RIGHT)
static int UP  (stała  akcji UP )

 Każdy kod klawisz może być mapowany na co najwyżej jedną akcję gry. Jednakże jedna akcja gry może mapowana na kilka kodów klawiszy. Sprawdzenia mapowań tych dokonujemy odpowiednio za pomocą metod getGameAction(keyCode) i getKeyCode(gameAction).W przypadku drugiej z wymienionych metod zwraca ona jeden z możliwych kodów klawisza. Wyniki mapowań zależą w dużym stopniu od danego urządzenia.  Dla jednego akcje gry UP,DOWN,LEFT,RIGHT mogą być mapowane na czterokierunkowy klawisz nawigacji; dla innego urządzenia mogą to być klawisze numeryczne 2,4,6,8.

Implementacja nie pozwala na zmianę mapowań w czasie egzekucji programu.

Zdarzenia związane z urządzeniem wskaźnikowym

Poniżej przedstawiono zestaw metod klasy Canvas związanych z urządzeniem wskaźnikowym(o ile jest dostępne)

Metody klasy Canvas związane ze wskaźnikiem
 boolean hasPointerEvents()
sprawdza czy platforma obsługuje zdarzenia wskaźnikowe.
 boolean hasPointerMotionEvents()
sprawdza czy platforma obsługuje ruch wskaźnika( przeciąganie).
protected  void pointerDragged(int x, int y)
wywoływana gdy wskaźnik jest przeciągany.
protected  void pointerPressed(int x, int y)
wywoływana gdy wskaźnik jest wciśnięty.
protected  void pointerReleased(int x, int y)
wywoływana gdy wskaźnik jest zwolniony.

Wykreślanie w klasie Canvas.

Klasa Canvas umożliwia również wykreślanie  na wyświetlaczu przy pomocy podanego wykreślacza

Komponent Canvas może wyświetlany być w trybie normalnym lub w trybie pełnego ekranu. W trybie normalnym obszar wyświetlacza może być zajęty przez etykiety komend, tytuł i tekst taśmowy. Ustawienie trybu pełnego ekranu jest żądaniem przydzielenia komponentowi Canvas tak dużo obszaru wyświetlacza jak to jest możliwe. W tym trybie tytuł i tekst taśmowy nie są wyświetlane natomiast komendy mogą być prezentowane w inny sposób niż normalnie np. w jako menu wyskakujące. Ponadto implementacja może wykorzystywać część wyświetlacza np. do prezentacji wskaźników statusu.

Domyślnie komponent ten jest w trybie normalnym.

Metody klasy Canvas związane z wykreślaniem
protected  void hideNotify()
wywoływana bezpośrednio po usunięciu obiektu Canvas z wyświetlacza.
 boolean isDoubleBuffered()
sprawdza czy wyświetlanie jest  buforowane.
protected abstract  void paint(Graphics g)
wykreśla komponent  Canvas. .
 void repaint()
żąda wykreślenia całego komponentu.
 void repaint(int x, int y, int width, int height)
żąda wyświetlenia części komponentu określonej przez punkt x,y oraz rozmiary width,height.
 void serviceRepaints()
wymusza jak najszybsze odrysowanie komponentu           
 void setFullScreenMode(boolean mode)
ustawia tryb pełnego ekranu lub tryb normalny. Może spowodować wywołanie sizeChanged().
protected  void showNotify()
wywoływana bezpośrednio przed pojawieniem się na wyświetlaczu..
protected  void sizeChanged(int w, int h)
wywoływana kiedy zmieniają się rozmiary obszaru do wykreślania.

Uwaga:

Kiedy obiekt Canvas jest aktualnym obiektem Displayable użytkownik może wydawać komendy. Komendy są mapowane na klawisze i menus w sposób specyficzny dla danego urządzenia. Dla pewnych urządzeń klawisze używane do komend mogą nakładać się na klawisze które generują zdarzenia klawiatury. W takim przypadku urządzenie powinno dostarczyć przejrzystego sposobu odróżnienia czy dany klawisz dostarcza komendę czy też zdarzenie klawiaturowe. Jeżeli Canvas jest w trybie normalnym zbiór zdarzeń klawiaturowych nie będzie zmieniał się w zależności od liczby komend lub słuchacza komend. Jeżeli Canvas jest w trybie pełnego ekranu to w przypadku nieobecności słuchacza komend implementacja może dostarczać zdarzenia klawiaturowe pochodzące od klawiszy zarezerwowanych dla i komend. Projektanci gier powinni być świadomi że sposób dostępu do komend może zmieniać się od urządzenia do urządzenia i fakt ten może utrudnić  realizację gry.

Podsumowując obsługę błędów w klasie Canvas należy podkreślić że klasa ta definiuje kilka metod obsługi wywoływanych przez implementację. Są to :

showNotify(),hideNotify()

keyPressed(), keyRepeated(), keyReleased()

pointerPressed(), pointerDragged(), pointerReleased()

paint()

Metody te są wywoływane sekwencyjnie - jedna metoda się kończy i dopiero wywoływana jest następna. Wyjątkiem od tej reguły jest metoda serviceRepaints() która blokuje się dopóki nie wykona się paint()

Metody związane z klawiszami, ze wskaźnikiem i metoda paint() będą wywołane tylko wtedy gdy komponent Canvas bedzie widoczny na wyświetlaczu tzn. po wywołaniu showNotify() a przed hideNotify(). Wywołania metody run() wymuszone przez callSerially(Runnable) mogą wystąpić niezależnie od showNotify() i hideNotify(). Zmiany widoczności komponentu Canvas mogą być powodowane przez AMS przy zmianie stanu z pierwszego planu na stan działania w tle i odwrotnie. Tak więc wywołania metod showNotify() i hideNotify() nie są sterowane przez midlet i mogą występować dość często. Zadania konfiguracyjne i finalizujące powinny być umieszczane poza tymi metodami.

Poniższy program ilustruje omówione metody klasy Canvas do wyrysowania małego kwadratu na środku pulpitu i przesuwaniu go za klawiszy skojarzonych z akcjami gry: LEFT,RIGHT,UP,DOWN. Uniknięcie migotania  zapewniono poprzez wykreślanie do  bufora pozaekranowego  w przypadku gdy system nie zapewnia podwójnego buforowania. 

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;

public class NoFlicker extends MIDlet 
implements CommandListener {
	
    public Display display;
    public TextBox tb;
    public Command exit,back ;
    public String str="";
    MyCanvas mc;
    int left,right,up,down;
    
    public NoFlicker() { 
           
      display = Display.getDisplay(this);
      
      exit = new Command("EXIT", Command.EXIT, 1);
      back = new Command("BACK", Command.BACK, 1);
      
      tb = new TextBox("CANVAS INFO","",1024,0);
      tb.addCommand(back);      
      tb.setCommandListener(this);                                           
      
      mc = new MyCanvas();
      mc.addCommand(exit); 
      mc.setCommandListener(this);    
      
      left = mc.getKeyCode(mc.LEFT);
      right =mc.getKeyCode(mc.RIGHT);
      up = mc.getKeyCode(mc.UP);
      down = mc.getKeyCode(mc.DOWN);
      
      str=str+"LEFT: " + mc.getKeyName(left)+"\n";
      str=str+"RIGHT: " + mc.getKeyName(right)+"\n";
      str=str+"UP: " + mc.getKeyName(up)+"\n";
      str=str+"DOWN: " + mc.getKeyName(down);
      
      tb.setString(str);
      
    }
    
    public void startApp() {                                     
      display.setCurrent(tb);
               
    }
    public void pauseApp() {
    	
    }  
    public void destroyApp(boolean unconditional) {
    	
    }

    public void commandAction(Command c, Displayable s) {   
      if(c==back)display.setCurrent(mc);      
      if(c==exit){ destroyApp(true);notifyDestroyed();}
    }
                 
  class MyCanvas extends Canvas {        
       
    Image buffer; //bufor
    Graphics gb;  //wykreślacz bufora
    int w,h;      //rozmiary bufora
    int wr,hr;    //rozmiary prostokąta
    int xp,yp;    //położenie początkowe
    int x,y;      //położenie aktualne
    int dx,dy;    //przesunięcie X i Y
        
    public MyCanvas(){    	    	
        
        h = getHeight(); w = getWidth();     
        wr = w/10; hr = h/10;
        xp = w/2-wr/2; yp = h/2-hr/2;
        dx = w/40; dy = h/40;
        
        //utworzenie bufora i wykreślacza bufora        
        buffer = Image.createImage(w,h);
        gb = buffer.getGraphics();                  
                      
        x = xp ; y = yp;
                      
    }//MyCanvas()
        
     public void paint( Graphics g ){
                       
      if( !isDoubleBuffered() ){//brak p.buforowania systemowego
      	g.drawImage( buffer,0,0, g.LEFT | g.TOP );
      }
      else {//jest buforowanie systemowe
        g.setColor(255,255,255);
        g.fillRect( 0, 0, w,h ); 
      	g.setColor( 0,255,0);//green
        g.fillRect( x, y, wr,hr );
      }
      
    }//paint()
    
    public void keyPressed(int keyCode){
      
      //jaką akcję daje wciśnięty klawisz
      int action = getGameAction(keyCode);
      	
      if(action == LEFT){
      	x -= dx; if(x <= 0)x = 0;      
      }  
      if(action == RIGHT){
      	x += dx; if(x + wr >= w)x = w-wr;      
      }  
      if(action == UP){
      	y -= dy; if(y <= 0)y = 0;      
      }  
      if(action==DOWN){
      	y += dx; if(y + hr > h)y = h-hr;      
      }  
            
      if( !isDoubleBuffered() ){
      	gb.setColor(255,255,255);
        gb.fillRect( 0, 0, w,h );
      	gb.setColor(255,0,0);//red
        gb.fillRect( x, y, wr,hr );        
      }
      repaint();	
    }
      
  }//class MyCanvas

}//class NoFlicker

 

Wykreślacz obiektów graficznych - klasa Graphics

Jako ostatnią klasę pakietu GUI omówimy krótko klasę wykreślacza Graphics. Obiekt klasy Graphics pozwala wykreślanie i wypełnianie tekstu, obrazów, linii, prostokątów i łuków.

Stosowany jest 24-bitowy model koloru gdzie  trzy bajty opisują składową czerwoną, zieloną i niebieską .W urządzeniach nie obsługujących pełnego 24-bitowego modelu kolory wymagane przez aplikację będą mapowane na kolory dostępne w urządzeniu. Charakterystyki urządzenia można uzyskać za pomocą metod omawianej już klasy Display.

We wszystkich operacjach wykreślania piksel wynikowy tworzony jest jako kombinacja piksla źródłowego i docelowego zgodnie z zasadą SOD (Source over Destination).

Dla tekstu, linii, prostokąta i łuku piksel źródłowy reprezentuje bieżący kolor wykreślacza całkowicie nieprzezroczysty. Zasada SOD oznacza w tym przypadku zastąpienie piksla docelowego pikslem źródłowym .

Dla wykreślania obrazów źródłem jest sam obraz a nie bieżący kolor wykreślacza. W tym przypadku zasada SOD oznacza: całkowicie nieprzezroczysty piksel źródłowy zamienia piksel docelowy, całkowicie przezroczysty piksel źródłowy nie zmienia piksla docelowego a półprzezroczysty piksel źródłowy jest wymieszany z pikslem docelowym.  Jeżeli implementacja nie obsługuje mieszania wszystkie półprzezroczyste piksele obrazu w czasie jego tworzenia muszą być zastąpione całkowicie przezroczystymi.                  

Obiekty graficzne mogą być wykreślane bezpośrednio na wyświetlaczu za pomocą metody paint() klasy Canvas. Wykreślacz dostęepny jest w tej metodzie jako jej parametr tylko w czasie działania tej metody.

Obiekty graficzne mogą być też wykreślane poza ekranem do bufora. Wykreślacz dostępny jest wówczas po wywołaniu metody getGraphics() na rzecz obrazu modyfikowalnego. Wykreślacz taki dostępny jest przez cały czas działania aplikacji.

Wykreślanie tekstu lub obrazu oparte jest o punkty zakotwiczenia. Celem ich użycia jest zminimalizowanie obliczeń wymaganych do umieszczenia tekstu w odpowiedniej pozycji. Pozycjonowanie tekstu wymaga bowiem wywołania metody stringWidth() lub charWidth() klasy Font oraz dokonania kombinacji operacji arytmetycznych. Do określania punktu zakotwiczenia używa się kilku stałych statycznych klasy Graphics:

Stałe klasy Graphics określające punkty zakotwiczenia w poziomie i w pionie
static int BASELINE
punkt zakotwiczenia na linii bazowej tekstu.
static int BOTTOM
punkt zakotwiczenia poniżej tekst lub obrazu.
static int HCENTER
tekst lub obraz wycentrowany w poziomie względem punktu zakotwiczenia
static int LEFT
punkt zakotwiczenia z lewej strony  tekstu lub obrazu.
static int RIGHT
punkt zakotwiczenia  z prawej strony tekstu lub obrazu.
static int TOP
punkt zakotwiczenia powyżej tekstu lub obrazu.
static int VCENTER
tekst lub obraz wycentrowany w pionie względem punktu zakotwiczenia.

Stałe te są nazwami położeń w poziomie i w pionie na bokach prostokąta ograniczającego tekst lub obraz.

Dla tekstu szerokość i wysokość prostokąta ograniczajacego otrzymujemy przez wywołanie metod stringWidth()  i getHeight() klasy Font. Uwzględniają one odstępy między znakami i między liniami.

Dla obrazu wymiary prostokąata ograniczającego uzyskujemy poprzez metody getWidth() i getHeight() klasy Image.

Stałe LEFT,HCENTER,RIGHT w alternatywie bitowej ze stałymi TOP,BASELINE,BOTTOM   tworzą punkt zakotwiczenia. Określa on położenie prostokąta ograniczającego tekst lub obraz względem podanego punktu (x,y). Punkty zakotwiczenia są argumentami następujących metod klasy Graphics :

Metody klasy Graphics wykorzystujące punkt zakotwiczenia
 void copyArea(int x_src, int y_src, int width, int height, int x_dest, int y_dest, int anchor)
kopiuje obszar prostokątny (x_src, y_src, width, height) do obszaru docelowego, którego punkt zakotwiczenia   anchor jest ulokowany w punkcie  (x_dest, y_dest).
 void drawChar(char character, int x, int y, int anchor)
wykreśla podany znak o podanym punkcie zakotwiczenia.
 void drawChars(char[] data, int offset, int length, int x, int y, int anchor)
wykreśla znaki z tablicy  w miejscu o podanym punkcie zakotwiczenia.
 void drawImage(Image img, int x, int y, int anchor)
wykreśla obraz o podanym punkcie zakotwiczenia.
 void drawRegion(Image src, int x_src, int y_src, int width, int height, int transform, int x_dest, int y_dest, int anchor)
kopiuje podany obszar obrazu do miejsca podanego przez punkt zakotwiczenia.
 void drawString(String str, int x, int y, int anchor)
wykreśla podany łańcuch w miejscu określonym przez punkt zakotwiczenia.
 void drawSubstring(String str, int offset, int len, int x, int y, int anchor)
wykreśla podłańcuch podanego łańcucha w miejscu określonym przez punkt zakotwiczenia

Przykładowe wywołanie  g.drawString("abc",25,35, TOP|LEFT) oznacza że punkt znajdujący się na górze i z lewej strony prostokąta ograniczjącego napis "abc" (lewy górny wierzchołek) ma znaleźć się w punkcie (25,35).

Pozostałe metody klasy Graphics odpowiadają metodom z platformy J2SE więc nie będziemy ich tutaj omawiać.

Poniższy program prezentuje animację małego kwadratu na pulpicie za pomocą metody callSerially() z klasy Display. Zapewnione jest jak w poprzednim przykładzie podwójne buforowanie.

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import java.util.Random;

public class Animacja extends MIDlet 
implements CommandListener {
	
    public Display display;   
    public Command exit,start,stop;    
    MyCanvas mc;    
    Random random;
    boolean running;
    
    public Animacja() { 
           
      display = Display.getDisplay(this);
      
      start = new Command("START", Command.BACK, 1);
      stop = new Command("STOP", Command.BACK, 1);
      exit = new Command("EXIT", Command.EXIT, 2);   
           
      mc = new MyCanvas();
      mc.addCommand(start);
      mc.addCommand(stop);
      mc.addCommand(exit);       
      mc.setCommandListener(this); 
      
      random = new Random();         
           
    }
    
    public void startApp() {                                     
      display.setCurrent(mc);
               
    }
    public void pauseApp() {
    	
    }  
    public void destroyApp(boolean unconditional) {
    	
    }

    public void commandAction(Command c, Displayable s) {   
    
      if(c==start) mc.start();
      if(c==stop)  mc.stop();      
      if(c==exit){ destroyApp(true);notifyDestroyed();}
    }
                 
  class MyCanvas extends Canvas 
  implements Runnable {        
       
    Image buffer; //bufor
    Graphics gb;  //wykreślacz bufora
    int w,h;      //rozmiary bufora
    int wr,hr;    //rozmiary prostokąta
    int xp,yp;    //położenie początkowe
    int x,y;      //położenie aktualne
    int dx,dy;    //przesunięcie X i Y
    int seed;
        
    public MyCanvas(){    	    	
        
        h = getHeight(); w = getWidth();     
        wr = w/10; hr = h/10;
        xp = w/2-wr/2; yp = h/2-hr/2;
        dx = w/40; dy = h/40;
                
        buffer = Image.createImage(w,h);
        gb = buffer.getGraphics();                  
                      
        x = xp ; y = yp;
                      
    }//MyCanvas()
        
    public void paint( Graphics g ){
                       
      if( !isDoubleBuffered() ){ //brak buforowania systemowego
      	g.drawImage( buffer,0,0, g.LEFT | g.TOP );
      }
      else { //jeżeli buforowanie systemowe
        g.setColor(255,255,255);
        g.fillRect( 0, 0, w,h ); 
      	g.setColor( 0,255,0);//green
        g.fillRect( x, y, wr,hr );
      }
      
    }//paint()
    
    public void run() {       	
      //losowanie liczb ze zbioru {0,1,2,3}
      switch(random.nextInt()%4){
        //ruch w lewo,w prawo,do góry lub do dołu    	
        case 0: x -= dx; if(x <= 0)x += dx; break;              
        case 1: x += dx; if(x + wr >= w)x -= dx; break;           
        case 2: y -= dy; if(y <= 0)y += dy; break;           
        case 3: y += dy; if(y + hr > h)y -= dy; break;      
      }
            
      if( !isDoubleBuffered() ){
      	gb.setColor(255,255,255);
        gb.fillRect( 0, 0, w,h );
      	gb.setColor(255,0,0);//red
        gb.fillRect( x, y, wr,hr );        
      }
      repaint();
      if(running)display.callSerially(this);	
      
    }//run()
    
    public void start(){running = true; display.callSerially(this);}
    public void stop() {running = false;}  
    
  }//class MyCanvas

}//class Animacja

 


13.7.  GRY

MIDP2.0 dostarcza  specjalne klasy  pozwalające na tworzenie gier dla urządzeń bezprzewodowych.  Strutura tych bibliotek uwzględnia ograniczenia pamięci i mocy procesora i przewiduje wykorzystanie kodu natywnego, akceleratorów sprzętowych formatów obrazów  specyficznych dla danego urządzenia. Biblioteki te znajdują się w pakiecie javax.microedition.lcdui.game.

 
Klasy pakietu javax.microedition.lcdui.game.
GameCanvas podstawowa klasa GUI dla gier. Pozwala na odpytanie stanu klawiszy i wymiecenie bufora na ekran.                        Rozszerza Canvas.
Layer warstwa-reprezentuje wizualny element gry. Dostarcza podstawowych atrybutów takiego elementu jak położenie, rozmiar i widoczność.
LayerManager zarządza szeregiem warstw. Pozwala ustawić okno widoku i automatycznie wykreśla warstwy do tego widoku.
Sprite duszek-podstawowy element wizualny który może być wyświetlany w kilku kadrach zgromadzonych w Image. Wyświetlanie różnych kadrów daje efekt animacji. Dostarcza metod transformacji i detekcji kolizji komponentów.Rozszerza Layer
TiledLayer wizualny element utworzony z siatki komórek które mogą być wypełnione zbiorem obrazów. Komórki mogą być również wypełnione animowanymi częściami. Pozwala na utworzenie dużych powierzchni z graficzną zawartością np. falującej powierzchni wody. Rozszerza  Layer.
 

Metody modyfikujące stan LayerManager, Layer, Sprite i TiledLayer nie dają bezpośrednio widocznego efektu na ekranie. Stan ten zapisany jest wewnątrz danego obiektu i ujawnia się w czasie kolejnych wywołań metody paint() klasy GameCanvas. Podejśie to jest charakterystyczne dla gier gdzie wewnątrz każdego cyklu gry stany obiektów są uaktualniane a pod koniec cyklu cały ekran jest odświeżany . 

Podstawowa klasa dla gier to GameCanvas rozszerzająca Canvas. Oprócz cech właściwych dla Canvas wprowadza możliwości specyficzne dla gier t.jak bufor pozaekranowy oraz odpytywanie klawiatury o jej stan.

Stałe klasy GameCanvas repezentujące stany klawiszy związane z akcjami gry
static int DOWN_PRESSED
bit reprezentujący wciśnięty klawisz DOWN          
static int FIRE_PRESSED
bit reprezentujący wciśnięty klawisz FIRE              
static int GAME_A_PRESSED
bit reprezentujący wciśnięty klawisz GAME_A              
static int GAME_B_PRESSED
bit reprezentujący wciśnięty klawisz GAME_B          
static int GAME_C_PRESSED
bit reprezentujący wciśnięty klawisz GAME_C           
static int GAME_D_PRESSED
bit reprezentujący wciśnięty klawisz GAME_D         
static int LEFT_PRESSED
bit reprezentujący wciśnięty klawisz LEFT          
static int RIGHT_PRESSED
bit reprezentujący wciśnięty klawisz RIGHT
static int UP_PRESSED
bit reprezentujący wciśnięty klawisz UP              
 

 
Jedyny konstruktor klasy Canvas
protected GameCanvas(boolean suppressKeyEvents)
 pozwala zablokować zdarzenia pochodzące z klawiatury         
 
Metody klasy GameCanvas
 void flushGraphics()
 wymiata cały dedykowany bufor  na ekran         
 void flushGraphics(int x, int y, int width, int height)
wymiata podany obszar dedykowanego bufora          
Graphics getGraphics()
 dostarcza dedykowany bufor do wykreślania komponentu GameCanvas
int getKeyStates()
podaje stany klawiszy. Dostarczoną wartość mnoży się  bitowo się ze stałymi reprezentującymi stany klawiszy         
 void paint(Graphics g)
wykreśla komponent GameCanvas          
 

Dla każdego obiektu GameCanvas dedykowany jest osobny, unikalny dla tego obiektu  bufor. Zawartość tego bufora może być modyfikowana tylko przez wykreślacz dostarczony przez metodę otrzymany przez getGraphics() klasy GameCanvas, natomiast nie jest modyfikowana przez inne midlety lub system. Poczatkowo bufor wypełniony jest białymi pikselami.

Rozmiar tego bufora jest taki jak maksymalne rozmiary obiektu GameCanvas, natomiast jego aktualne rozmiary (przy uwzględnieniu takich komponentów jak Ticker,Command) mogą być pobrane za pomocą getWidth() i getHeight()  i takie też są rozmiary obszaru wymiatanego na ekran

Gra może dostarczać własnego wątku realizującego pętlę gry. Pętla ta sprawdza wejście (klawiaturę), implementuje logikę gry i wykreśla zaktualizowany komponent GameCanvas. 

Przykładowa pętla gry:

      //pętla gry oparta jest na pętli nieskończonej while(true)

        Graphics gb = getGraphics(); //wykreslacz bufora
        while (true) {
          // sprawdzenie stanu klawiszy
          int keyState = getKeyStates();
          if ((keyState & LEFT_PRESSED) != 0) sprite.move(-1, 0);
          else if ((keyState & RIGHT_PRESSED) != 0) sprite.move(1, 0);
         
          // ustawienie koloru tła na biały i wypełnienie nim bufora
          gb.setColor(0xFFFFFF);
          gb.fillRect(0,0,getWidth(), getHeight());
          
          // rysowanie duszka
          sprite.paint(gb);
          
          // wymiecenie bufora na ekran
          flushGraphics();
      }//while
  

 


13.8. PAMIĘĆ TRWAŁA  (pamięć rekordów)

Profil MIDP dostarcza mechanizmu gromadzenia danych i odzyskiwania  tych danych z pamięci trwałej. Mechanizm ten nazywa się systemem zarządzania rekordami danych (skrót ang. RMS) i jest oparty  o bazę danych rekordów.

Rekordy są tablicami bajtów w związku z czym programista może używać klasycznych strumieni ByteArrayInputStream oraz ByteArrayInputStream w połączeniu z DataInputStream and DataOutputStream do pakowania i odpakowywania danych różnych typów do i z rekordów.

Rekordy są jednoznacznie identyfikowane w danej pamięci rekordów poprzez numer identyfikacyjny (klucz pierwotny w bazie). Pierwszy utworzony rekord ma numer 1, numery następnych rekordów zwiększają się kolejno o jeden. Midlety mogą tworzyć własne indeksy rekordów poprzez użycie klasy RecordEnumeration.

Pamięć rekordów jest kolekcją rekordów danych które pozostają stałe podczas wielokrotnych wywołań midletu. Pamięć rekordów jest jednoznacznie identyfikowalna poprzez swoją nazwę i nazwę jednostki aplikacji. Nazwy  pamięci rekordów są czułe na wielkość liter i mogą składać się z kombinacji do 32 znaków Unikodu.

Midlety w ramach jednostki aplikacji mogą tworzyć wiele pamięci rekordów pod warunkiem że każda z nich ma swoją unikalną nazwę. Pamięci rekordów mogą być współdzielone między midletami w ramach tej jednostki. Mogą być również współdzielone między różnymi jednostkami aplikacji.

W mechanizmie RMS nie przewidziano operacji blokowania. Implementacja pamięci rekordów zapewnia że wszystkie operacje na rekordach są atomowe, synchronizowane i serializowane tak że nie powinno wystąpić zniekształcenie danych w czasie wielowątkowego dostępu. Jednakże jeżeli sam midlet używa wielu wątków uzyskujących dostęp do rekordu danych to midlet ten powinien być odpowiedzialny za koordynację tego dostępu,mimo że pamięć rekordów będzie szeregować kolejne operacje różnych wątków we właściwy sposób - kolejne nadpisywanie rekordów może spowodować problemy w samym midlecie a nie w pamięci  rekordów.

Pamięci rekordów są oznakowywane czasem i datą ostatniej modyfikacji oraz numerem wersji zwiększającym się przy każdej modyfikacji zawartości.

Jeżeli jednostka aplikacji jest usuwana z platformy urządzenia, wszystkie pamięci rekordów skojarzone z tymi midletami muszą byc również usunięte.

Biblioteki RMS zawarte są w pakiecie javax.microedition.rms. Składa się on z  4 interfejsów, jednej klasy głównej RecordStore i 5-ciu klas wyjątków.

 
Interfejsy pakietu javax.microedition.rms
RecordComparator Definiuje komparator porównujący dwa rekordy w sposób zdefiniowany przez programistę.
RecordEnumerator Reprezentuje dwukierunkowy iterator pamięci rekordów
RecordFilter  Definiuje filtr wybierający rekordy spełniające określone, podane kryteria.
RecordListener Obsługa zdarzeń powstających przy dodawaniu, usuwaniu i zmianie rekordów pamięci danych. .Deklaruje trzy metody obsługi  których argumentami są: pamięć rekordów i rekord podlegający operacji:

Interfejs RecordComparator

Pola interfejsu to stałe statyczne typu int zwracane przez jedyną metodę  compare() tego interfejsu.Parametrami tej metody są dwa rekordy - tablice bajtów rec1 i rec2.

Pola interfejsu RecordComparator
static int EQUIVALENT 
 oznacza że dwa rekordy rec1 i rec2 są takie same.
static int FOLLOW
  oznacza że rec1 nastepuje po rec2.
static int PRECEDES
 oznacza że rec1 poprzedza rec2..
Metoda (jedyna) interfejsu RecordComparator
 int compare(byte[] rec1, byte[] rec2)
zwraca:  RecordComparator.PRECEDES jeżeli rec1 poprzedza rec2                                                                                     RecordComparator.FOLLOWS jeżeli rec1 następuje po rec2                                                                                         RecordComparator.EQUIVALENT jeżeli rec1 i rec2 są równoważne
 

Interfejs RecordEnumerator

Interfejs ten reprezentuje dwukierunkowy iterator rekordów w pamięci rekordów. Iterator ten będzie iterować cały zbiór rekordów lub podzbiór rekordów jeżeli użyto danego filtru (RecordFilter) w porządku określonym przez podany komparator (RecordComparator).

Metody interfejsu RecordEnumerator
 boolean hasNextElement()
sprawdza czy istnieje następny rekord.
 boolean hasPreviousElement()
sprawdza czy istnieje poprzedni element.
 void keepUpdated(boolean keepUpdated)
 ustala tryb uaktualniania rekordów po operacjach dodawania, usuwania i zmiany.
 byte[] nextRecord()
zwraca kopię następnego  rekordu  biorąc pod uwagę podany filtr i komparator.
 int nextRecordId()
zwraca identyfikator następnego rekordu biorąc pod uwagę podany podany filtr i komparator.
 int numRecords()
zwraca ilość rekordów dostępnych w zbiorze lub podzbiorze rekordów.
 byte[] previousRecord()
 zwraca poprzedni rekord biorąc pod uwagę podany filtr i komparator.
 int previousRecordId()
 zwraca identyfikator poprzedniego rekordu biorąc pod uwagę podany filtr i komparator.
 void rebuild()
uaktualnia iterator zgodnie z aktualnym zbiorem rekordów.
 

Jeżeli w trakcie iteracji pewne rekordy są usuwane lub dodawane do pamięci, zwracane identyfikatory rekordów mogą nie odpowiadać właściwym rekordom. Żeby uniknąć tego  problemu obiekt RecordEnumeration może być słuchaczem RecordStore i reagować na te zmiany poprzez odpowiednią zmianę identyfikatorów. Jednakże to rozwiązanie może znacznie spowolnić działanie aplikacji.

Jeżeli obiekt typu RecordStore jest zamknięty to obiekt iteratora takiego obiektu jest niewłaściwy i wszystkie jego operacje mogą dać niewłaściwe wyniki lub wygenerować wyjątek RecordStoreNotOpenException. Dodatkowo wywołania hasNextElement() i hasPreviousElement() będą zwracać false.

Interfejs RecordFilter

Interfejs definiuje filtr badający czy dany rekord spełnia określone kryteria i jest używany do poszukiwania w pamięci odpowiednich rekordów lub tworzenia podzbiorów rekordów o określonych własnościach.

Jedyna metod interfejsu RecordFilter
 boolean matches(byte[] candidate)
 zwraca true jeżeli  rekord candidate spełnia określone kryteria.
 

Interfejs RecordListener

Interfejs nasłuchu zdarzeń pochodzących z obiektu RecordStore przy dodawaniu, usuwaniu i zmianie rekordów.
 
Metody interfejsu RecordListener
 void recordAdded(RecordStore recordStore, int recordId)
wywoływana po dodaniu rekordu do pamięci.
 void recordChanged(RecordStore recordStore, int recordId)
wywoływana po zmianie rekordu w pamięci.
 void recordDeleted(RecordStore recordStore, int recordId)
 wywoływana po usunięciu rekordu z pamięci.
 

Klasa RecordStore

Jest to główna klasa pakietu rms do operowania na pamięci rekordów.

Klasa główna pakietu javax.microedition.rms
RecordStore Reprezentuje pamięć rekordów i operacje na nich:

Stałe statyczne klasy RecordStore pozwalają na określenie praw dostępu do pamięci rekordów z innych jednostek aplikacji.

Pola klasy RecordStore
static int AUTHMODE_ANY
pozwala na dostęp do pamięci dowolnej jednostce aplikacji.
static int AUTHMODE_PRIVATE
 pozwala na dostęp do pamięci tylko bieżącej jednostce aplikacji.

Klasa RecordStore zawiera dużo metod - tutaj wymienimy tylko niektóre z nich:

Podstawowe metody w klasie RecordStore
 int addRecord(byte[] data, int offset, int numBytes)
dodaje nowy rekord do pamięci rekordów.
 void addRecordListener(RecordKistener listener)
 dodaje słuchacza zmian rekordów do pamięci rekordów.
 void closeRecordStore()
 zamyka pamięć rekordów.
 void deleteRecord(int recordId)
 usuwa rekord o danym identyfikatorze z pamięci rekordów.
static void deleteRecordStore(String recordStoreName)
usuwa pamięć rekordów o danej nazwie.
 RecordEnumeration enumerateRecords(RecordFilter filter, RecordComparator comparator, boolean keepUpdated)
zwraca iterator do iterowania pamięci rekordów. Iterator ten zwraca rekordy w porządku określonym przez comparator  po zastosowaniu filtra filter.
 int getNumRecords()
 zwraca aktualną liczbę rekordów w pamięci rekordów.
 int getVersion()
zwraca wersję pamięci rekordów - przy każdej modyfikacji pamięci poprzez wywołanie addRecord(), setRecord() lub deleteRecord() numer wersji jest zwiększany o jeden
static RecordStore openRecordStore(String recordStoreName, boolean createIfNecessary)
otwiera i ewentualnie tworzy pamięć rekordu dla danej jednostki aplikacji.
static RecordStore openRecordStore(String recordStoreName, boolean createIfNecessary, int authmode, boolean writable)
otwiera i ewentualnie tworzy pamięć rekordów z określonym trybem dostępu authmode.
 void removeRecordListener(RecordListener listener)
odłącza podanego słuchacza od pamięci rekordów..
 void setMode (int authmode, boolean writable)
zmienia tryb dostępu do pamięci rekordów.
 void setRecord(int recordId, byte[] newData, int offset, int numBytes)
ustala rekord o identyfikatorze recordId. Dane pobiera z tablicy bajtów newData począwszy od indeksu offset i wprowadza ilość bajtów równą numBytes.
 

Klasy wyjątków

Pakiet rms zawiera 5 klas wyjątków które mogą być generowane w czasie operacji na pamięci rekordów.

Klasy wyjątków javax.microedition.rms
InvalidRecordException wskazuje na niewłaściwy identyfikator rekordu - operacja nie może byc przeprowadzona.
RecordStoreException ogólny błąd operacji na pamięci rekordów.
RecordStoreFullException wskazuje przepełnienie pamięci systemu -operacja nie może być wykonana.
RecordStoreNotFoundException wskazuje że nie znaleziono obiektu pamięci rekordów - operacje nie może być wykonana .
RecordStoreNotOpenException  wskazuje na próbę operacji na zamkniętej pamięci rekordów.

 W poniższym programie pierwsze wywołanie midletu  tworzy pamięć rekordów i zapisuje w niej kilka rekordów danych o pracownikach. Drugie wywołanie midletu pobiera z pamięci niektóre rekordy (dla których pole wiek >40) stosując odpowiedni filtr .Trzecie wywołanie usuwa bazę danych z pamięci. Pobrane rekordy są sortowane  leksykograficznie i wypisywane  na ekran.

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import javax.microedition.rms.*;

import java.io.*;
import java.lang.*;

public class Rekordy extends MIDlet 
implements CommandListener {
	
    public Display display;
    public TextBox tb;
    public Command exitCom ;
    public String str="";
    static int liczMID;    
    
    RecordStore store;
    Record rec;
    byte[] ba;
    String[] names = {"Kowalski","Mazur","Lewicki","Pawlak","Folenda"};
    int[] ages = {50,30,35,45,41};
    int[] salaries = {3000,3100,1500,1700,2200};
    
    public Rekordy(){                
    
      liczMID++;
      
      display = Display.getDisplay(this);
       
      exitCom = new Command("Exit", Command.EXIT, 1);
                                                         
      tb = new TextBox(" Historia bazy danych-"+liczMID,"",1024,0);
      tb.addCommand(exitCom);        
      tb.setCommandListener(this);                                                       
    }
    
    public void startApp(){                                       
      
      display.setCurrent(tb);                
      
      if(liczMID==1){
      	try{
      	  store=RecordStore.openRecordStore("empList",true);       
        
      	  for(int i=0;i<names.length;i++){      	  	           	
            str=str+names[i]+" "+ages[i]+" "+salaries[i]+"\n";      	
            rec=new Record(names[i],ages[i],salaries[i]);   
            ba=rec.setByteArray();
            store.addRecord(ba,0,ba.length);
           }//for       
           tb.setString("Wpisywane rekordy:"+"\n\n"+str);     		
      		
           store.closeRecordStore();	
        }      	
      	catch(RecordStoreException e){}
      }//if
      
      if(liczMID==2){
      	try{
      	  CompFilter cf=new CompFilter();	
      	  store=RecordStore.openRecordStore("empList",true);                 
      	  RecordEnumeration re=store.enumerateRecords(cf,cf,true);
      	  byte[] ba;
      	  Record rec;
      	  str="";
      	        	  	           
      	  while(re.hasNextElement()){
      	    ba=re.nextRecord();
      	    rec=Record.getRecord(ba);       	         	  	           
            str=str+rec.name+" "+rec.age+" "+rec.salary+"\n";      	                          
          }//while       
          tb.setString("Odfiltrowane rekordy :"+"\n\n"+str);     		
      		
          store.closeRecordStore();	
        }      	
      	catch(RecordStoreException e){}
      }//if                        
        
      if(liczMID==3){
      	try {
          RecordStore.deleteRecordStore("empList");
          tb.setString("Baza usunięta"); 
        }                           
        catch(RecordStoreException e){tb.setString(e.getMessage());}                      
            
      }//if  
                      
    }//startApp()

    public void pauseApp(){
    	
    }
  
    public void destroyApp(boolean unconditional){
    	
    }

    public void commandAction(Command c, Displayable s){                                       
      if(c==exitCom){        	                  	
        destroyApp(true);notifyDestroyed();
      }
    }
                 
}//class ioMidlet4

class Record {
	
  String name;
  int age;
  int salary;	

  public Record() {}
	
  public Record(String name,int age,int salary) {
  	this.name=name;
  	this.age=age;
	this.salary=salary;
  }	

  public byte[] setByteArray()  {
    ByteArrayOutputStream baos=new ByteArrayOutputStream();
    DataOutputStream dos=new DataOutputStream(baos);
    try{	
      dos.writeUTF(name);
      dos.writeInt(age);
      dos.writeInt(salary);
    }
    catch(IOException e){}
  
    return baos.toByteArray();			
  }

  public static Record getRecord(byte[] array) {
    ByteArrayInputStream bais=new ByteArrayInputStream(array);
    DataInputStream dis=new DataInputStream(bais);	
    Record rec=new Record();	
  
    try{
      rec.name=dis.readUTF();
      rec.age=dis.readInt();
      rec.salary=dis.readInt();
    }
    catch(IOException e){}
    return rec;	
}
	
}//class Record

class CompFilter 
implements RecordComparator,RecordFilter {
	
    public int compare(byte[] rec1,byte[] rec2)	{
      ByteArrayInputStream bais1=new ByteArrayInputStream(rec1);
      DataInputStream dis1=new DataInputStream(bais1);	
      
      ByteArrayInputStream bais2=new ByteArrayInputStream(rec2);
      DataInputStream dis2=new DataInputStream(bais2);
      
      String s1="",s2="";
      try{
         s1=dis1.readUTF();
         s2=dis2.readUTF();
      }
      catch(IOException e){}
      
      if(s1.compareTo(s2)<0)return RecordComparator.PRECEDES;
      else if(s1.compareTo(s2)==0)return RecordComparator.EQUIVALENT;
      else return RecordComparator.FOLLOWS;
      
    }//compare()
	
    public boolean matches(byte[] candidate) {
      ByteArrayInputStream bais=new ByteArrayInputStream(candidate);
      DataInputStream dis=new DataInputStream(bais);
      String name="";
      int age=0;
      
      try{
        name=dis.readUTF();        
        age=dis.readInt();
      }
      catch(IOException e){}
         	
	  if(age>40)return true;
	  else return false;	
		
     }//matches()
	
}//class CompFilter

13.9. POŁĄCZENIA  SIECIOWE  Z  INTERNETEM

Uwaga: Wymagania specyfikacji żądają od implementacji dostarczania wskaźnika informującego o aktualnym podłączeniu się do sieci. Jeżeli wskaźnik ten jest wykreślany na wyświetlaczu musi być on widoczny w podczas aktywnego połączenia nawet jeżeli komponent Canvas jest w trybie pełnego ekranu.

 Profil MIDP rozszerza obsługę połączeń sieciowych dostarczoną przez CDLC w ramach struktury połączeń ogólnych (GenericConnection). MIDP obsługuje podzbiór protokołu HTTP implementowanego przy użyciu protokołów IP (TCP/IP) jak innych takich jak WAP pozwalających na dostęp do Interrnetu poprzez łącze (bramę) TCP/IP. Struktura połączeń obsługuje model klient - server i przesyłanie datagramów.

Używanie protokołów zdefiniowanch przez MIDP zapewnia przenośność programów. Implementacje MIDP na różnych urządzeniach muszą zapewnić połączenia i dostęp do serwerów i serwisów HTTP1.1 poprzez wysyłanie żądań HEAD, GET i POST.

Specyfikacja HTTP1.1 dostarcza zbioru nagłówków żądań i odpowiedzi pozwalających ustalać formę, format, język i inne atrybuty przesyłanej zawartości. W profilu  MIDP aplikacja jest odpowiedzialna za selekcję i przetwarzanie nagłówków żądań i odpowiedzi.

W standardowym nagłówku żądania występują pola user-agent i accept-language.Pole user-agent może być uzywane do identyfikacji urządzenia wysyłającego żądanie a pole accept-lanuage do określenia języka żądania.

Pola te powinny zawierać opis cech zdefiniowanych przez właściwości systemowe, które można pobrać za pomocą metody java.lang.system.getProperty(). Wymienione właściwości to:

Przykład nagłówka żądania HTTP:

User-Agent: Profile/MIDP-2.0 Configuration/CLDC-1.0
Accept-Language: en-US

Zestaw interfejsów klas do połączeń sieciowych znajduje się w pakiecie javax.microedition.io. Poniższa tabela zawiera krótki opis bibliotek sieciowych.
 
Interfejsy pakietu javax.microedition.io
CommConnection definiuje połączenie przez port szeregowy. Rozszerza StreamConnection
Connection definiuje bazowy typ połączenia ogólnego - deklaruje tylko jedną metodę close()
ContentConnection definiuje połączenie strumieniowe  z  przesłaną zawartością. Rozszerza StreamConnection
Datagram definiuje ogólny interfejs reprezentujący dane datagramu. Rozszerza DataInput,DataOutput
DatagramConnection definiuje możliwości które musi mieć połączenie do przesyłąnia datagramów. Rozszerza Connection
HttpConnection definuje stałe i metody potrzebne do połączenia HTTP. Rozszerza ContentConnection
HttpsConnection definiuje stałe i metody potrzebne do bezpiecznego połączenia HTTPS. Rozszerza HttpConnection
InputConnection definiuje możliwości wejściowego połączenia strumieniowego. Rozszerza Connection
OutputConnection definiuje możliwości wyjściowego połączenia strumieniowego. Rozszerza Connection
SecureConnection definiuje bezpieczne połączenie strumieniowe poprzez gniazdo. Rozszerza SocketConnection
SecurityInfo definiuje metody dostepu do informacji o bezpiecznych połączeniach sieciowych.
ServerSocketConnection definiuje połączenie strumieniowe gniazda serwera. Rozszerza StreamConnectionNotifier
SocketConnection definiuje połączenie strumieniowe poprzez gniazdo od strony klienta. Rozszerza StreamConnection
StreamConnection definiuje ogólne połączenie strumieniowe. Rozszerza InputConnection i OutputConnection
StreamConnectionNotifier definiuje akceptację i otwarcie połączenia strumieniowego poprzez gniazdo od strony serwera.                   Rozszerza Connection
UDPDatagramConnection  definiuje połączenie do przesyłania  datagramów ze znanym adresem punktu końcowego.                           Rozszerza DatagramConnection
 

Wykorzystanie funkcjonalności tych interfejsów następuje po fabrykacji obiektu ogólnego typu Connection i zrzutowaniu w dół na odpowiedni typ interfejsowy wymaganego połączenia.

Poniższy diagram przedstawia hierarchię dziedziczenia  omówionych interfejsów .

 

W podanych dalej przykładach należy zaobserwować wykorzystanie podanej hierarchii dziedziczenia.

W pakiecie javax.microedition.io mamy dwie konkretne klasy  Connector i PushRegistry.

Klasy pakietu javax.microedition.io
Connector fabrykator obiektów typu Connection.
PushRegistry rejestr (lista)  nie zrealizowanych połączeń.
 

Klasa fabrykatora połączeń Connector dostarcza wymienionych wyżej obiektów typu Connection.

Klasa wyjątku  pakietu javax.microedition.io
ConnectionNotFoundException  sygnalizuje że nie znaleziono obiektu docelowego połączenia.
 
Uwaga: metody interfejsu StreamConnection nie są synchronizowane. Jedyna bezpieczna metoda strumieniowa to close(). Jednakże jeżeli w jednym wątku wywołana zostanie metoda close() na strumieniu wykonywanym w innym wątku wygenerowany zostanie wyjątek InterruptedIOException.

Najważniejszą klasą pakietu jest klasa Connector odpowiedzialna za tworzenie połączeń sieciowych. Pola statyczne tej klasy opisują tryby dostępu do danego połączenia.
Pola klasy Connector
static int READ
 dostęp w trybie czytania (READ).
static int WRITE
 dostęp w trybie  pisania  (WRITE)
static int READ_WRITE
 dostęp w trybie czytania i pisania (READ_ WRITE) - tryb domyślny

Metody tej klasy tworzą połączenia sieciowe typu ogólnego Connection oraz dostarczają strumieni typu InputStream, DataInputStream, OutputStream, DataOutputStream związanych z połączeniem sieciowym. Korzystanie ze strumieni stosujemy w przypadku gdy nie interesują nas właściwości samego połlączenia a tylko przesłanie bądź odbiór danych.

Metody klasy Connector
static Connection open(String url)
tworzy i otwiera połączenie Connection o podanym adresie URL.
static Connection open(String url, int mode)
tworzy i otwiera połączenie Connection o podanym adresie URL i w podanym trybie.
static Connection open(String url, int mode, boolean timeouts)
tworzy i otwiera połączenie Connection o podanym adresie URL, w podanym trybie i warunku  timeout. Jeżeli timeout jest true implementacja protokołu może generować wyjątek InterruptedIOException.
static DataInputStream openDataInputStream(String url)
dostarcza strumienia wejściowego DataInputStream otwartego połączenia.
static DataOutputStream openDataOutputStream(String url)
dostarcza strumienia wyjściowego DataOutputStream otwartego połączenia  .
static InputStream openInputStream(String url)
dostarcza strumienia wejściowego InputStream otwartego połączenia.
static OutputStream openOutputStream(String url)
dostarcza strumienia wyjściowego OutputStream otwartego połączenia

Argumentem wszystkich metod jest łańcuch url zawierający adres URL połączenia. Jego ogólna struktura ma postać:

<protokół> : // <adres hosta> : <port>  ; <parametry>

Protokołem może być : http, https, datagram,  socket, ssl, comm,sms,cbs

Adresem hosta może być adres IP (IPv4 lub IPv6) lub nazwa hosta

Parametry mają postać szeregu przypisań  <nazwa parametru> = <wartość parametru>

Adres hosta i port może być pominięty - przydzielony zostanie dostępny port lokalny.

Obiekt otwartego połączenia dostarczany jest w odnośniku typu Connection . W zależności od postaci adresu URL(w szczególności od protokołu) to otrzymujemy w tym odnośniku połączenie określonego typu.

 

postać adresu URL

Typ dostarczonego obiektu
"http://..." HttpConnection
"https://..." HttpsConnection

"socket://host:port"

SocketConnection

"socket://:port" ServerSocketConnection
"datagram://host:port" UDPDatagramConnection
"datagram://"

DatagramConnection

"ssl://..." SecureConnection

Mając teraz odnośnik typu Connection i typ obiektu w nim przechowywanego możemy wykonać rzutowanie w dół na wymagany typ w celu uzyskania funkcjonalności określonego połączenia, posługując się przedstawionym wcześniej diagramem dziedziczenia interfejsów.

Poniżej podamy kilka fragmentów programów wykorzystujących  omówione  klasy sieciowe.

Fragment-1: serwer pobiera od klienta dane i wysyła je z powrotem(echo-server)

      
      // utworzenie gniazda serwera oczekującego na porcie 1234;
      ServerSocketConnection ssc = (ServerSocketConnection)Connector.open("socket://:1234");
      // oczekiwanie na połączenie; i uzyskanie gniazda do komunikacji z klientem
      SocketConnection sc = (SocketConnection) ssc.acceptAndOpen();
      
      // konfiguracja uzyskanego gniazda komunikacji z klientem;
      sc.setSocketOption(DELAY, 0);
      sc.setSocketOption(LINGER, 0);
      sc.setSocketOption(KEEPALIVE, 0);
      sc.setSocketOption(RCVBUF, 128);
      sc.setSocketOption(SNDBUF, 128);
      
      // uzyskanie strumienia wejściowego gniazda typu DataInputStream
      DataInputStream dis = sc.openDataInputStream();;
      
      // uzyskanie strumienia wyjściowego gnizada typu DataOutputStream;
      DataOutputStream dos = sc.openDataOutputStream();
      
      // wczytanie danych od klienta ze strumienia.
      String result = dis.readUTF();
      
      // wysłanie tych samych danych do klienta
      dos.writeUTF(result);
      
      //zamknięcie strumieni i połączenia
      dis.close(); dos.close();
      sc.close(); ssc.close();
  

Fragment-2: pisanie i czytanie z portu szeregowego

      
      //uzyskanie połączenia z portem szeregowym
      CommConnection comcon = (CommConnection)Connector.open("comm:com0;baudrate=19200");
      
      //pobranie ustawionej prędkości transmisji      
      int baudrate = comcon.getBaudRate();
      
      //uzyskanie strumienia wejsciowego i wyjściowego tego połączenia
      InputStream is = comcon.openInputStream();
      OutputStream os = comcon.openOutputStream();
      int ch = 0;
      
      //zapisywanie i czytanie znaków do strumieni
      while(ch != 'Z') {
       os.write(ch);
       ch = is.read();
       ch++;
      }
      //zamknięcie strumieni i połączenia
      is.close(); os.close(); comcon.close();
  

Fragment-3: przykład połączenia zawierającego przesłaną zawartość

      
      //metoda contentCon() pobiera adres URL,tworzy połączenie i wczytuje zawartość do tablicy
      
      void contentCon(String url) throws IOException {
      ContentConnection cc = null; //połączenie z zawartością
      DataInputStream is = null; //strumień wejściowy połączenia
      try {
        //uzyskanie połaczenia typu ContentConnection
        cc = (ContentConnection)Connector.open(url);
        int len = (int)cc.getLength(); //pobranie ilości bajtów zawartości
        dis = cc.openDataInputStream(); //otworzenie strumienia wejściowego
      
        if (len > 0) {
          //przygotowanie tablicy bajtów
          byte[] data = new byte[len];
          //zapełnienie tablicy bajtów danymi ze strumienia
          dis.readFully(data);
        }
        else { //jeżeli ilość bajtów zawartości nie jest znana czytamy bajt po bajcie
          int ch;
          Vector vec = new Vector(); //tablica automatycznie rozszerzalna
          while ((ch = dis.read()) != -1) {vec.addElement(new Integer(ch)) }
        }
      }//try
      finally {
       //zamknięcie strumienia i połączenia
       if (dis != null) dis.close();
       if (c != null) c.close();
      }
      }
  

Fragment-4: pobieranie danych przez połączenie http za pomocą żądania GET

    
    void httpCon(String url) throws IOException {

       HttpConnection c = null;
       InputStream is = null;
       int rc;
       
       try { //czy nie ma błędu rzutowania w dół - niewłaściwa postać adresu URL
       
         //utworzenie połączenia http jeżeli właściwy adres URL
         c = (HttpConnection)Connector.open(url);
         //ustalenie metody żądania na GET
         c.setRequestMethod(HttpConnection.GET);
       
         //ustawienie pól nagłowka żadania     
         c.setRequestProperty("If-Modified-Since", "29 May 2003 19:43:31 GMT");
         c.setRequestProperty("User-Agent","Profile/MIDP-2.0 Configuration/CLDC-1.0");      
         c.setRequestProperty("Content-Language", "en-US");
         
         //uzyskanie kodu odpowiedzi - jeżeli kod niewłaściwy generacja wyjątku
         rc = c.getResponseCode();
         if (rc != HttpConnection.HTTP_OK) throw new IOException("HTTP response code: " + rc);
         
         //jeżeli kod właściwy otwarcie strumienia wejściowego połączenia
         is = c.openInputStream();
         // pobranie typu zawartości
         String type = c.getType();
         // pobranie długości zawartości
         int len = (int)c.getLength();
         if (len > 0) { //jeżeli długość określona wczytanie do przygotowanej tablicy
         
           int actual = 0; //aktualny bajt
           int bytesread = 0 ; //ilość wczytanych bajtów
           byte[] data = new byte[len];
           while ((bytesread != len) &&
           (actual != -1)) {
            actual = is.read(data,
            bytesread, len - bytesread);
            bytesread += actual;
           } //while
         } //if
         else { //jeżeli zawartość niekreślonej długości wpisujemy do tablicy rozszerzalnej
          int ch;
          Vector vec = newVector();
          while ((ch = is.read()) != -1) { vec.addElement(new Integer(ch); }
         }
       }//try
       catch (ClassCastException e) throw new IllegalArgumentException("Not an HTTP URL");
       finally { //zamknięcie strumienia i połączenia
         if (is != null) is.close();
         if (c != null) c.close();
       }
      }
    
  

Bardziej zaawansowany przykład dotyczy wywołania serlwetu po stronie serwera HTTP. Serwlet (specjalna klasa Javy) obsługuje model żądanie - odpowiedź, Kiedy klient wysyła żądanie do serwera ten uruchamia serwlet do obsługi tego żadania. Serwlet tworzy odpowiedź na żądanie klienta, którą serwer odsyła klientowi. Serwlet  w odróżnieniu np. od skryptu CGI działa w ramach tego samego procesu co serwer.

Przykład-5: midlet wywołuje serwlet za pomocą żądania GET. Serwlet przesyła pozdrowienie Hello Midlet i aktualną datę.

Midlet
import java.io.*;
import javax.microedition.io.*;
import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;

public class ServletMidlet extends MIDlet {
  private Display display;
  String url = "http://www.java.sun.com/servlet/HelloServlet";

  public ServletMidlet() {
    display = Display.getDisplay(this);
  }

  public void startApp() {
    try {
      invokeServlet(url); //wywołanie serwletu
    } 
    catch (IOException e) {
      System.out.println("IOException " + e);
      e.printStackTrace();
    }
  }
  
  public void pauseApp() { }

  public void destroyApp(boolean unconditional) { }
  
  void invokeServlet(String url) throws IOException {
    HttpConnection c = null;
    InputStream is = null;
    StringBuffer b = new StringBuffer();
    TextBox tb = null;
    try {
      //uzyskanie połączenia http i skonfigurowanie pól żądania
      c = (HttpConnection)Connector.open(url);
      c.setRequestMethod(HttpConnection.GET);
      c.setRequestProperty("IF-Modified-Since","20 Jan 2001 16:19:14 GMT");
      c.setRequestProperty("User-Agent","Profile/MIDP-1.0 Configuration/CLDC-1.0");
      c.setRequestProperty("Content-Language", "en-CA");
      //uzyskanie strumienia wejściowego i otworzenie połączenia
      is = c.openDataInputStream();

      int ch;

      //odebranie odpowiedzi i wstawienie do utworzonego komponentu TextBox 
      while ((ch = is.read()) != -1) {
        b.append((char) ch);
      }
      tb = new TextBox("Servlet response", b.toString(), 1024, 0);
    } 
    finally {
      if(is!= null) is.close();
      if(c != null) c.close();    
    }
    display.setCurrent(tb);//wyświetlenie na ekranie komponentu z odpowiedzią
  } 
}
Serwlet
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class HelloServlet extends HttpServlet {
  //obsługa żądania GET
  public void doGet(HttpServletRequest request, 
	      HttpServletResponse response)
              throws IOException, ServletException {
    response.setContentType("text/plain"); //ustalenie typu odpowiedzi
    PrintWriter out = response.getWriter();  //obiekt do formatowania odpowiedzi
    out.println("Hello Midlet - today is "); //wypisanie odpowiedzi
    out.println(new Date());
  }
}

 


13.10. PAKIETY OPCJONALNE 

BIBLIOTEKI  MULTIMEDIALNE (MMAPI-Mobile Media API)

MMAPI przeznaczone jest dla urządzeń o zaawansowanych możliwościach  multimedialnych (audio,video) takich jak telefony komórkowe dużej mocy czy cyfrowe sekreterki osobiste. MMAPI jest pakietem opcjonalnym i mieści się w pakiecie javax.microedition.media i w jego podpakietach javax.microedition.media.control i javax.microedition.media.protocol. MMAPI może zostać zaadoptowane  do określonego profilu.

Podzbiorem kompatybilnym z MMAPI przeznaczonym dla urządzeń masowego użytku  o ograniczonych możliwościach multimedialnych jest MIDP2.0 Media API .Spelnia on nastepujące wymagania:

MIDP2.0  Media różni się od pełnego MMAPI dwoma podstawowymi cechami:

Podzbiór ten jest jednak kompatybilny w przód z MMAPI. Uzyskanie pełnej funkcjonalności MMAPI wymaga tylko implementacji dodatkowych klas i metod MIDP2.0 Media API.

Klasy  te są zaimplementowane  w pełnej wersji MMAPI w pakietach javax.microedition.media, javax.microedition.media.control i javax.microedition.media.protocol.

System obsługi multimediów w MMAPI składa się z czterech podstawowych obiektow:

Manager jest sterownikiem najwyższego poziomu. Aplikacja używa tego obiektu do tworzenia obiektów typu Player, do uzyskiwania informacji o własnościach, obsługiwanych typach zawartości i protokołach komunikacyjnych.

Obiekt Player odtwarza zawartośś multimedialną ze źródła danych DataSource. DataSource obsługuje zestaw protokołów komunikacyjnych. Interfejs Control jest stosowany do implementacji sterowników które może posiadać obiekt Player.

Zestaw interfejsów i klas pakietu javax.microedition.media podają poniższe tabele:

Interfejsy pakietu javax.microedition.media
Control implementacja pewnych funkcji przetwarzania mediów.W podpakiecie javax.microedition.media.control znajdują się interfejsy rozszerzające Control a mianowicie: ToneControl, VolumeControl.VideoControl,MIDIControl ...
Controllable deklaruje funkcjonalność do otrzymywania obiektów typu Control z obiektów Player.
Player odtwarza zawartość multimedialną . Rozszerza  Controllable
TimeBase żródło pomiaru czasu z dokładnością do milisekund. Podstawowe narzędzie do synchronizacji odtwarzania.
PlayerListener nasłuch zdarzeń pochodzących od obiektu Player.Definiuje standardowe zdarzenia jako stałe statyczne.
 

Interfejs Controllable zawiera dwie metody:

Metody interfejsu Conrollable
 Control getControl(String controlType)
dostarcza obiekt typu Control gdzie controlType jest nazwą klasy obiektu Control.
 Control[] getControls()
dostarcza tablicę obiektów typu Control

Poniższy przykład podaje sposób wykorzystania funkcjonalności interfejsu Controllable

     
      Controllable controllable;
      //...
      Control controls[]; //tablica obiektów typu Control
      
      controls = controllable.getControls();
      for (int i = 0; i < cs.length; i++) {
      if (controls[i] instanceof
      ControlTypeA)//controlTypeA to np.ToneControl
      doWorkA(); //metoda
      wykonująca zadanie A
      if (controls[i] instanceof
      ControlTypeB)//controlTypeB to np.VolumeControl
      doWorkB(); //metoda
      wykonująca zadanie B
      // etc.
      }

Najbardziej rozbudowanym i najważniejszym interfejsem jest Player. Obiekt tego typu można otrzymać za pomocą jednej z metod statycznych createPlayer() klasy Manager . Obiekt typu Player może znajdować się w jednym z kilku stanów zdefiniowanych jako stałe tego interfejsu:

Stany obiektu Player
staticint CLOSED
 stan zakończenia działania.
static int PREFETCHED
stan wskazujący na  pobranie  zasobów potrzebnych do działania. W tym stanie można wywołać start()  
static int REALIZED
stan uzyskania potrzebnych informacji do pobrania  wymaganych do działania zasobów.
static int STARTED
stan wskazujący o rozpoczęciu odtwarzania - Player przetwarza dane multimedialne. Nie można wywoływać setLoopCount()
static long TIME_UNKNOWN
stan informujący że nie jest znany wymagany czas.
static int UNREALIZED
stan wskazujacy że nie uzyskano informacji potrzebnych do pobrania  wymaganych zasobów. W tym stanie nie wolno wywoływać metod getContentType(),setMediaTime() ,getControl() i getControls()

Celem wprowadzenia tych stanów było zapewnienie programistycznej kontroli nad operacjami zabierającymi dużo czasu. Po utworzeniu obiekt Player jest w stanie UNREALIZED. Przechodząc do stanu REALIZED obiekt ten komunikuje się z serwerem lub systemem plików dla lokalizacji i pobrania potrzebnych zasobów. Typowa sekwencja stanów to UNREALIZED,REALIZED,PREFETCHED i STARTED. Obiekt Player zatrzymuje swoje działanie po osiągnięciu końca danego zasobu multimedialnego lub po wywołaniu metody stop(). Po zatrzymaniu Player wraca do stanu PREFETCHED i jest wtedy gotowy do powtórzenia cyklu.

Przed efektywnym użyciem obiektu Player trzeba ustawic jego parametry a potem aplikować odpowiednie metody powodujące przejście przez w/w stany.

Metody interfejsu Player
 void addPlayerListener(PlayerListener playerListener)
dodaje słuchacza zdarzeń pochodzących od obiektu Player.
 void close()
kończy działanie i zwalnia zasoby.
 void deallocate()
zwalnia  wykorzystywane  zasoby systemu
 String getContentType()
dostarcza typ zawartości odtwarzanego zasobu multimedialnego
 long getDuration()
dostarcza czas trwania odtwarzania.
 long getMediaTime()
dostarcza bieżący czas danego medium
 void prefetch()
pobiera wymagane zasoby i przetwarza tak dużą ilość danych aby zmniejszyć opóźnienie startu.
 int getState()
dostarcza bieżący stan obiektu Player.
 void realize()
powoduje przejście ze stanu UNREALIZED do REALIZED.
 void removePlayerListener(PlayerListener playerListener)
usuwa słuchacza PlayerListener.
 void setLoopCount(int count)
ustala liczbę powtórzeń odtwarzania.
 long setMediaTime(long now)
ustala czas dla danego medium.
 void start()
rozpoczyna odtwarzanie tak szybko jak możliwe
 void stop()
zatrzymuje działanie obiektu  Player.

 

Sposoby osiągania i przejścia pomiędzy powyższymi stanami ilustruje poniższy diagram:

                    Źródło: dokumentacja MIDP2.0

W różnych etapach  życia Player  generowane są różne zdarzenia informujące o aktualnym stanie obiektu. Żeby wykorzystać te zdarzenia należy utworzyć obiekt słuchacza i dodać do obiektu Player za pomocą addPlayerListener(PlayerListener). W interfejsie PlayerListener mamy zdefiniowane określone zdarzenia:

Niektóre stałe interfejsu PlayerListener -niektóre zdarzenia pochodzące od obiektu Player
static String CLOSED
generowane w momencie zamknięcia Player.
static String DEVICE_AVAILABLE
wysyłane gdy system lub inna aplikacja o wyższym priorytecie zwolniła wyłączny zasób i jest on dostępny
static String DEVICE_UNAVAILABLE
wysyłane gdy system lub inna aplikacja o wyższym priorytecie przejęła tymczasowo kontrolę nad wyłącznym zasobem i jest on niedostępny.
static String DURATION_UPDATED
generowane gdy uaktualniono czas  działania Player.
static String END_OF_MEDIA
wysyłane po osiągnięciu końca zasobu multimedialnego.
static String ERROR
wysyłane w momencie wystąpienia błędu. Jeżeli błąd jest krytyczny Player jest w stanie CLOSED.
static String STARTED
wysyłane po wystartowaniu Player.
static String STOPPED
wysyłane po zatrzymaniu Player metodą stop().
static String VOLUME_CHANGED
wysłane po zmianie głośności urządzenia audio.
 
Jedyna metoda interfejsu PlayerListener
 void playerUpdate(Player player, String event, Object eventData)
wywoływana w momencie zajścia zdarzenia event na obiekcie Player. Zdarzeniu temu towarzyszy obiekt danych o zdarzeniu eventData.
 

Podstawową klasą tego  pakietu jest Manager - opisuje ona zarządcę zasobów systemowych potrzebnych do przetwarzania multimediów.

Klasa podstawowa pakietu javax.microedition.media
Manager umożliwia dostęp do zasobów systemowych pozwalających na przetwarzanie multimediów - m.in.  dostarcza obiekty Player.
 
Pole klasy Manager
static String    MIDI_DEVICE_LOCATOR                                                                                                                                                                                                                                          lokalizator dla metody createPlayer tworzącej obiekt Player ze sterownikiem MIDIControl do przetwarzanie plików MIDI ("device://midi")
static String TONE_DEVICE_LOCATOR
 lokalizator ("device://tone")  dla metody createPlayer(String) tworzącej obiekt Player ze sterownikiem ToneControl  do generacji sekwencji tonów.
 
Metody klasy Manager
static Player createPlayer(DataSource source)                                                                                                                                                       tworzy obiekt typu Player do odtwarzania danych ze źródła source
static Player createPlayer(InputStream stream,String type)
tworzy obiekt typu Player do odtwarzania danych ze strumienia stream.
static Player createPlayer(String url)
tworzy obiekt Player do odtwarzania danych z podanego lokalizacji. Postać url to <protokół> : <lokalizacja>
static String[] getSupportedContentsTypes(String protocol)
dostarcza listę obsługiwanych typów zawartości dla podanego protokołu.
static String[] getSupportedProtocols(String content_type)
dostarcza listę obsługiwanych protokołów dla danego typu zawartości. Najbardziej popularne typy zawartości to: audio/x-wav, audio/basic, audio/mpeg, audio/midi, audio/x-tone-seq, video/mpeg
static void playTone(int note, int duration, int volume)
odtwarza jeden ton wyspecyfikowany przez note o czasie trwania i głośności. Ważna szczególnie dla gier i aplikacji audio.

Argumentem pierwszej metody createPlayer() jest obiekt typu DataSource. Klasa DataSource jest klasą abstrakcyjną-poprzez zaimplementowanie jej metod abstrakcyjnych można zdefiniować własną obsługę protokołu.

I ostatnia klasa tego pakietu to klasa wyjątku:

 
Klasa wyjatku pakietu javax.microedition.media
MediaException nieoczekiwany błąd metody przetwarzającej.
 

Przykład-1: pięciokrotne odtwarzanie utworu z pliku pobieranego z sieci

      
      try { 
        Player p =
          Manager.createPlayer("http://webserver/music.wav");
        p.setLoopCount(5);
        p.start();
      }
      catch (IOException ioe) { } 
      catch (MediaException me) { }
    
  

Przykład-2: odtwarzanie pliku medialnego znajdujacego sie w pliku JAR

       
        try {
          InputStream is =
            getClass().getResourceAsStream("music.wav");
          Player p = Manager.createPlayer(is, "audio/X-wav");
          p.start();
        }
        catch (IOException ioe) { }
        catch (MediaException me) { }
      
  
  

Przykład-3: odtworzenie pliku z pamięci rekordów rms

      
      RecordStore rs;
      int recordID;
      
      //utworzenie obiektu rs typu RecordStore...
      
      try {
        InputStream is = new ByteArrayInputStream(rs.getRecord(recordID));
        Player p = Manager.createPlayer(is,"audio/X-wav");
        p.start();
      } 
      catch (IOException ioe) { } 
      catch (MediaException me) { }
   

Przykład-4: generacja pojedynczego tonu

     
      try {
        Manager.playTone(ToneControl.C4, 5000 /* ms */, 100 /* max vol */);
      }
      catch (MediaException e) { }
 

Przykład-5: generacja własnej sekwencji tonów

    
     try {      
      
        Player p = Manager.createPlayer(Manager.TONE_DEVICE_LOCATOR);
        p.realize();
        ToneControl tc =
          (ToneControl)p.getControl("ToneControl");
        tc.setSequence(mySequence);
        p.start();
      }
      catch (IOException ioe) { }
      catch (MediaException me) {}  

Przykład-6: kontrolowane odtwarzanie pliku MIDI

      
      Player p;
      TempoControl tc;
      
      try {
        p = Manager.createPlayer("http://webserver/tune.mid");
        p.realize();
      
        // uzyskanie sterownika TempoControl
        tc =(TempoControl)p.getControl("TempoControl");
        tc.setTempo(120000); // 120 /min
        p.start();      
      } 
      catch (IOException ioe) { } 
      catch (MediaException me) { }
      

Przykład-7: odtwarzanie audio z nasłuchem PlayerListener

      
      //...
      
      static final long SECS_TO_MICROSECS = 1000000L;
      
      Player p;
      VolumeControl vc;
      
      try {
        p = Manager.createPlayer("http://webserver/music.mp3");
        p.realize();
      
        // Set a listener.
        p.addPlayerListener(new Listener());
        
        // uzyskanie sterownika głośności;ustawienie na maksimum       
        vc = (VolumeControl)p.getControl("VolumeControl");
        if (vc != null) vc.setLevel(100);
      
        // ustawienie czasu startu
        p.setMediaTime(5 * SECS_TO_MICROSECS);
      
        // zapewnienie najmniejszego opóźnienia
        p.prefetch();
      
        // nie blokujący start
        p.start();
       
      } 
      catch (IOException ioe) {  } 
      catch (MediaException me) { }
      
      //...
      
      class Listener implements PlayerListener {
      
        public void playerUpdate(Player p, String event, Object eventData) {
      
          if (event == END_OF_MEDIA || event == STOP_AT_TIME) {
      
            System.out.println("Done processing");
       
            try {      
              p.setMediaTime(5 * SECS_TO_MICROSECS);      
              p.start();
            }  
            catch (MediaException me) { }
            break;
          }//if
          
        }//playerUpdate()
        
      }//class Listener
      
  

Przykład-8: odtwarzanie video pliku mpg

      
      Player p;
      VideoControl vc;
      
      try {
        p = Manager.createPlayer("http://webserver/movie.mpg");
        p.realize();
      
        // uzyskanie sterownika video 
        vc =(VideoControl)p.getControl("VideoControl");
       
        if (vc != null) {       
          Form form = new Form("video");      
          form.append((Item)vc.initDisplayMode(vc.USE_GUI_PRIMITIVE, null));      
          Display.getDisplay(midlet).setCurrent(form);
        }
      
        p.start();
      
      } 
      catch (IOException ioe) { } 
      catch (MediaException me) { }
    
  

WMA (Wireless Messaging API) - BIBLIOTEKI DO WYSYŁANIA I ODBIERANIA WIADOMOŚCI (SMS,CBS)

W pakiecie javax.wireless.messaging znajdują się interfejsy określające funkcjonalność potrzebną do wysyłania i odbierania wiadomości binarnych i tekstowych. Biblioteki są ogólne i nie zależą od protokołu komunikacyjnego (GSM SMS, CDMA SMS)

Interfejsy pakietu javax.wireless.messaging
BinaryMessage reprezentuje wiadomość binarną . Rozszerza Message
Message reprezentuje ogólnie wiadomość dowolnego typu.
MessageConnection definiuje podstawową funkcjonalność dla wysyłania i odbierania wiadomości. Rozszerza Connection. Połączenie to może być otwarte w trybie serwera lub w trybie klienta. Tryb serwera uzyskamy przez dostarczenie fabrykatorowi połączeń (klasa Connector) adresu URL z identyfikatorem aplikacji dla wiadomości przychodzących na lokalnym urzadzeniu. Identyfikatorem takim może być numer portu i wówczas wiadomości docierające do niego będą przekazane aplikacji. Tryb serwera pozwala na odbieranie i wysyłanie wiadomości. Tryb klienta uzyskamy podając adres URL wskazujący na inne urządzenie np. numer telefonu komórkowego. W tym trybie możemy tylko wysyłać wiadomości.
MessageListener słuchacz wiadomości przychodzących.
TextMessage reprezentuje wiadomość tekstową. Rozszerza Message

Pakiet ten został zaprojektowany z myślą o pracy z obiektem ogólnego typu Message .Interfejsy rozszerzające BinaryMessage i TextMessage definiują funkcjonalność związaną z określonym typem wiadomości a ten z kolei z określonym protokołem przesyłania. 

Należy pamiętać że wiadomości tego typu  będą docierały do odbiorcy nawet jeżeli nie jest on aktualnie podłączony w momencie wysyłania. Wysyłanie i odbiór wiadomości wiąże się z określonym kosztem finansowym zatem aplikacja powinna wysyłać wiadomości tylko w przypadkach uzasadnionej konieczności.

Interfejs  Message

Deklaruje metody pobierania i ustalania adresu związanego z tą wiadomością oraz określania daty wysłania wiadomości .
Metody interfejsu Message
 String getAddress()
zwraca adres związany z tą wiadomością - jeżeli jest to wiadomość wysyłana dostarcza adres odbiorcy; jeżeli jest to wiadomość odebrana dostarcza adres nadawcy.
 Date getTimeStamp()
zwraca datę wysłania wiadomości.
 void setAddress(String addr)
ustawia adres związany z tą wiadomością 

 
Interfejs  BinaryMessage

Metody tego interfejsu pozwalają na ustalenie i pobranie danych binarnych składających się na tą wiadomość.
Metody interfejsu BinaryMessage
 byte[] getPayloadData()
zwraca tablicę bajtów reprezentującą dane binarne wiadomości.
 void setPayloadData(byte[] data)
pobiera dane binarne wiadomości z tablicy bajtów.

 

Interfejs TextMessage

Metody tego interfejsu pozwalają na ustalenie i pobranie danych binarnych składających się na tą wiadomość.
Metody interfejsu TextMessage
 String getPayloadText()
zwraca dane tekstowe składające się na wiadomość.
 void setPayloadText(String data)
ustala dane tekstowe dla wiadomości

Interfejs  MessageConnection

Definiuje podstawową funkcjonalność dla wysyłania i odbierania wiadomości. Obiekty tego typu są tworzone przez wywołanie metody Connector.open() z adresem URL postaci "sms://<nr telefonu>:<port>". Po wykorzystaniu połączenia powinno być ono zamknięte przez aplikację za pomocą metody close(). Po zamknięciu każde wywołanie innej metody niż close() generuje wyjątek IOException.

Jeżeli aplikacja tworzy kilka obiektów obiekty typu MessageConnection to mogą one wszystkie być w trybie klienta lub w trybie serwera. 

Ten interfejs nie zakłada żadnego specyficznego protokołu przesyłania wiadomości i przeznaczony jest do wszystkich protokołów komunikacyjnych.

Pola interfejsu MessageConnection opisujące typy wiadomości
static String BINARY_MESSAGE
wiadomość typu binarnego (value = "binary").
static String TEXT_MESSAGE
wiadomość typu tekstowego (value = "text").

 

Metody interfejsu  MessageConnection
Message newMessage(String type)
tworzy wiadomość (obiekt  Message ) podanego typu
Message newMessage(String type, String address)
tworzy wiadomość podanego typu i inicjalizuje go podanym adresem.
 int numberOfSegments(Message msg)
zwraca liczbę segmentów protokołu potrzebną do wysłania podanej wiadomości.
Message receive()
zwraca odebraną wiadomość. Jeżeli nie ma wiadomości metoda blokuje się aż nadejdzie wiadomość  lub nastąpi  close()
 void send(Message msg)
wysyła podaną wiadomość. Jeżeli nie może wysłać generuje określony wyjątek
void setMessageListener(MessageListener l)
 ustala słuchacza nadchodzących wiadomości 

Obiekt typu MessageConnection może rejestrować słuchacza typu MessageListener powiadamiającego o przyjściu wiadomości. W ten sposób watek nie musi być blokowany w czasie oczekiwania na przychodzące wiadomości.

Jedyna metoda interfejsu MessageListener
 void notifyIncomingMessage(MessageConnection conn)
wywoływana przez platformę w momencie przyjścia wiadomości          

Aplikacja uzyskuje wiadomość która nadeszła za pomocą metody receive() . Metoda ta nie powinna być wywołana bezpośrednio przez obiekt słuchacza MessageListener. Może być wywołana w innym wątku lub w innej metodzie na zewnątrz słuchacza.

Jeżeli wiadomości przychodzą jedna za drugą w krótkim czasie implementacja może wywoływać słuchacza w wielu watkach równocześnie, ale wówczas aplikacja musi przeprowadzać odpowiednią synchronizację .

Poniższy fragment aplikacji ilustruje sposób użycia słuchacza MessageListener i osobnego wątku do odbierania wiadomości.


// ilustracja zastosowania MessageListener
// po odbiorze wiadomości pobierany jest adres nadawcy
// a następnie odsyłane jest potwierdzenie odebrania 

 import java.io.IOException;
 import javax.microedition.midlet.*;
 import javax.microedition.lcdui.*;
 
 import javax.microedition.io.*;
 import javax.wireless.messaging.*;
 
 public class smsSR 
   extends MIDlet 
   implements CommandListener,MessageListener {
 	
   MessageConnection messageCon;
   Message message;
   boolean done;
   Receiver receiver;
   Display display;
   Command exitCommand;
   TextBox historyBox;
      
   public smsSR(){
     display = Display.getDisplay(this);
     exitCommand  = new Command("Exit", Command.EXIT, 2);
     historyBox = new TextBox("sms history",null, 256, TextField.ANY);
     historyBox.addCommand(exitCommand);
     historyBox.setCommandListener(this);    
	 display.setCurrent(historyBox);    
 		    
   }
   // utworzenie połączenia, rejestracja nasłuchu, uruchomienie wątku odbierającego.
   public void startApp() {
   	 try {
 	   // utorzenie połączenia.
 	   messageCon = (MessageConnection)Connector.open("sms://:50000");
 	   // rejestracja słuchacza nadchodzących wiadomości
 	   messageCon.setMessageListener(this);
 	   // utworzenie i wystartowanie wątku odbierającego wiadomości
 	   done = false;
 	   receiver = new Receiver();
 	   new Thread(receiver).start();
 	 } 
 	 catch (IOException e) {
 	    // obsługa blędów IO
 	 }
   }//startApp()
   
   // wywołanie zwrotne - obsługa wiadomości przychodzących
   public void notifyIncomingMessage(MessageConnection conn) {
 	 if (conn == messageCon) {
  	   receiver.handleMessage();
     }
   }
   // zatrzymanie wątku odbierającego
   // zamknięcie połączenia
   public void pauseApp() {
 	 done = true;
     try {
 	    messageCon.close();
 	 } 
 	 catch (IOException e) {
 	    // obsługa błędów IO
 	 }
   }//pauseApp()
   
   // zatrzymanie wątku odbierającego
   // zamknięcie połączenia
   public void destroyApp(boolean unconditional) {
 	 done = true;
 	 try {
 	    messageCon.setMessageListener(null);
 	    messageCon.close();
 	 } 
 	 catch (IOException e) {
 	    // obsługa błędów IO
 	 }
   }//destroyApp
   public void commandAction(Command c, Displayable s) {
        try {
          if (c == exitCommand ) {
            destroyApp(true);
            notifyDestroyed();            
          }
        } 
        catch (Exception ex) {
          ex.printStackTrace();
        }
    }  
 		        
   // wątek odbierający wiadomości - może być blokowany
   // po odbiorze wiadomości wysyła potwierdzenie  
   class Receiver implements Runnable {
   	
     //liczba wiadomości oczekujących na odebranie    	
 	 private int pendingMessages = 0;
 	
 	 public void run() {
 	   while (!done) {
 		 synchronized(this) {
 		   if (pendingMessages == 0) {
 		     try { wait();	} 
 			 catch (Exception e) {
 			    // obsługa przerwania stanu wait
 			 }
 		   }
 		   pendingMessages--;
 		 }//synchronized()
 
 		//odebranie wiadomości a potem wysłanie potwierdzenia
 		try {
 			
 		    String info ="";    			
 		    
 		    message = messageCon.receive();
 		    String senderAddr = message.getAddress();
 		    
 		    info ="nadawca: "+senderAddr;
 		    historyBox.setString(info);
 		    
 		    String senderText = null;
 		    
 		    if(message instanceof TextMessage){
 		      senderText = ((TextMessage)message).getPayloadText();
 		      info=info+"\n"+senderText;
 		      historyBox.setString(info);
 		      Message reply =
 		        messageCon.newMessage(MessageConnection.TEXT_MESSAGE,senderAddr);
 		      ((TextMessage)reply).setPayloadText("Odebrano: "+senderText);
 		      messageCon.send(reply);
 		      info=info+"\n"+"potwierdzono odebranie: "+senderText;
 		      historyBox.setString(info);
 		      
 		    }  
 		    
 		} 
 		catch (IOException ioe) {
 		    // obsługa błędów IO
 		}
 	  }//while
 	}//run()
 
 	public synchronized void handleMessage() {
      pendingMessages++;
      notify();
 	}
 	
  }//class Receiver
  
}//class smsSR
 

13.11. WIRELESS  TOOLKIT - zbiór narzędzi do uruchamiania i testowania    midletów 

J2ME Wireless Toolkit (J2MEWTK) jest zbiorem narzędzi dostarczających programiście dokumentacji narzędzi, środowiska do emulacji urządzeń komórkowych i przykładów aplikacji ilustrujących zastosowanie dostarczonych bibliotek API.

Cechami obecnie najnowszej wersji J2MEWTK2.0 są:

Przybornik ten jest szczególnie przydatny do testowania programów  przed załadowaniem ich do urządzenia  komórkowego.


13.12. Ćwiczenia i zadania

Zadanie-1 : załadować z sieci pakiet J2MEWTK2.0 i uruchomić w nim programy z wykładu

Zadanie-2 : napisać i przetestować midlet łączący się z aplikacją na komputerze stacjonarnym, pobierający z niego określone dane, zapisujący je w pamieci trwałej  i wyswietlający je na wyswietlaczu telefonu komórkowego.


13.13. Literatura,źródła