Start Magazin Zeit- und Ortserfassung mit Android, Teil 2

Zeit- und Ortserfassung mit Android, Teil 2

Teil 1 unseres Zweiteilers zeigte Ihnen, wie man unter Android Zeitstempel erfasst. Im zweiten Teil kümmern wir uns um die Einstellungen der App und die GPS-Daten.

Wie schon im ersten Teil der Artikelserie in Heft 04/2013 angekündigt [1], erfahren Sie hier, wie Sie die App um einige nützliche Funktionen erweitern. So ist es beispielsweise wichtig, auch dem Anwender Einstellmöglichkeiten in der App zu bieten, da in der Regel jeder Anwender eigene Pfade, Servernamen, Kennworte etc. verwendet. Darüber hinaus lernen Sie, wie Sie mittels GPS und der Google Geocoding API Ihre aktuelle Position ermitteln können. Da aus der Zeiterfassung nun auch eine Ortserfassung werden soll, wurde auch der Quellcode des Projekts in "ZeitOrtErfassung" umbenannt.

Seit Drucklegung des ersten Teils der Artikelserie hat Google den Installationsaufwand für das Einrichten der Entwicklungsumgebung erheblich vereinfacht, da nun das Android SDK mit den Android Developer Tools bereits fertig in Eclipse integriert als "ADT Bundle" unter [4] als Download angeboten wird. So gestaltet sich die Installation kurz zusammengefasst wie folgt:

  • Installation von Java Runtime (JRE) und Java-Development-Kit (JDK) [2]
  • Installation des ADT Bundle [4]
  • Start des SDK Manager im ADT-Bundle-Ordner und Download der gewünschten SDK Platform (z. B. Android 2.2) und der passenden Google APIs
  • Gegebenenfalls Herunterladen des Google USB Drivers im SDK Manager
  • Start von eclipse.exe im Eclipse-Ordner und falls Sie kein eigenes Android-Gerät besitzen, Aufruf von Window | Android Virtual Device Manager, um die für den Emulator die passenden virtuellen Android-Geräte einzurichten

Da Sie die App ja nicht nur für sich persönlich verwenden wollen, müssen Sie dem Endanwender einen Einstellungsdialog für die verschiedenen Parameter anbieten. Da Einstellungsdialoge eigentlich in fast jeder App vorkommen, gibt es dafür die vorgefertigte Klasse PreferenceActivity welche eine XML-Resourcendatei mit den Einstellungen verwendet. Beim Erstellen der XML-Datei können Sie sich vom ADT-Designer wie in Abbildung 1 gezeigt helfen lassen. So brauchen Sie keine eigenen Einstellungsdialog-Formulare zu erstellen. Die PreferenceActivity nimmt Ihnen den größten Teil der Arbeit ab; Sie müssen nur eine Ressourcendatei mit den einzustellenden Parametern erstellen, welche Sie in der onCreate-Methode der PreferenceActivity mittels der Anweisung addPreferencesFromResource(R.xml.prefs); einlesen. Die XML-Datei enthält jeweils die Schlüsselnamen, Default-Werte, Hilfetexte, Beschreibungstexte und Eingabeformate. Aus der Datei prefs.xml (siehe Quellcode) und Listing 1 ergibt sich zur Laufzeit etwa der in Abbildung 2 gezeigte Dialog.

Listing 1

Auszug aus strings.xml

<string name="ServerName">Adresse oder Name des FTP-Servers</string>
<string name="UserName">FTP-Benutzername</string>
<string name="Password">FTP-Kennwort</string>
<string name="ServerDir">FTP-Server Zielverzeichnis</string>
<string name="CSVSeparator">CSV-Trennzeichen</string>
<string name="TimeDirectory">Verzeichnis der Zeitdatei</string>
<string name="TimeFileName">Name der Zeitdatei</string>
<string name="ftpport">FTP-Port</string>
<string name="uselastknownloc">Zuletzt bekannte Position verwenden</string>

Um in der Haupt-Activity ZeiterfassungActivity, deren Layout in main.xml definiert ist Platz für die spätere Azeige der Position zu schaffen, wurden von der ersten App-Version "ZeitErfassung" aus dem vorigen Heft bis auf einen Button alle anderen Buttons entfernt und deren Funktion in ein Optionsmenü ausgelagert. Hierzu wurde die Datei optionsmenu.xml erstellt (Listing 2).

Listing 2

Das Optionsmenü

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
    <item android:id="@+id/item1" android:title="@string/speicherbuttontext"
    android:icon="@drawable/ic_content_save"></item>
    <item android:id="@+id/item2" android:title="@string/bearbeitenbuttontext"
    android:icon="@drawable/ic_action_edit"></item>
    <item android:id="@+id/item3" android:title="@string/hochladebuttontext"
    android:icon="@drawable/ic_av_upload"></item>
    <item android:id="@+id/item4" android:title="@string/loeschbuttontext"
    android:icon="@drawable/ic_action_delete"></item>
    <item android:id="@+id/item5" android:title="@string/einstellungsbuttontext"
    android:icon="@drawable/ic_action_settings"></item>
</menu>

Die folgenden Zeilen initialisieren das Optionsmenü in der ZeiterfassungsActivity:

public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.optionsmenu, menu);
        return super.onCreateOptionsMenu(menu);
}

Der in Listing 3 dargestellte Codeausschnitt sorgt dafür, dass die entsprechenden Programmfunktionen aufgerufen werden.

Listing 3

Auswertung des Optionsmenüs

public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case R.id.item1:
                speichern();
                return true;
        case R.id.item2:
                bearbeiten();
                return true;
        case R.id.item5:
                einstellungen();
                return true;
        default:
                return super.onOptionsItemSelected(item);
        }
}

Um nun den Einstellungsdialog innerhalb der Funktion einstellungen() aufzurufen sind nur wenige Anweisungen erforderlich:

public void einstellungen() {
        meldung="Einstellungen wurde angeklickt";
        System.out.println(meldung);
        Intent intent = new Intent(getApplicationContext(), PrefsActivity.class);
        startActivity(intent);
}

Listing 4

PrefsActivity

package de.cssenior.zeitorterfassung;
import android.os.Bundle;
import android.preference.PreferenceActivity;
public class PrefsActivity extends PreferenceActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        addPreferencesFromResource(R.xml.prefs);
    }
}

Innerhalb der App kann dann zur Laufzeit beispielsweise mittels der Anweisung zeitdateiname = prefs.getString("TimeFileName", "zeit.txt"); auf einen bestimmten Wert der Einstellungen zugegriffen werden, wobei prefs innerhalb der ZeiterfassungAcitvity folgendermaßen definiert ist: public SharedPreferences prefs;.

Abbildung 2: So sieht der Einstellungsdialog zur Laufzeit aus.
Abbildung 2: So sieht der Einstellungsdialog zur Laufzeit aus.

Ermitteln der GPS-Koordinaten

Um überhaupt auf die aktuelle Position zugreifen zu können, müssen zunächst in der Datei AndroidManifest.xml die Berechtigungen android.permission.ACCESS_COARSE_LOCATION und android.permission.ACCESS_FINE_LOCATION angefordert werden. Darüber hinaus muss das Paket android.location importiert werden. Das Android-Betriebssystem hat mit den Location-Services permanent einen Dienst laufen, welcher anderen Programmen/Apps die Möglichkeit bietet über Positionsänderungen des Gerätes informiert zu werden. Damit dies funktioniert benötigt die Activity, welche die Anzeige und Auswertung der GPS-Koordinaten übernehmen soll, zunächst ein Objekt der Klasse LocationManager, welche sie (die ZeiterfassungActivity) innerhalb ihrer onCreate-Methode mittels der Anweisung manager = (LocationManager) getSystemService(LOCATION_SERVICE); zugewiesen bekommt. Nachdem das System nun weiß, dass es Positionsänderungen an die App zu melden hat, benötigt es noch ein Objekt, welches die gesendeten Informationen auch verarbeitet: Hierfür gibt es einen LocationListener, welcher mit den Anweisungen aus Listing 5 definiert und auch gleich mit den entsprechenden Verarbeitungsanweisungen gefüllt wird.

Listing 5

LocationListener

// LocationListener-Objekt erzeugen
listener = new LocationListener() {
        @Override
        public void onLocationChanged(Location location) {
                Log.d(TAG, "onLocationChanged()");
                if (location != null) {
                        breite = location.getLatitude();
                        laenge = location.getLongitude();
                        String s = getString(R.string.breite) + " " + breite + "
" +
                                   getString(R.string.laenge) + " " + laenge;
                        Statuslabel.setText(s);
                        new AsyncCode().execute(location);
                } // if
        }
}; //listener

Nachdem innerhalb der onCreate-Methode nun die beiden Objekte der Klassen LocationManager und LocationListener definiert wurden, müssen sie noch in der onStart-Methode der ZeiterfassungActivity mit der Anweisung manager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 3000, 0, listener); miteinander verbunden werden. Der erste Parameter besagt dabei, dass das GPS-System als Lieferant für die Positionsänderungs-Informationen herangezogen werden soll, während der zweite Parameter angibt, in welchen Millisekunden-Abständen die Abfrage erfolgen soll. Je kürzer die Zeitabstände sind, desto höher ist dann allerdings der Energieverbrauch. Der dritte Parameter gibt an wie groß die Positionsänderung in Metern sein soll, damit sie als solche auch erkannt wird, während der vierte Parameter angibt, an welches Objekt der LocationListener-Klasse die Positionsänderung gemeldet werden soll. Da eine Anzeige von Positionsänderungen nur benötigt wird, wenn die entsprechende App sichtbar im Vordergrund ist, wird in der onPause-Methode, also wenn die Activity in den Hintergrund gelegt oder gar beendet wird, mittels manager.removeUpdates(listener); die Verbindung der beiden Objekte wieder gelöst, damit nicht unnötigerweise Strom verbraucht wird. Abbildung 3 zeigt die beteiligten Klassen und Objekte.


Das Ergebnis

Die fertige App ZeitOrtErfassung gibt es für 79 Cent im Google PlayStore [7] zum Download.

Ermitteln der zur GPS-Position passenden Adresse

Google bietet mit der Geocoding API die Möglichkeit aus einer gegebenen GPS-Position die an dieser Position befindliche Adresse ausfindig zu machen. Das gelingt zwar nicht immer, was im übrigen nicht verwunderlich ist, da ja nicht jeder Punkt mitten im Wald eine Anschrift mit Hausnummer hat, aber es ist in normal besiedelten Gebieten in der Regel problemlos möglich zumindest näherungsweise den Straßennamen oder gar die Hausnummer zu ermitteln. So kann es natürlich auch vorkommen, dass die Straßenangabe die Parallelstraße nennt, oder zeitweise gar keine Adresse ermittelt werden kann, etwa weil der entsprechende Google-Dienst nicht verfügbar ist. Daher ist das entsprechende Textfeld in der Beispiel-App so konzipiert, dass manuelle Eingaben oder Korrekturen an dieser Stelle möglich sind. Da die Ermittlung je nach Server- und Netzauslastung auch länger dauern kann, findet sie in einem separaten Thread statt. Eine große Hilfe hierbei ist der Einsatz der Android Hilfsklasse "AsyncTask" welche den recht komplexen Vorgang vereinfacht. Jedes Mal, wenn der LocationListener mittels onLocationChanged-Callback eine Positionsänderung meldet, wird mittels new AsyncCode().execute(location); der in Listing 7 gezeigte Prozess der asynchronen Geokodierung angestoßen. Da der Geocoder grundsätzlich eine Liste möglicher Adressen um die angegebene GPS-Position herum zurückliefern kann, wird dem Geocoder für seine Methode getFromLocation auch eine Liste angeboten; allerdings wird hier der Einfachheit halber grundsätzlich nur der erste Eintrag angefordert und ausgewertet, was über den letzten Parameter der Methode getFromLocation gesteuert wird.

Listing 7

Geocoding der ermittelten GPS-Position

// Asynchrone Geocodierung
class AsyncCode extends AsyncTask<Location, Integer, String> {
        @Override
        protected String doInBackground(Location... params) {
                // TODO Auto-generated method stub
                Geocoder gc = new Geocoder(ZeiterfassungActivity.this);
                List<Address> addressList=null;
                try {
                  addressList = gc.getFromLocation(params[0].getLatitude(), params[0].getLongitude(), 1);
                  Address address = addressList.get(0);
                  String strasse = address.getThoroughfare();
                  String hausnr = address.getSubThoroughfare();
                  String PLZ = address.getPostalCode();
                  String ort = address.getLocality();
                  String t = strasse + " " + hausnr + ", " + PLZ + " " + ort;
                  System.out.println("Addresse: " + t);
                  return t;
                } catch (IOException e) {
                  Log.e(TAG,"Geocoder fehlgeschlagen");
                  return e.getMessage();
                }  // try
        }
        @Override
        protected void onPostExecute(String result) {
                // TODO Auto-generated method stub
                super.onPostExecute(result);
                position.setText(result);
        }
        @Override
        protected void onProgressUpdate(Integer... values) {
                // TODO Auto-generated method stub
                super.onProgressUpdate(values);
                // wird in diesem Fall nicht benötigt
        }
} // Class AsyncCode

Fazit und Ausblick

Wie Sie sehen, ist es mit relativ wenig Aufwand möglich, eine eigene App für Android-Smartphones zu schreiben. Eine gute Programm-Idee vorausgesetzt steht nach ausgiebigen Tests Ihrer Eigenentwicklungen einer Vermarktung beispielsweise im Google Play Store nun nicht mehr viel entgegen. Sie müssen Ihre App nur noch als Signed Application Package mittels der Android Tools exportieren und verteilen.

Kommentiere den Artikel

Please enter your comment!
Please enter your name here