sobota, 4 października 2014

Pojedynek z Androidem - odc. 11 lokalizacja geograficzna

Przyszła pora na lokalizację. Przebrnięcie przez ileś filmów związanych z tą tematyką wydawało się nużące… Jednak po zapoznaniu się z całością udało mi się zebrać interesujące informacje oraz uzyskać dość jasny obraz całości, który teraz przedstawię.

Różne providery lokalizacji (sieć, GPS) - rzecz znana również w rodzinie Windows, aczkolwiek zrealizowana odrobinę inaczej. W aplikacjach Windows Store nie mamy aż takiej infrastruktury do wyszukiwania providerów spełniających określone wymagania czy predefiniowanego dostępu do formatu NMEA z GPS. W Android aplikacja może w prosty sposób otworzyć okno z ustawieniami sieci Wi-Fi czy trybu lotniczego. W ekosystemie Windows nie spotkałem się z czymś takim jak pasywny provider. Jeśli ktoś zapyta w systemie o lokalizację (np. jakaś inna aplikacja) to my dowiemy się o tym w naszej aplikacji za pomocą tego właśnie providera.

LocationManager - nie rozumiem, po co ktoś wymyślił metodę pobierającą ostatnio ustaloną wartość, która może być przed kilku miesięcy lub być null. Oprócz tego możemy też pobrać aktualną wartość lub zapiąć się na zmiany lokalizacji, co jest również możliwe w Windows/Windows Phone.

Monitorowanie zmian lokalizacji a wątki - tutaj moim zdaniem pewne rzeczy z architektury Androida utrudniają realizację tego zadania, jak choćby niszczenie i odtwarzanie aktywności przy obracaniu ekranu, loopery i handlery może nie są i najgorsze, ale obsługa ich w porównaniu z Windows jest bardziej skomplikowana. Przy okazji Activity.runOnUiThread jednoznacznie kojarzy mi się z wywoływaniem kodu w dispatcherze w aplikacjach XAML. Odbieranie aktualizacji w Androidzie jest możliwe za pomocą callbacków lub pending intent. Callbacki mają jakieś przełożenie na API w Windows.  Pending intent w przypadku lokalizacji w Android wydaje się dobrym sposobem na wygodne nasłuchiwanie zmian, ale z uwagi na większy od callbacka narzut nie jest polecany do scenariuszy z częstą aktualizacją położenia.

Proximity alerts - jak dla mnie odpowiednik geo-fencingu w Windows 8.1/Windows Phone 8.1, też wykrywamy wejścia/wyjścia z jakiegoś obszaru wokół danej lokalizacji geograficznej

Geocoding - mapowanie współrzędnych geograficznych lub nazwy obiektu na adres możemy również zrobić na platformie Windows/Windows Phone. Temat poruszany jest zwykle przy omawianiu map.  Dostajemy usługę Bing. W przypadku Androida mamy doczynienia z analogicznym rozwiązaniem, jest API korzystające z serwisów Google.  Na platformie Windows z uwagi na prowadzoną politykę nie ma możliwości, by geolokalizacja na jakichś urządzeniach nie działała, w przypadku Androida urządzenia z systemem w wersji Open Source mogą tego nie obsługiwać.

 

Podstawy

LocationManager

  • Obsługuje wszystkie funkcjonalności związane z lokalizacją
    • zwrócenie bieżącej lokalizacji
    • monitorowanie zmian w statusie
    • tworzenie notyfikacji po osiągnięciu lokalizacji
  • Dostęp
    • Context.getSystemService
    • nazwa serwisu:  Context.LOCATION_SERVICE

LocationManager lm = (LocationManager)  getSystemService(LOCATION_SERVICE);

Większość urządzeń z Android wspiera 2 providery lokalizacji

  • GPS  (LocationManager.GPS_PROVIDER)
  • bazujący na sieci (LocationManager.NETWORK_PROVIDER  - kombinacja znanych hotspotów Wi-Fi i baz komórkowych)

Uprawnienia

  • różne w zależności od wymaganej dokładności
  • ACCESS_COURSE_LOCATION - sieć, mała dokładność
  • ACCESS_FINE_LOCATION - GPS, wysoka precyzja
  • z nadania uprawnień do wysokiej precyzji nie wynikają uprawnienia do niskiej

Odbieranie informacji o zmianach w lokalizacji i providerze

  • implementacja interfejsu LocationListener
  • onLocationChanged - instancja klasy Location, bieżąca lokalizacja
  • onProviderEnabled/onProviderDisabled - włączenie/wyłączenie powiązanego providera
  • onStatusChanged -istotna zmiana w statusie providera

Klasa Location

  • surowe informacje o lokalizacji (brak np. adresu z ulicą)
  • kluczowe metody: 
    • getLatitude / getLongitude
    • getAccuracy (w metrach)
  • czas: 
    • getTime (czas w UTC w danej lokalizacji, mogą być różnice między różnymi providerami)
    • getElapsedRealtimeNanos (czas lokalizacji w ns od bootowania urządzenia, spójny dla wszystkich providerów, od Android 4.2, API 17)
  • inne informacji w zależności od providera:
    • getSpeed, getBearing, getAltitude
    • metody hasXXX do sprawdzenia, czy zawiera dane informacje

public class MyLocationListener implements LocationListener  {

        …

        public void onLocationChanged(Location location)  {

                 String provider = location.getProvider();

                 double lat = location.getLatitude();

                 double lng = location.getLongitude();

                 float accuracy = location.getAccuracy();

                 long time = location.getTime();

 

                 SimpleDateFormat formatter = new SimpleDateFormat(“yyyy-MM-dd ’T’ HH:mm:ss”);

                 formatter.setTimeZone(TimeZone.getTimeZone(“UTC”));

                 String timeStamp = formatter.format(time);

                 String message = String.format(“%s   |  lat/lng=%f/%f  |  accuracy=%f | time=%s”,  provider, lat, lng, accuracy, timeStamp);

        }

 

        public void onStatusChanged(String s, int i, Bundle bundle)  {

        }

 

         public void onProviderEnabled(String s)  {   //nazwa providera

         }

 

         public void onProviderDisabled(String s)  {

         }

}

Odbieranie informacji o zmianach w lokalizacji

  • metoda requestLocationUpdates w serwisie lokalizacji, do której przekazujemy
    • nazwę providera
    • minimalny czas w ms do notyfikacji (0 - najczęściej jak się da)
    • minimalna zmiana położenia w m do notyfikacji (0 - przy każdym ruchu)
    • referencję do implementacji LocationListener
  • aktualizacje przychodzą dopóki nie wywołamy metody removeUpdates (przekazujemy do niej implementację LocationListener przekazaną do requestLocationUpdates)

LocationManager lm = (LocationManager)  getSystemService(LOCATION_SERVICE);

LocationListener listener = new MyLocationListener();

lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, listener);

Pobieranie pojedynczej wartości

  • szybkie
    • LocationManager.getLastKnownLocation
    • mogą być sprzed iluś minut, godzin, dni
    • może się zdarzyć wartość null (rzadko)
  • pojedyncza, bieżąca wartość
    • LocationManager.requestSingleUpdate
    • callback jak w requestLocationUpdates

LocationManager lm = (LocationManager) getSystemService(LOCATION_SERVICE);

Location recentLocation = lm.getLastKnownLocation(LocationManager.GPS_PROVIDER);

 

LocationListener myListener = new MyListenerClass();

lm.requestSingleUpdate(LocationManager.GPS_PROVIDER, myListener, null); 

//null – odpalenie w wątku UI

 

Location Providers

2 główne providery:

  • LocationManager.GPS_PROVIDER
  • LocationManager.NETWORK_PROVIDER

Providery mogą opisywać swoje charakterystyki

  • klasa LocationProvider
    • do pobrania informacji o providerze używamy LocationManager.getProvider
  • identyfikacja wymaganego poziomu zasilania
    • getPowerRequirements
  • horyzontalna dokładność
    • getAccuracy
    • zwraca Criteria.ACCURACY_COARSE/ACCURACY_FINE
  • rzeczy potrzebne do ustalenia położenia:
    • requiresSatellite,  requiresNetwork,  requiresCell
  • identyfikacja pozyskiwanych informacji
    • supportsAltitude, supportsBearing, supportsSpeed

LocationManager lm = (LocationManager) getSystemService(LOCATION_SERVICE);

LocationProvider gpsProvider = lm.getProvider(LocationManager.GPS_PROVIDER);

Specyfikacja pożądanego zachowania za pomocą klasy Criteria

  • wymagane informacje
    • setAltitudeRequired, setBearingRequired, setSpeedRequired
  • wymagania co do dokładności szerokości i długości geograficznej (Fine / Course)
    • setAccuracy
  • inne wymagania co do dokładności (High/Medium/Low)
    • setSpeedAccuracy, setVerticalAccuracy
  • akceptowalne zużycie zasobów
    • setPowerRequirement, setCostAllowed

Dopasowanie zachowania do providerów za pomocą klasy Criteria

  • sprawdzenie, czy provider spełnia kryteria
    • LocationProvider.meetsCriteria
  • wybranie providerów, które spełniają kryteria
    • LocationManager.getProviders
    • wszystkie pasujące lub tylko obecnie włączone
    • lista ograniczona jest uprawnieniami aplikacji
  • pobranie uaktualnień do lokalizacji przy użyciu providera spełniającego dane kryteria
    • LocationManager.requestLocationUpdates/requestSingleUpdate

Criteria criteria = new Criteria();

criteria.setAccuracy(Criteria.ACCURACY_FINE);

criteria.setSpeedRequired(true);

criteria.setAltitudeRequired(true);

LocationManager lm = (LocationManager) getSystemService(LOCATION_SERVICE);

List<String> matchingProviderNames = lm.getProviders(criteria, false);

for(String providerName:matchingProviderNames)  {

         LocationProvider provider = lm.getProvider(providerName);

         boolean requiresCell = provider.requiresCell();

         …

}

Sprawdzanie dostępności providera

  • LocationListener dostarcza tylko część informacji
    • onEnabled/onDisabled informują jedynie kiedy użytkownik włączył/wyłączył
  • Przed użyciem zawsze sprawdzajmy dostępność
    • LocationManager.isProviderEnabled

Provider sieciowy jest uzależniony od

  • włączenia Wi-Fi (możliwość sprawdzenia aktywności za pomocą ConnectivityManager)
  • włączenia trybu lotniczego (ustalenie stanu za pomocą klasy Settings.Global z AIRPLANE_MODE_ON)

Wykrywanie zmian w providerze sieciowym

BroadcastReceiver

  • w onReceive sprawdzamy Wi-Fi i tryb lotniczy
  • filtr intencji
    • Wi-Fi:  ConnectivityManager.CONNECTIVITY_ACTION
    • Tryb lotniczy:  Intent.ACTION_AIRPLANE_MODE_CHANGED

 

boolean isAvailable = lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER);

 

boolean isOff = Settings.System.getInt(getContentResolver(), Settings.System.AIRPLANE_MODE_ON, 0) == 0;

 

ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);

NetworkInfo wifiInfo = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI);

boolean isAvailable = wifiInfo.isAvailable();

Udostępnianie potrzebnych funkcjonalności

  • Android nie pozwala aplikacjom zmieniać większości funkcjonalności
  • Aplikacja powinna automatycznie wyświetlać ekran z właściwościami
    • startActivity z odpowiednią intencją dla ekranu właściwości
    • Settings.ACTION_LOCATION_SOURCE_SETTINGS (providerzy lokalizacji)
    • Settings.ACTION_WIFI_SETTINGS
    • Settings.ACTION_AIRPLANE_MODE_SETTINGS

Provider GPS dostarcza więcej informacji niż tylko bieżące położenie

zdania NMEA (typowe dla urządzeń GPS)

  • implementacja interfejsu GpsStatus.NmeaListener
  • rozpoczęcie słuchania dzięki LocationManager.addNmeaListener

Pasywny provider lokalizacji

Inicjuje niejawnie monitorowanie lokalizacji

  • LocationManager.PASSIVE_PROVIDER
  • otrzymamy informację o lokalizacji, gdy inne źródło zażąda tego
  • prawdziwy provider możemy zidentyfikować za pomocą metody Location.getProvider
  • jak inna aplikacja np. pogoda pobierze lokalizację, u nas zostanie to zalogowane

 

Zarządzanie lokalizacją i wątkami

Domyślnie całe przetwarzanie odbywa się w głównym wątku aplikacji.

Jeśli chcemy wykonywać bardziej czasochłonne operacje jak np. zapis na dysk, do bazy danych lub komunikacja z siecią warto używać looper-a (z HandlerThread) obsługującego komunikaty w innym wątku niż UI. Przekazujemy instancję looper-a do LocationManager.requestLocationUpdates

HnadlerThread thread = new HandlerThread(“locthread”);

thread.start();

Looper looper = thread.getLooper();

locationManager.requestLocationUpdates(locationManager.NETWORK_PROVIDER, 0, 0, listener, looper);

locationManager.removeUpdates(listener);

looper.quit();

Aktualizacja UI z wątku lokalizacji jest wyzwaniem

  • Trzeba użyć międzywątkowego bezpiecznego mechanizmu
    • najłatwiej Activity.runOnUiThread
  • Trudności w synchronizacji UI z listenerem lokalizacji
    • aktywności się niszczone i tworzone od nowa przy obracaniu urządzenia
  • Od API 13 fragmenty mogą być persystowane między aktywnościami (setRetainInstance(true) w onCreate)
  • Przed API 13 trzeba jawnie zarządzać lokalizacją i obiektami wątków
    • zapisanie informacji pomiędzy aktywnościami przez nadpisanie onRetainNonConfigurationInstance
    • pobranie informacji między aktywnościami przez wywołanie getLastNonConfigurationInstance

final Location theLocation = location;

getActivity().runOnUiThread(new Runnable() {

        public void run() {

               setLocation(theLocation);

        }

});

 

public class TrackingActivity extends Activity {

        …

        public void onCreate(Bundle savedInstanceState) {

                …

                LocationState state = getLastNonConfigurationInstance();

                if (state != null)  {

                        _looper = state.getLooper();

                        _locationListener = state.getLocationListener();

                        if (_locationListener != null)

                               _locationListener.setActivity(this);

                }

        }

        …

        @Override

        public Object onRetainNonConfigurationInstance() {

                 return new LocationState(_looper, _locationListener);

        }

}

Trackowanie lokalizacji w tle

  • Serwis umożliwia trackowanie bez aktywności
  • Nadal trzeba używać loopera, by nie blokować głównego wątka aplikacji
  • Nie należy używać IntentService, bo może zostać zamknięty automatycznie

 

Kontrola częstotliwości aktualizacji lokalizacji

Ograniczenie częstotliwości aktualizacji

  • LocationManager - dwa sposoby:
    • czas (np. co 5 min)
    • odległość (np. co 1 km)
  • ustawienia limitów zachowują się inaczej przed i po API 16 (Android 4.1)
    • przed API 16 limity są tylko wskazówką dla providera (może się częściej aktualizować)
    • od API 16 provider musi uwzględniać limity

Ograniczenia czasowe

  • w ms
    • 0 - tak często, jak tylko możliwe
    • <=1000 - w praktyce to samo jak 0
  • provider może wyłączyć się pomiędzy aktualizacjami
    • im większe odstępy czasu, tym bardziej jest to prawdopodobne
    • znacząca oszczędność energii
  • dla dużych wartości odstępy pomiędzy aktualizacjami mogą być większe niż żądane
    • providerzy nie zaczynają szukać lokalizacji zanim nie minie okres przerwy
    • dla providera GPS różnica może być znacząca (10 s lub więcej)

Ograniczenia odległości

  • w metrach
    • 0 - tak często, jak tylko możliwe
    • minimalna znacząca wartość różni się pomiędzy providerami
  • provider rzadko się wyłącza pomiędzy aktualizacjami (musi być aktywny by ustalić dystans, o jaki się przemieściliśmy)
  • używany głównie do ograniczenia przetwarzania przez program

Otrzymywanie aktualizacji

Dwa sposoby

  • callback
    • większość kontroli po stronie aplikacji
    • mniejszy narzut systemowy (dobry wybór dla częstych aktualizacji)
  • pending intents
    • prostszy do oprogramowania
    • większy narzut systemowy (dopasowywanie Intent Filter) (unikać przy częstych aktualizacjach)
  • te same rodzaje limitów
  • te same informacje

Znaczne uproszczenie scenariuszy w lokalizacji

  • background tracking
    • system wyśle do serwisu bez rozwiązywania problemów z czasem życia
    • tworzymy prosty serwis dziedziczący po IntentService
  • wiele odbiorców
    • możliwość powiązania broadcastu z pending intent
    • odbiorcy tworzą broadcast receiver’a z odpowiednim filtrem intencji

Implementacja aktualizacji lokalizacji z Pending Intents

  • zdefiniowanie własnej akcji
  • implementacja serwisu/broadcast receivera z filtrem intencji dla tej akcji
  • stworzenie intencji dla tej akcji
  • stworzenie PendingIntent opakowujący Intent
  • wywoływanie LocationManager.requestLocationUpdates z obiektami Pending Intent
  • kończymy wywołując LocationManager.removeUpdates

Intent intent = new Intent(“com.xxx.ACTION_LOCATION_UPDATE”);

PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent, 0);

LocationManager lm = (LocationManager) getSystemService(LOCATION_SERVICE);

lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, pendingIntent);

Dostęp do informacji o lokalizacji wysyłanej dla Pending Intent

Cel Pending Intent otrzymuje Intent z lokalizacją

  • otrzymany Intent zawiera wszystkie informacje w oryginalnym Intent
  • system lokalizacji dodaje specyficzne extras
    • zmiany w lokalizacji:  LocationManager.KEY_LOCATION_CHANGED (zwracana wartość jest instancją klasy Location)
    • włączenie/wyłączenie providera:  LocationManager.KEY_PROVIDER_ENABLED (zwracana wartość 1=enabled, 0=disabled)

public class XActivity extends Activity {

        …

        private PendingIntent _locationChangeServicePendingIntent;

        private PendingIntent _locationChangeBroadcastPendingIntent;

        @Override

        public void onCreate(Bundle savedInstanceState)  {

                …

                Intent intent = new Intent(“com.xxx.ACTION_LOCATION_CHANGED”);

                _locationChangeServicePendingIntent = PendingIntent.getService(this, 0, intent, 0);

                _locationChangeBroadcastPendingIntent =  PendingIntent.getBroadcast(this, 0, intent, 0);

        }

        public void onMenuStartClick(MenuItem item)  {

                LocationManager lm = (LocationManager)  getSystemService(LOCATION_SERVICE);

                lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, _locationChangeServicePendingIntent);

                /* _locationChangeBroadcastPendingIntent */

        }

        public void onMenuStopClick(MenuItem item)  {

                LocationManager lm = (LocationManager)  getSystemService(LOCATION_SERVICE);

                 lm.removeUpdates(_locationChangeServicePendingIntent);

                 /* _locationChangeBroadcastPendingIntent */

        }

}

public class LocationUpdateService extends IntentService  {

        public LocationUpdateService()  {

                super(“LocationUpdateService”);

        }

        @Override

        protected void onHandleIntent (Intent intent)  {

                String action = intent.getAction();

                if (action.equals(“com.xxx.ACTION_LOCATION_CHANGED”))  {

                        Bundle extras = intent.getExtras();

                        Location location = extras.get(LocationManager.KEY_LOCATION_CHANGED);

                        …

                }

        }

}

public class LocationUpdateReceiver1 extends BroadcastReceiver {

         public void onReceive(Context context, Intent intent)  {

                 String action = intent.getAction();

                 …

         }

}

<manifest …>

          <application …>

                     <service android:name=”.LocationUpdateService”>

                               <intent-filter>

                                        <action android:name=”com.xxx.ACTION_LOCATION_CHANGED” />

                               </intent-filter>

                      </service>

                       <service android:name=”.LocationUpdateReceiver1”>

                               <intent-filter>

                                        <action android:name=”com.xxx.ACTION_LOCATION_CHANGED” />

                               </intent-filter>

                      </service>

         </application>

</manifest>

Kiedy unikać lokalizacji z Pending Intent?

  • duża częstotliwość aktualizacji
  • aktywność jako cel dla Pending Intent
    • ciągłe wywoływanie onCreate w aktywności
    • aktywność może stać się aktywnością foreground jeśli była w tle
    • można używać, gdy aktywność ma ustawiony tryb na singleInstance
      • trzeba wywołać removeUpdate jeśli użytkownik przełączy się na inną aktywność

Proximity alerts

System może wysłać alert jeśli znajdziemy się blisko określonej lokalizacji

  • LocationManager.addProximityAlert (może być kilka równocześnie aktywnych)
  • Wewnętrznie używane są zarówno sieć jak i GPS
  • Potrzeba dużej ilości energii
  • Wymagane uprawnienia
    • API 16 (Android 4.1.1)  lub wcześniej:  ACCESS_COURSE_LOCATION (wystarczy)
    • API 17 (Android 4.2) lub wyżej:  ACCESS_FINE_LOCATION

Czego potrzebujemy do proximity alert ?

  • szerokość i długość geograficzną lokalizacji
  • proximity = odległość od lokalizacji w m
  • jak długo monitorować wejście do proximity (w ms)
    • -1: aż do wywołania removeProximityAlert
    • 0: od razu
  • odpalenia Pending Intent, kiedy urządzenie wchodzi do proximity
    • logiczny extra o nazwie LocationManager.KEY_PROXIMITY_ENTERING
      • true - wejście
      • false - wyjście

LocationManager lm = (LocationManager) getSystemService(LOCATION_SERVICE);

long expiration = -1;

lm.addProximityAlert(latitude, longitude, radius, expiration, pendingIntent);

 

Informacje o lokalizacji czytelne dla ludzi

Klasa Address

  • części adresu
    • getThouroughfare: zwykle nazwa ulicy
    • getLocality: zwykle nazwa miasta
    • getAdminArea: zwykle nazwa stanu lub prowinicji
  • adres gotowy do wyświetlenia
    • getMaxAddressLineIndex
    • getAddressLine(index)
  • współrzędne
    • getLatitiude/getLongitude

Klasa Geocoder

  • pełny opis przyjazny dla komputera i dla człowieka
  • getFromLocation
    • jeden lub więcej instancji Address dla współrzędnych szerokości i długości geograficznej
  • getFromLocationName
    • jeden lub więcej instancji Address dla nazwanej lokalizacji
  • obie metody zwracają List<Address>
    • w wywołaniach metod specyfikujemy maksymalną liczbę instancji Address
    • lista zawierać będzie od 0 do max żądanych wartości
    • pierwszy zwracany wynik jest zwykle najbardziej pasujący
  • klasa zależy od własnych bibliotek i usług Google
    • jest dostępna na większości urządzeń kupionych w sklepie
    • może nie być dostępna na urządzeniach z Androidem w wersji Open Source
    • sprawdzenie czy wymagane biblioteki Google są na urządzeniu - statyczna metoda isPresent (API 9/Android 2.3)
  • klasa wykorzystuje web serwis
    • getFromLocation i getFromLocationName wywołują zdalne serwisy
  • IOException
    • biblioteki nie są dostępne
    • błąd w komunikacji z serwerem
  • wartość zwracana pusta lub null
    • błąd w komunikacji z serwerem
    • żaden adres nie pasuje do zapytania
  • metod getFromLocation/getFromLocationName nie należy wywoływać w wątku UI
    • obie oparte są na blokujących wywołaniach sieciowych
    • należy zastosować dowolne rozwiązania pozwalające uniknąć blokowania wywołania w wątku UI (najprostsze: AsyncTask)

Geocoder geocoder = new Geocoder(context);  //w activity: this

List<Address> addrList1 = geocoder.getFromLocation(x,y,5);

List<Address> addrList2 = geocoder.getFromLocationName(place, 5);

Java

  • metoda(String… strings)

Brak komentarzy: