Moderne Smartphones oder auch Tablets haben ihre starke Verbreitung nicht nur der permanenten Erreichbarkeit via Mail, SMS, Twitter und Facebook zu verdanken. Ein weiteres starkes Feature ist die Positionsbestimmung ? sei es in Form einer Navigationslösung, einer App für den Sportler oder auch nur als Orientierungshilfe.
Aus diesem Grund ist es wenig überraschend das der Umgang mit dem LocationManager des Android Systems immer wieder in den einschlägigen Foren für Entwickler nach oben rutscht. Wir stellen Ihnen in diesem Artikel in Form einer kompletten Beispiel Anwendung ein Skelett einer App vor. Diese Demo-App ist als solche komplett lauffähig. Der Quellcode lässt sich für eigene Bedürfnisse erweitern oder in Teilen für eine eigene App übernehmen
Servicekraft
In der Android Umgebung ist für die Bestimmung der aktuellen Position der LocationManager [1] zuständig. Dieser stützt sich auf zwei Informationsquellen.
Das GPS Feature ist eine Hardware Komponente die in den meisten Android Geräten verbaut wird. Mit Erscheinen von kostengünstigen Tablets ist diese Hardware aber nicht mehr unbedingt in jedem Device enthalten.
Trotz fehlendem GPS Feature kann Android weiterhin eine aktuelle Position bestimmen. Dazu nimmt es die Positionsbestimmung an Hand der umgebenden WLAN-Infrastruktur vor. Diese Ortung weist üblicherweise eine geringere Genauigkeit als GPS auf. In manchen Gegenden, in denen keine WLAN Infrastruktur in der Nähe vorhanden ist, kann eine Positionsbestimmung ohne GPS somit komplett ausfallen.
Wie genau eine aktuelle Ortung ist, wird durch viele Einflüsse bestimmt. Der „Blick“ des Gerätes auf die GPS-Satelliten oder die Entfernung und die Anzahl der WLAN -Strukturen im Umfeld sind hier die Taktgeber. In welcher zeitlichen Frequenz oder zu welchen Distanzen eine Messung zu erfolgen hat, gibt hingegen der Programmierer der Anwendung vor. Android kann, muss aber nicht, auf diese gewünschten Einstellungen eingehen. Diese etwas „zickige“ Eigenschaft zieht sich übrigens wie ein roter Faden durch Android und muss vom Entwickler berücksichtigt werden.
Beide Positionsbestimmungen (GPS und WLAN) können sich gegenseitig ersetzen. Der Benutzer legt somit unter Android fest, ob eine Ortung via WLAN oder via GPS erfolgt. Da beide Systeme sicherheitshalber entsprechende Rechte benötigen, zeigt unsere Beispiel-App während der Installation einen entsprechenden Hinweis an und der Benutzer muss die Rechte bestätigen.
Der LocationManager
ist die Komponente, die dem Entwickler gegenüber beide Systeme kapselt. Wie jede andere Komponente des Android Systems benötigen GPS und WLAN Strom. Das GPS-System zieht ziemlich kräftig am Akku. WLAN kommt mit weniger Ressourcen aus. Aus diesem Grund ist es erforderlich, die Aktivierung und das Abschalten der Ortung mit Hilfe des LocationManagers sehr dosiert vorzunehmen.
Die kleine Demo Anwendung (Abbildung 1) erlaubt das An- sowie Abschalten des LocationManagers. Dies geschieht durch zwei Buttons
auf der einzig sichtbaren Activity
dieser Anwendung. Die Buttons sind dazu wechselseitig sichtbar.
Ist die Positionsbestimmung aktiviert, dann werden aktuelle Geschwindigkeiten (Speed und Pace) sowie Locations auf der Activity angezeigt.
Drei derzeit statische Werte regeln einige Merkmale der App. Das Abschalten des Bildschirms ist unterbunden solange der LocationManager aktiv ist. Des weiteren wird vom LocationManager spätestens alle fünf Sekunden bzw. sieben Meter eine aktuelle Positionsbestimmung angefordert. Diese drei Werte sind in der Datei MyPreferenceActivity
als statische Variablen hinterlegt. In einer realen Anwendung würde man diese Werte als einstellbare Optionen dem Benutzer anbieten.
In diversen echten Anwendungen wurde mit den verschiedenen Werten für den LocationManager bereits reichlich Erfahrung gesammelt. Die sieben Meter sind in etwa die Genauigkeit die man im Schnitt ohnehin so erwarten kann. Für eine Fußgänger- oder Radfahrer-Anwendung liefern diese Werte akzeptable Ergebnisse. Für eine PKW-Anwendung hingegen wäre das Intervall zu hoch. Hier kann der Entwickler die Messwerte auf 1000 Meter respektive 10 Sekunden erhöhen. Man riskiert mit diesen höheren Werten zwar eine gewisse Unschärfe, die zum Beispiel bei der Ermittlung der zurückgelegten Strecke auftritt, aber da Entfernungsangaben hierzulande ohnehin auf den Kilometer gerundet werden, relativiert sich das in den meisten Fällen.
Es lohnt sich also mit dem Platz in einer lokalen Datenbank, in der die Location Points gespeichert werden, und somit auch mit dem Akku schonend umzugehen.
Eine Demo Anwendung, wie die hier vorgestellte, ist nicht sehr aufwändig (Abbildung 2). Neben den Anwendungstexten (strings.xml
, deutsch sowie englisch), einem Layout (main.xml
) den Standard-Android-Icons aus der Entwicklungsumgebung (ic_launcher.png
, diverse Auflösungen) benötigt man nur noch wenig Java-Programmcode. Diverse Einstellungen für die Zielsysteme liefert die Manifest Datei.
Der sichtbare Teil der Anwendung besteht nur aus einer Activity (Main.java
, main.xml
). Sie gibt bei aktiviertem LocationManager diverse Texte aus. Neben der Geschwindigkeit (Pace in min/km, Speed in km/h) zeigt die Activity die zurückgelegte Entfernung sowie die aktuelle Position (Längengrad, Breitengrad) an.
Stille Post
Jedes Android Projekt benötigt eine Manifest
-Datei. Sie regelt unter anderem die benötigten Ressourcen und erforderlichen Rechte und gibt Infos zur aktuellen Version der App. Die Manifest-Datei der Demo enthält unter anderem diese drei deklarierten Rechte. Sie sorgen dafür, dass der Benutzer während der Installation der App aufgefordert wird, seine Erlaubnis für diese Rechte zu erteilen: ACCESS_COARSE_LOCATION
erklärt die Positionsbestimmung per WLAN während ACCESS_FINE_LOCATION
die Positionsbestimmung durch das GPS Feature regelt. Die Erlaubnis den Bildschirm permanent angeschaltet zu lassen verbirgt sich hinter dem WAKE_LOCK
-Recht.
Listing 1
Erforderliche Rechte
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
Für Geräte ohne GPS-Feature würde eine solche App im Market niemals zum Download angeboten, denn das Recht ACCESS_FINE_LOCATION
setzt implizit GPS voraus und verweigert den Download auf Geräten ohne GPS. Mit dem uses-feature
kann diese implizite Einschränkung ausgehebelt werden. Das uses-feature
teilt dem Play Store mit, dass diese App trotz fehlendem GPS-Feature zum Download angeboten werden soll. Auf Geräten ohne GPS-Feature funktioniert dann halt nur die WLAN-Ortung. Hier würde es sich anbieten, wenn die App den Benutzer zum Start der App darauf aufmerksam macht, dass manche Positionsdaten nicht die Qualität einer GPS-Messung erreichen.
Listing 2
GPS Hardware nicht erforderlich
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
Eine Besonderheit ist das Vorhandensein der Klasse MyApplication
die von der Standardklasse Application
abgeleitet wurde (Listing 3). Dieses Konstrukt garantiert, dass vor dem Aufbau der ersten sichtbaren Activity eine bestimmte Klasse einmalig instanziert wird. Diese Klasse muss dann aber im Manifest hinterlegt werden. In den einschlägigen Foren gibt es immer wieder lange Diskussionen zum Für und Wider dieses Ansatzes. Die Java-Fraktion verweist selbstverständlich und zu recht auf die Nutzung von Singletons. Android-Kenner nehmen ohne Hemmung die Application-Klasse, da diese garantiert genau einmal vorkommt. In langjähriger Praxis hat sich jedenfalls mit diesem Konstrukt eine sehr nützliche Möglichkeit für zentrale Daten eröffnet.
Listing 3
App Start ändern
<application ... android:name="MyApplication" >
Die Klasse MyApplication
kann verschiedenen Objekte eine gewisse Persistenz bieten. Im Beispiel werden hier einmalig vor Anwendungsstart die statischen Optionen eingerichtet. Viele reale Anwendungen nutzen dieses Konstrukt um an zentraler Stelle zum Beispiel eine zentrale Datenbankverbindung zu halten. Wie unsere App die Klasse nutzt, zeigt Listing 4.
Listing 4
Zentrale Application Instanz
public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); MyPreferenceActivity.createSettings(getApplicationContext()); } }
Zum guten Schluss teilt man Android noch den Namen eines service
mit (Listing 5). Services sind eine Besonderheit im Android-Universum [2]. Während Activities/Fragments für die Interaktion mit dem Benutzer ausgelegt sind, werkeln Services im Hintergrund. Es ist nicht vorgesehen, dass der Benutzer unmittelbaren Einfluss auf einen Service hat. Services sind für langlaufende Tätigkeiten gedacht. Dabei laufen sie trotzdem im selben UI-Thread wie die Activities der selben Anwendung. Soll in einem Service ein langlaufender Task durchgeführt werden, so muss der Entwickler auch in einem Service einen Thread dafür einrichten. Der Service der Demo-App benötigt keine zusätzlichen Threads, da er zu keinem Zeitpunkt langwierige Aufgaben durchführt. Eintreffende Aktualisierungen des LocationManagers werden kurz berechnet und sofort an die Activity weitergeleitet.
Ein Service hat den Vorteil, dass er weiterläuft, obwohl der Benutzer alle Activities der App in den Hintergrund verschoben hat. Die Beispielanwendung startet einen Service, sobald der Benutzer auf den Button Start
drückt. Drückt der Benutzer danach auf den Stop
-Button, beendet die App den Dienst. Der Benutzer kann so die Activity verlassen und sich einer anderen App zuwenden – der Service läuft derweil weiter.
Listing 5
Service bekanntgeben
<service android:name="MyService"></service>
Es gibt verschiedene Arten von Services und verschiedene Arten um einen Dienst zu starten. Im Beispiel handelt es sich um einen lokalen Service, der ausschließlich mit seiner eigenen Anwendung kommuniziert. Der Dienst wird von der Activity mit startService
gestartet und mit stopService
wieder beendet (Listing 6).
Listing 6
Service dynamisch starten
public static final String TAG = "de.asltd.androiduser.gps.MyService"; private void processStartService() { Intent intent = new Intent(context, MyService.class); intent.addCategory(MyService.TAG); intent.putExtra("APPNAME", appName); startService(intent); }
Der Dienst durchläuft die Methoden onCreate
und onStartCommand
. Hier wird in den Preferences
der App vermerkt, dass der Service aktiv ist, eine Notification
in der Informationsleiste ausgegeben, der LocationManager gestartet und, wenn gewünscht, das Abschalten des Bildschirms unterbunden. Dem System wird mit dem Flag START_REDELIVER_INTENT
mitgeteilt, dass nach einem Abschuss des Dienstes und automatischem Neustart (was durchaus passieren kann), ein identischer Intent an den neuen Service übergeben werden soll (Listing 7).
Listing 7
Der Service startet
@Override public void onCreate() { context = getApplicationContext(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent != null) { Bundle bundleExtras = intent.getExtras(); if (bundleExtras != null) { appName = bundleExtras.getString("APPNAME"); MyPreferenceActivity.setServiceActive(context, true); processStartNotification(); processLocationListener(); if (MyPreferenceActivity.isLockScreen()) { processStartWakeLock(); } } } return START_REDELIVER_INTENT; }
Beender der Nutzer die zugehörige Activity, dann gibt es so ohne Weiteres keine Möglichkeit den Service wieder zu stoppen. Deshalb zeigt die Beispiel-App in der Informationsleiste ein Icon an. Aktiviert der Benutzer den zugehörigen Eintrag, dann erscheint die Activity wieder im Vordergrund. Dort sieht der Benutzer die aktuellen Daten und kann mit Hilfe des Stop
-Buttons den Dienst beenden (Abbildung 3). Dazu bereitet der Programmierer einen Intent vor, der dann nach der Auswahl des Eintrags in der Informationsleiste gefeuert wird. Listing 8 zeigt den passenden Code.
Listing 8
Einrichten einer Notification
public static final int MYSERVICE_NOTIFICATION = 1; private void processStartNotification() { Intent intent = new Intent(this, Main.class); intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0); Notification notification = new Notification(R.drawable.ic_launcher, appName, System.currentTimeMillis()); notification.setLatestEventInfo(this, appName, getResources().getString(R.string.txt_showactivity), pendingIntent); notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); notificationManager.notify(MYSERVICE_NOTIFICATION, notification); }
Der WakeLock
unterbindet das Abschalten des Displays. Diese einstellbare Sicherheitsmaßnahme soll eigentlich verhindern, dass das Display als größter Stromverbraucher den Akku leert. Bei einer Positionsbestimmung während einer Radfahrt oder einer Autofahrt verfügt der Benutzer üblicherweise über eine externe Stromversorgung in Form eines externen Akkus oder des Zigarettenanzünder. In solchen Fällen soll das Display angeschaltet bleiben.
Listing 9
Abschalten des Displays unterbinden
void processStartWakeLock() { PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); if (powerManager != null) { wakeLock = powerManager.newWakeLock(PowerManager.ON_AFTER_RELEASE | PowerManager.SCREEN_DIM_WAKE_LOCK, MYSERVICE_WAKELOCK); if (wakeLock != null) { wakeLock.acquire(); } } }
Einmal gestartet, liefert der LocationManager dem Dienst in den gewünschten Intervallen neue Positionsangaben. Diese treffen in der Methode onLocationChanged
ein. Da die Beispielanwendung auf GPS- sowie WLAN-Daten zugreifen möchte, werden beide Listener gestartet.
Listing 10
LocationListener anschalten
private void processLocationListener() { locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); locationListener = new LocationListener() { @Override public void onLocationChanged(Location location) { // } }; processStartGpsListener(); processStartNetworkListener(); } void processStartGpsListener() { if (locationManager != null) { locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, MyPreferenceActivity.getTimeInterval() * 1000, MyPreferenceActivity.getDistanceInterval(), locationListener); } } private void processStartNetworkListener() { if (locationManager != null) { locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, locationListener); } }
Wie kommen nun die permanenten Updates des LocationListeners zur Anzeige? Die Updates fallen im Dienst an und es besteht nach dem in diesem Beispiel gewählten Ansatz keine direkte Verbindung zwischen Service und Activity. Auch hier gibt es verschiedene Lösungsansätze, von denen die Beispiel-App einen wählt: Broadcasts. Der Dienst verpackt die eintreffenden Location Updates nach kurzer Kalkulation in einem Intent und schickt diesen mittels Broadcast an die Activity.
Listing 11
Broadcast an Activity vorbereiten
Intent intent = new Intent(); intent.putExtra("LATITUDE", location.getLatitude()); intent.putExtra("LONGITUDE", location.getLongitude()); intent.putExtra("SPEED", location.getSpeed()); intent.putExtra("SUMDISTANCE", sumDistance); intent.putExtra("SUMTIME", sumTime); intent.setAction(Main.MyBroadcastReceiver.TAG); sendBroadcast(intent);
Damit die Activity diese Broadcasts empfangen kann, muss ein BroadcastReceiver
in der Activity eingerichtet werden. In der entsprechenden inneren Klasse der Activity trifft der Broadcast im onReceive
ein und wird dort auf den Schirm gebracht. Solche BroadcastReceiver müssen Sie dynamisch starten und wieder löschen, wie Listing 12 zeigt.
Listing 12
Broadcasts-Empfang
public class MyBroadcastReceiver extends BroadcastReceiver { public static final String TAG = "de.asltd.androiduser.gps.Main.MyBroadcastReceiver"; @Override public void onReceive(Context context, Intent intent) { if (intent != null) { Bundle bundleExtras = intent.getExtras(); if (bundleExtras != null) { // } } } } private void processStartBroadcastReceiver() { IntentFilter intentFilter = new IntentFilter(Main.MyBroadcastReceiver.TAG); broadcastReceiver = new MyBroadcastReceiver(); if (broadcastReceiver != null && intentFilter != null) { registerReceiver(broadcastReceiver, intentFilter); } }
Die Zustellung von Daten mittels Broadcasts ist nicht unbedingt garantiert. Unter heftiger Systemlast kann schon einmal ein Broadcast verloren gehen. In solchen Fällen ist es aber wahrscheinlich das auch andere Systeme betroffen sind. Zudem ist in einer Anwendung, in der fortlaufend eine Positionsbestimmung durchgeführt wird, das Fehlen einer einzigen Position nicht so tragisch.
Wie unter Android üblich unterstützt die Demo App bereits Mehrsprachigkeit (Abbildung 4) und die Erweiterbarkeit durch Stile (Abbildung 5) vorbereitet. Das ist der einzuhaltende Standard und erleichtert bei einem eintretenden Erfolg der App die Erweiterung mit einfachsten Mitteln.
Fazit
Mit dem hier vorgestellten Skelett einer Anwendung lässt sich eine App erstellen, die mit Positionsbestimmungen arbeitet. Der Aufwand ist gering und das erforderliche KnowHow hält sich in Grenzen. Die Beispiel-App zeigt recht schön, wie effektiv und bestens strukturiert sich das Android Ökosystem dem Nutzer präsentiert. Die Standardsprache Java, ein in höchstem Maße optimiertes Klassengerüst gepaart mit manchmal wackeligen Entwicklungswerkzeugen (Eclipse) helfen dem Entwickler, in kürzester Zeit hochwertige Apps zu erstellen. Den Quellcode zu diesem Projekt finden Sie unter [3] auf der Android-User-Homepage zum Download.