ubiquitous-computing-geo-ti.../documentation/parts/android.tex

242 lines
20 KiB
TeX

\chapter{Android-App}
\section{Technologiebeschreibung}
\subsection{Android SDK}
Die Android-Entwicklung wurde, aufgrund der Ausgereiftheit und den Emulatoren, mit Android-Studio realisiert. Android-Studio verwaltet auch das SDK und unterstützt beim aktuell halten der Bibliotheken. \\
Das minimale API-Level, welches das Endgerät haben darf, wurde auf 23 "Marshmallow" festgelegt. Dadurch werden ca. 85\% der Geräte unterstützt und ist aktuell genug um gewisse Features, wie das neue Berechtigungssystem, zu unterstützen. Die Zielversion ist das aktuelle Android 10 mit API-Level 29. In dieser Version wurden erneut Berechtigungen geändert, wodurch im Code einige Anpassungen gemacht werden mussten (siehe: \ref{sec:Probleme})
\subsection{Kotlin}
Die Entscheidung fiel auf Kotlin als Programmiersprache, da die Sprache von Google für die Entwicklung von Android-Apps bevorzugt wird. Außerdem bietet dies die Gelegenheit eine neue Programmiersprache zu erlernen. Dadurch jedoch musste viel Zeit investiert werden um zum Einen die Sprache und zum Anderen die Entwicklungsumgebung, sowie der Aufbau einer Android-App zu lernen. Dafür wurden zwei von insgesamt fünf Sprints eingeplant, weswegen die App nur die Grundfunktionen besitzt.\\
Die Kotlin-Version bei Fertigstellung ist die aktuellste Version (1.3.72).
\subsection{Retrofit}
Für die Kommunikation mit dem Backend wurde die Bibliothek Retrofit in der Version 2.8.1 verwendet. Retrofit ist ein HTTP-Client für Android mit dem man REST-Endpunkte simpel ansprechen kann. Zusammen mit der Gson-Bibliothek lassen sich JSON-Nachrichten senden und empfangen. \\
Das angefragte API wird mit Klassen und Methoden in der Anwendung modelliert. Dadurch ist es möglich nur die Felder abzufragen, welche auch benötigt werden. Genaueres in Kapitel \ref{subsec:AnzeigeDaten}.
\subsection{Material Design}
Material ist eine Bibliothek, die Komponenten und Richtlinien bereitstellt. Nach einmaligem Einbinden der Bibliothek können die Komponenten verwendet werden, indem der Komponente der Style zugewiesen wird.
\section{Farbschema und Designsprache}
In einem gemeinsamen Meeting mit dem Web-Frontend einigten wir uns auf Farbcodes, die auf beiden Oberflächen verwendet werden. So haben wir uns auf ein dunkles Schema festgelegt, mit den Farben aus dem Logo für Schrift und Akzente. Als Schriftart wird Montserrat verwendet (siehe: Abbildungen \ref{Abb:login} - \ref{Abb:settings}). \\
\section{Umsetzung}
\subsection{Design der Activities}
Insgesamt besitzt die App die vier Activities: Login, MainActivity, Register und Settings. Wobei die Register- und die Settings-Activity aus zeitlichen Gründen ohne Funktion sind. Sie haben auch noch die alten unschönen Eingabefelder, sind aber für die Funktion der gesamten Anwendung nicht sonderlich relevant, weshalb entschieden wurde diese zu vernachlässigen und den Fokus auf die Funktionalität zulegen. \\
Jeder Bildschirm hat eine Top-Bar auf der, je nachdem auf welchem Bildschirm man sich befindet, unterschiedliche Inhalte angezeigt werden. Beim Einloggen und Account erstellen wird außer dem Logo und dem Namen der App nichts angezeigt. In den Einstellungen erscheint anstatt dem Logo ein Zurück-Button und auf dem Hauptbildschirm gibt es ein Menü zum Ausloggen und um zu den Einstellungen zu gelangen.
\begin{figure}[H]
\centering
\begin{minipage}[b]{.4\linewidth}
\includegraphics[width=\linewidth]{img/android/login}
\caption{Login Activity}
\label{Abb:login}
\end{minipage}
\hspace{.1\linewidth}
\begin{minipage}[b]{.4\linewidth}
\includegraphics[width=\linewidth]{img/android/register}
\caption{Register Activity}
\end{minipage}
\end{figure}
Links die Eingabefelder mit Material Design und rechts die alten selber erstellten.
\begin{figure}[H]
\centering
\begin{minipage}[b]{.4\linewidth}
\includegraphics[width=\linewidth]{img/android/main}
\caption{Main Activity}
\label{Abb:main}
\end{minipage}
\hspace{.1\linewidth}
\begin{minipage}[b]{.4\linewidth}
\includegraphics[width=\linewidth]{img/android/settings}
\caption{Settings Activity}
\label{Abb:settings}
\end{minipage}
\end{figure}
Wie zu erkennen ist lag der Fokus der Implementierung deutlich auf der Main Activtiy, da sie auch das Wichtigste der App beinhaltet. Prominent ist dabei der 'START'-Knopf an der Unterseite, mit dem die Aufzeichnung gestartet werden kann (genaueres im Kapitel \ref{subsec:main}).
\subsection{Authentifizierung}
Zur Authentifizierung benutzen wir JWT, welches bei jeder Anfrage ans Backend mit geschickt werden muss. Das Token erhält man beim Einloggen mit den richtigen Daten und muss persistiert werden, bis sich der Benutzer ausloggt. Dazu wird das Token im privaten Speicher der App gespeichert. In allen weiteren Activities kann dann auf den Speicher zugegriffen werden und das Token beim Erstellen des \verb|AuthenticationInterceptor|s mitgegeben werden. Beim Ausloggen wird einfach die Datei mit dem Token aus dem Speicher gelöscht. \\
Der \verb|AuthenticationInterceptor| ist Kind von der \verb|Interceptor|-Klasse aus der \verb|okhttp3|-Bibliothek, welche in Retrofit eingebunden ist. Mithilfe des Interceptors können REST-Aufrufen Header-Daten mitgegeben werden. In unserem Fall ist das das \verb|Authorization|-Feld mit dem Token.
\begin{lstlisting}[language=Kotlin]
class AuthenticationInterceptor(pToken: String) : Interceptor {
private val token = pToken
override fun intercept(chain: Interceptor.Chain): Response {
val original = chain.request()
val builder = original.newBuilder()
.header("Authorization", token)
val request = builder.build()
return chain.proceed(request)
}
}
\end{lstlisting}
Der Interceptor wird dem HTTP-Client hinzugefügt, welcher später bei der Erzeugung des Retrofit-Builders notwendig ist.
\begin{lstlisting}[language=Kotlin]
val httpClient = OkHttpClient.Builder()
val interceptor = AuthenticationInterceptor(token)
httpClient.addInterceptor(interceptor)
\end{lstlisting}
\subsection{Anzeige der Daten in der Main Activity}\label{subsec:AnzeigeDaten}
Die Daten werden per REST-Aufruf mithilfe vom Retrofit-Framework vom Backend geholt. Um Anfragen zusenden benötigt man einen Retrofit-Builder. Diesem wird die anzufragende URL, ein JSON-Konverter und ein HTTP-Client mitgegeben. Aus diesem Builder und einer Service-Klasse, in der die Methoden definiert sind, wird ein Objekt erzeugt mit dem die Methoden aufrufbar sind.
\begin{lstlisting}[language=Kotlin]
val builder = Retrofit.Builder()
.baseUrl("http://plesk.icaotix.de:5000")
.addConverterFactory(GsonConverterFactory.create())
.client(httpClient.build())
val retrofit = builder.build()
service = retrofit.create(GeofenceService::class.java)
\end{lstlisting}
Die Klasse \verb|GeofenceService| dient, wie oben beschrieben, zur definition der Endpunkte in Form von Methodenaufrufen. Dort wird definiert, ob es ein \verb|POST|- oder \verb|GET|-Entpunkt ist, wie der Pfad lautet und was für Parameter mitgegeben werden.
\begin{lstlisting}[language=Kotlin]
@POST("/login")
fun login(@Body login_data: ValuesUserLogin): Call<Void>
@GET("whoami")
fun getUser(): Call<ValuesUser>
@GET("accounts/search/findByUsername")
fun getAccounts(@Query("username") username : String): Call<EmbeddedAccounts>
\end{lstlisting}
Der Rückgabewert der Methoden ist immer vom Typ \verb|Call|. Wenn aus dem Body Werte gelesen werden sollen, muss eine Art Skelett-Klasse angelegt werden mit den relevanten Feldern. Die Klasse \verb|ValuesUser| stellt Werte der Antwort bereit, wie z. B. den Vornamen.
\begin{lstlisting}[language=Kotlin]
class ValuesUser(firstname: String) {
@SerializedName("firstname")
var firstname = firstname
}
\end{lstlisting}
Der Aufruf der Methode erfolgt Asynchron. Deshalb darf sich nicht auf das Ergebnis des Aufrufs direkt danach verlassen werden, sonst bekommt man eine Null-Pointer-Excetion. Die Methode \verb|enqueue| besitzt ein Callback-Objekt als Parameter, welches \verb|onResponse| und \verb|onFailure| überschreibt. Dort wird entsprechend definiert was in den jeweiligen Fällen ausgeführt werden soll.
\begin{lstlisting}[language=Kotlin]
val call = service.getUser()
call.enqueue(object : Callback<ValuesUser> {
override fun onResponse(call: Call<ValuesUser>, response: Response<ValuesUser>) {
if (response.isSuccessful) {
val firstname = response.body()?.firstname
lbl_username.text = "Hello " + firstname
} else {
println("Response not successful: ${response.code()}")
}
}
override fun onFailure(call: Call<ValuesUser>, t: Throwable) {
println("Response 'whoami' failed. " + t.message)
}
})
\end{lstlisting}
\bigskip
In dieser Art und Weise werden alle Anfragen ans Backend gehandhabt. Dazu zählen:
\begin{itemize}
\item Abfragen der Location-Daten zu dem Benutzer für den Geofence
\item Befüllen des Dropdown-Menüs mit den Timetrack-Accounts des Benutzers
\item Anzeigen der Beschreibung und der Vergütung
\item Befüllen des RecyclerViews mit den heutigen Einträgen
\item Auslösen des Start-/Stopp-Events
\item Einloggen
\end{itemize}
\subsection{Geofencing}\label{subsec:geofence}
Die Geofencing-Funktion ist die zentrale Funktion für die App und auch für das gesamte Projekt. Deshalb war es wichtig, dass sie frühzeitig funktioniert. \\
Um die Position eines Gerätes zu bestimmen bedarf es einer Berechtigung, die vom Benutzer bestätigt werden muss. Für Geräte mit API-Level 28 und niedriger muss dafür die \verb|ACCESS_FINE_LOCATION|-Berechtigung gesetzt werden und für API-Level 29 und höher \verb|ACCESS_BACKGROUND_LOCATION|. \\
Der Geofence wird initialisiert, wenn für den Benutzer Geo-Daten gespeichert sind. Ist dies der Fall, so wird ein \verb|GeofencingClient| angelegt, dem dann der Geofence hinzugefügt wird. Der Geofence wird erzeugt mit den Parametern: Breitengrad, Längengrad, Radius, der Lebenszeit des Fence und den Übergangstypen. Die Typen sind in unserem Fall \verb|GEOFENCE_TRANSITION_ENTER| und \verb|GEOFENCE_TRANSITION_EXIT|, da wir immer reagieren wollen, wenn der Nutzer den Bereich verlässt oder betritt.
\begin{lstlisting}[language=Kotlin]
geofencingClient = LocationServices.getGeofencingClient(this)
geofence = Geofence.Builder().setRequestId("Geofence")
.setCircularRegion(lat, long, rad)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT)
.build()
\end{lstlisting}
Um den Geofence-Client zu starten wird auf das Objekt die \verb|addGeofences|-Methode ausgeführt mit einem \verb|GeofencingRequest|-Objekt und einem \verb|PendingIntent|-Objekt als Parameter.
\begin{lstlisting}[language=Kotlin]
geofencingClient.addGeofences(getGeofencingRequest(), geofencePendingIntent)?.run {
addOnSuccessListener { ... }
addOnFailureListener { ... }
}
\end{lstlisting}
In der \verb|getGeofencingRequest|-Methode wird festgelegt auf welches initiale Event reagiert werden soll und der oben erstellte Geofence wird hinzugefügt. Als initiales Event haben wir \verb|INITIAL_TRIGGER_ENTER| gewählt, da es ausgelöst wird wenn man sich bereits im Bereich befindet und die App startet. Denn erst mit dem Eintrittsevent wird der Button zum Starten der Aufzeichnung freigeschaltet. Das \verb|geofencePendingIntent| definiert die BroadcastReceiver-Klasse, welche bei jedem Event aufgerufen wird.
\begin{lstlisting}[language=Kotlin]
private fun getGeofencingRequest(): GeofencingRequest {
return GeofencingRequest.Builder().apply {
setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
addGeofence(geofence)
}.build()
}
private val geofencePendingIntent: PendingIntent by lazy {
val intent = Intent(this, GeofenceBroadcastReceiver::class.java)
PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
\end{lstlisting}
Die \verb|GeofenceBroadcastReceiver|-Klasse definiert was bei den jeweiligen Events ausgeführt werden soll. In unserem Fall ist dies das Verändern einer boolean Shared-Prefrences-Variable, je nachdem ob der Bereich betreten oder verlassen wurde. Warum diese Art und Weise gewählt wurde lesen Sie in Kapitel \ref{sec:Probleme}.\\
Das Code-Beispiel zeigt die Aktion beim Betreten des Bereichs.
\begin{lstlisting}[language=Kotlin]
context!!.getSharedPreferences("LOCATION", Context.MODE_PRIVATE)
?.edit()
?.putBoolean("ENABLED", true)
?.apply()
\end{lstlisting}
In der \verb|MainActivity| wird ein Listener für diese Shared-Prefrences-Variable definiert. Je nachdem zu welchem Wert sich die Variable ändert wird der Start/Stopp-Button freigeschalten oder gesperrt und wenn der Benutzer den Bereich verlässt, aber noch aufzeichnet, wird diese gestoppt und gespeichert. \\ \\
\section{Funktionen der App}
Wie oben beschrieben besteht die Android-App aus vier Activities. Die Register- und Settings-Activity sind aus zeitlichen Gründen ohne Funktion und layouttechnisch nicht überarbeitet. Der Fokus lag stark auf der Main-Activity, das Kernstück der App darstellt. Im Folgenden die Funktionalitäten der Activities Login und Main.
\subsection{Login Screen}
In der Abbildung \ref{Abb:login} ist der Login Screen zu sehen. Er besteht aus der Top-Action-Bar mit Logo und App-Name, den Eingabefeldern und zwei Buttons. Alle Komponenten sind aus der Material-Design-Bibliothek. \\
Zum Einloggen werden die Daten in die jeweiligen Felder eingegeben. Wenn ein Feld markiert ist, wird das ausgewählte Feld blau umrandet und der Hinweis wird auf die obere Linie verschoben. Das Passwortfeld zeigt nur kurz den eingegebenen Buchstaben an und wird dann zu einem '*', sodass das Passwort nicht offen lesbar ist. \\
Der Login-Button sendet die Daten an das Backend und prüft ob die Daten korrekt sind. Wenn dies der Fall ist, enthält die Antwort das Token, welches in den privaten Speicher abgelegt wird, und die App wechselt zum Hauptbildschirm. War der Login nicht erfolgreich, wird dem Benutzer eine Pop-Up-Meldung angezeigt und nichts weiter unternommen. Mit dem betätigen des Registrieren-Knopfes wird man auf die Register-Activity weitergeleitet.
\subsection{Main Activity}\label{subsec:main}
Auf dem Hauptbildschirm erscheint in der Top-Action-Bar ein drei Punkte Menü (Kebab-Menü), von dem aus man zu den Einstellungen gelangen oder sich ausloggen kann. Beim Ausloggen wird die Datei mit dem Benutzer-Token gelöscht und die Login-Activity aufgerufen.
\begin{figure}[H]
\centering
\includegraphics[width=0.4\linewidth]{img/android/main_menu}
\caption{Menü auf dem Hauptbildschirm}
\label{Abb:menu}
\end{figure}
Direkt unter der Top-Action-Bar wird der Benutzer mit dem Vornamen begrüßt (Abb.: \ref{Abb:main}). \\
In der Bedienfläche kann der Benutzer den Timetrack-Account auswählen und dessen Details ansehen, seine heutigen Arbeitszeiten ansehen und die Aufzeichnung starten oder stoppen. \\
Die Auswahl des Accounts erfolgt über ein Dropdown-Menü. Bei Auswahl wird sofort die zugehörige Beschreibung und die Vergütung angezeigt. Wenn die Aufzeichnung am laufen ist, wird das Dropdown-Menü ausgeblendet. Das verhindert, dass der Benutzer eine Aktivität für einen andern Account stoppen kann als er sie gestartet hat. Ist für den Benutzer noch kein Account vorhanden, wird "None" im Menü angezeigt und die beiden Felder für Beschreibung und Vergütung werden ausgeblendet. \\
Für die Anzeige der heutigen Arbeitszeiten haben wir eine RecyclerView verwendet. Das Layout dazu wird in einer extra XML-Datei definiert und mit Daten in einer Adapter-Klasse befüllt. Durch eine Backendabfrage bekommen wir die nötigen Daten dafür. Bei aktiver Aufzeichnung wir ein Element angezeigt mit der Startzeit und der Info, dass das Ende offen ist.
\begin{figure}[H]
\centering
\includegraphics[width=0.4\linewidth]{img/android/main_recording}
\caption{Laufende Aufzeichnung}
\label{Abb:menu}
\end{figure}
Der Start-Stop-Button schält die Aufzeichnung um, in dem ein Backend-Endpunkt angesprochen wird. In der App haben wir eine boolean-Variable \verb|running| definiert, welche gespeichert hält ob die Aufzeichnung aktiv ist. Anhand ihr wird entschieden wie der Start-Stop-Button aussieht und ob beim Verlassen des Geofence noch gestoppt werden muss. Der Button ist nicht Auswählbar wenn sich der Nutzer außerhalb seines Arbeitsplatzes befindet und zeigt dies auch an (Abb.: \ref{Abb:outside}). Ist der Nutzer dann im Bereich, wird "Start" angezeigt und der Button ist freigeschaltet. Während der Aufzeichnung trägt der Button die Schrift "Stop". Hat der Nutzer noch keine Geo-Daten für seinen Arbeitsplatz definiert, wird auch das auf dem Button angezeigt.
\begin{figure}[H]
\centering
\includegraphics[width=0.4\linewidth]{img/android/btn_outside}
\caption{Nutzer außerhalb seines Geofence}
\label{Abb:outside}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=0.4\linewidth]{img/android/btn_start}
\caption{Aufzeichnung kann gestartet werden}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=0.4\linewidth]{img/android/btn_stop}
\caption{Laufende Aufzeichnung}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=0.4\linewidth]{img/android/btn_no_fence}
\caption{Nutzer hat noch keinen Geo-Daten}
\end{figure}
Um versehentliches stoppen der Aufzeichnung zu verhindern, muss der Nutzer in einem Pop-Up-Dialog seine Aktion bestätigen.
\begin{figure}[H]
\centering
\includegraphics[width=0.4\linewidth]{img/android/confirm_stop}
\caption{Bestätigungs Dialog zum stoppen}
\label{Abb:menu}
\end{figure}
\section{Probleme und Lösungen}\label{sec:Probleme}
Damit die App auch die aktuellste Android Version unterstützt, mussten einige zusätzliche Punkte berücksichtigt werden. Neben der Berechtigung aus Kapitel \ref{subsec:geofence} musste in der \verb|build.gradle|-Datei Kompilierungsoptionen gesetzt werden. \\
Zu beginn wollten wir alle Activities mit Fragments realisieren, sodass es nur eine Activity gibt und alles weitere Fragments sind. Allerdings war es schwieriger zwischen den Fragments zu wechseln, als in den Tutorials beschrieben. Deshalb wurde mir von einem Teamkollegen empfohlen auf nur Activities umzusteigen. Zwischen diesen ist das hin- und herschalten deutlich einfacher, hat jedoch kein Zugriff auf Elemente der anderen Activities. \\
Das wurde bemerkbar, als wir aus der Klasse \verb|GeofenceBroadcastReceiver| eine Methode der \\\verb|MainActivity| zur Änderung der Oberfläche aufrufen wollte. Das hat den Grund, dass Android nicht sicher sagen kann das diese Activity gerade auch aktiv ist. Deshalb haben wir den Weg über die Shared Preferences gewählt mit einem Listener in der \verb|MainActivity|. \\
Initial wollten wir das Token in einer Datenklasse abspeichern, welche beim Einloggen befüllt wird. Dazu müsste allerdings das Objekt oder die Referenz zu jeder anderen Activity übergeben werden. Eine andere Möglichkeit stellen erneut die Shared Preferences dar. Wäre auch eine gute Lösung gewesen, welche wir aber dazu zu spät kennengelernt haben. Deshalb haben wir das Problem mit dem privaten Speicher gelöst. Er ist auch durch andere Apps und den Benutzer nicht einsehbar, bildet deshalb also keine Sicherheitslücke.\\
Unerwartet war, dass die Geofence-Funktion die normale Android Positionsbestimmung zusätzlich benötigt. Denn zuerst, hatten wir die Positionsbestimmung implementiert und dann die Geofence-Funktion, was funktioniert hat. Da in der Geofence-Funktion kein Code der normalen Positionsbestimmung referenziert wurde, dachten wir, man könne diesen weglassen, was ein Trugschluss war. Auch der Versuch Teile der Positionsbestimmung wegzulassen war ohne Erfolg. Deshalb beinhaltet die App auch Code für die normale Positionsbestimmung.
\section{Deployment}
Das Deployment spielte im Entwicklungsprozess der App keine große Rolle, da es Android-Studio benötigt um die App zu starten. Zum Abschluss haben wir allerdings den aktuellen Stand des Projekts in einer APK-Datei persistiert. Damit lässt sich die App auf andere Geräte installieren und in den App-Store laden. Zur Erstellung einer solchen APK muss ein Key zur Signatur angegeben werden.