Merge branch '111-backend-chapter' into 'master'
Resolve "Backend Chapter" Closes #111 See merge request marcel.schwarz/2020ss-qbc-geofence-timetracking!108
This commit is contained in:
commit
e38a3c4ce0
@ -1,6 +1,5 @@
|
|||||||
package de.hft.geotime.entities;
|
package de.hft.geotime.entities;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonBackReference;
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
package de.hft.geotime.entities;
|
package de.hft.geotime.entities;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.fasterxml.jackson.annotation.JsonManagedReference;
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import javax.persistence.*;
|
import javax.persistence.*;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
|
@ -11,7 +11,6 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
|||||||
import org.springframework.web.cors.CorsConfiguration;
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
import org.springframework.web.cors.CorsConfigurationSource;
|
import org.springframework.web.cors.CorsConfigurationSource;
|
||||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
import static de.hft.geotime.security.SecurityConstants.SIGN_UP_URL;
|
import static de.hft.geotime.security.SecurityConstants.SIGN_UP_URL;
|
||||||
|
|
||||||
@ -44,7 +43,7 @@ public class WebSecurity extends WebSecurityConfigurerAdapter {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
CorsConfigurationSource corsConfigurationSource() {
|
CorsConfigurationSource corsConfigurationSource() {
|
||||||
final CorsConfiguration configuration = new CorsConfiguration().applyPermitDefaultValues();
|
final CorsConfiguration configuration = new CorsConfiguration().applyPermitDefaultValues();
|
||||||
configuration.addAllowedMethod("*");
|
configuration.addAllowedMethod("*");
|
||||||
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
source.registerCorsConfiguration("/**", configuration);
|
source.registerCorsConfiguration("/**", configuration);
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
% Code
|
% Code
|
||||||
\usepackage{listings}
|
\usepackage{listings}
|
||||||
\usepackage{color}
|
\usepackage{xcolor}
|
||||||
\definecolor{dkgreen}{rgb}{0,0.6,0}
|
\definecolor{dkgreen}{rgb}{0,0.6,0}
|
||||||
\definecolor{gray}{rgb}{0.5,0.5,0.5}
|
\definecolor{gray}{rgb}{0.5,0.5,0.5}
|
||||||
\definecolor{mauve}{rgb}{0.58,0,0.82}
|
\definecolor{mauve}{rgb}{0.58,0,0.82}
|
||||||
@ -97,6 +97,33 @@
|
|||||||
sensitive=true,
|
sensitive=true,
|
||||||
stringstyle={\color{dkgreen}\ttfamily},
|
stringstyle={\color{dkgreen}\ttfamily},
|
||||||
}
|
}
|
||||||
|
\colorlet{punct}{red!60!black}
|
||||||
|
\definecolor{delim}{RGB}{20,105,176}
|
||||||
|
\colorlet{numb}{magenta!60!black}
|
||||||
|
\lstdefinelanguage{json}{
|
||||||
|
basicstyle=\normalfont\ttfamily,
|
||||||
|
stepnumber=1,
|
||||||
|
numbersep=8pt,
|
||||||
|
showstringspaces=false,
|
||||||
|
breaklines=true,
|
||||||
|
literate=
|
||||||
|
*{0}{{{\color{numb}0}}}{1}
|
||||||
|
{1}{{{\color{numb}1}}}{1}
|
||||||
|
{2}{{{\color{numb}2}}}{1}
|
||||||
|
{3}{{{\color{numb}3}}}{1}
|
||||||
|
{4}{{{\color{numb}4}}}{1}
|
||||||
|
{5}{{{\color{numb}5}}}{1}
|
||||||
|
{6}{{{\color{numb}6}}}{1}
|
||||||
|
{7}{{{\color{numb}7}}}{1}
|
||||||
|
{8}{{{\color{numb}8}}}{1}
|
||||||
|
{9}{{{\color{numb}9}}}{1}
|
||||||
|
{:}{{{\color{punct}{:}}}}{1}
|
||||||
|
{,}{{{\color{punct}{,}}}}{1}
|
||||||
|
{\{}{{{\color{delim}{\{}}}}{1}
|
||||||
|
{\}}{{{\color{delim}{\}}}}}{1}
|
||||||
|
{[}{{{\color{delim}{[}}}}{1}
|
||||||
|
{]}{{{\color{delim}{]}}}}{1},
|
||||||
|
}
|
||||||
|
|
||||||
\usepackage{setspace}
|
\usepackage{setspace}
|
||||||
\setstretch{1.2} %Zeilenabstand
|
\setstretch{1.2} %Zeilenabstand
|
||||||
@ -150,6 +177,8 @@
|
|||||||
|
|
||||||
\listoffigures
|
\listoffigures
|
||||||
|
|
||||||
|
\lstlistoflistings
|
||||||
|
|
||||||
\include{parts/einleitung}
|
\include{parts/einleitung}
|
||||||
|
|
||||||
\include{parts/projektplanung}
|
\include{parts/projektplanung}
|
||||||
|
BIN
documentation/img/backend/er-modell.png
Normal file
BIN
documentation/img/backend/er-modell.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 409 KiB |
BIN
documentation/img/backend/jwt.io.png
Normal file
BIN
documentation/img/backend/jwt.io.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 62 KiB |
BIN
documentation/img/gt_logo.png
Normal file
BIN
documentation/img/gt_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 299 KiB |
@ -1,61 +1,66 @@
|
|||||||
\chapter{Android-App}
|
\chapter{Android-App}
|
||||||
\section{Technologiebeschreibung}
|
\section{Technologiebeschreibung}
|
||||||
\subsection{Android SDK}
|
\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. \\
|
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})
|
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}
|
\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 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 musste jedoch viel Zeit investiert werden um zum einen die Sprache und zum anderen die Entwicklungsumgebung, sowie den 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).
|
|
||||||
|
Die aktuellste Kotlin-Version zur Zeit der Fertigstellung ist 1.3.72.
|
||||||
|
|
||||||
\subsection{Retrofit}
|
\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. \\
|
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}.
|
|
||||||
|
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}
|
\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.
|
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}
|
\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}). \\
|
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}
|
\section{Umsetzung}
|
||||||
\subsection{Design der Activities}
|
\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. \\
|
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 zu legen.
|
||||||
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.
|
|
||||||
|
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 des Logos ein Zurück-Button und auf dem Hauptbildschirm gibt es ein Menü zum Ausloggen und um zu den Einstellungen zu gelangen.
|
||||||
\begin{figure}[H]
|
\begin{figure}[H]
|
||||||
\centering
|
\centering
|
||||||
\begin{minipage}[b]{.4\linewidth}
|
\begin{minipage}[b]{0.4\linewidth}
|
||||||
\includegraphics[width=\linewidth]{img/android/login}
|
\includegraphics[width=\linewidth]{img/android/login}
|
||||||
\caption{Login Activity}
|
\caption{Login Activity}
|
||||||
\label{Abb:login}
|
\label{Abb:login}
|
||||||
\end{minipage}
|
\end{minipage}
|
||||||
\hspace{.1\linewidth}
|
\hspace{.1\linewidth}
|
||||||
\begin{minipage}[b]{.4\linewidth}
|
\begin{minipage}[b]{0.4\linewidth}
|
||||||
\includegraphics[width=\linewidth]{img/android/register}
|
\includegraphics[width=\linewidth]{img/android/register}
|
||||||
\caption{Register Activity}
|
\caption{Register Activity}
|
||||||
\end{minipage}
|
\end{minipage}
|
||||||
\end{figure}
|
\end{figure}
|
||||||
Links die Eingabefelder mit Material Design und rechts die alten selber erstellten.
|
Links die Eingabefelder mit Material Design und rechts die alten, selbst erstellten.
|
||||||
\begin{figure}[H]
|
\begin{figure}[H]
|
||||||
\centering
|
\centering
|
||||||
\begin{minipage}[b]{.4\linewidth}
|
\begin{minipage}[b]{0.4\linewidth}
|
||||||
\includegraphics[width=\linewidth]{img/android/main}
|
\includegraphics[width=\linewidth]{img/android/main}
|
||||||
\caption{Main Activity}
|
\caption{Main Activity}
|
||||||
\label{Abb:main}
|
\label{Abb:main}
|
||||||
\end{minipage}
|
\end{minipage}
|
||||||
\hspace{.1\linewidth}
|
\hspace{.1\linewidth}
|
||||||
\begin{minipage}[b]{.4\linewidth}
|
\begin{minipage}[b]{0.4\linewidth}
|
||||||
\includegraphics[width=\linewidth]{img/android/settings}
|
\includegraphics[width=\linewidth]{img/android/settings}
|
||||||
\caption{Settings Activity}
|
\caption{Settings Activity}
|
||||||
\label{Abb:settings}
|
\label{Abb:settings}
|
||||||
\end{minipage}
|
\end{minipage}
|
||||||
\end{figure}
|
\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}).
|
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}
|
\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 speichere ich das Token im privaten Speicher der App. 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. \\
|
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.
|
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]
|
\begin{lstlisting}[language=Kotlin,caption=AuthenticationInterceptor]
|
||||||
class AuthenticationInterceptor(pToken: String) : Interceptor {
|
class AuthenticationInterceptor(pToken: String) : Interceptor {
|
||||||
private val token = pToken
|
private val token = pToken
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
@ -68,15 +73,15 @@ class AuthenticationInterceptor(pToken: String) : Interceptor {
|
|||||||
}
|
}
|
||||||
\end{lstlisting}
|
\end{lstlisting}
|
||||||
Der Interceptor wird dem HTTP-Client hinzugefügt, welcher später bei der Erzeugung des Retrofit-Builders notwendig ist.
|
Der Interceptor wird dem HTTP-Client hinzugefügt, welcher später bei der Erzeugung des Retrofit-Builders notwendig ist.
|
||||||
\begin{lstlisting}[language=Kotlin]
|
\begin{lstlisting}[language=Kotlin,caption=HTTP Client]
|
||||||
val httpClient = OkHttpClient.Builder()
|
val httpClient = OkHttpClient.Builder()
|
||||||
val interceptor = AuthenticationInterceptor(token)
|
val interceptor = AuthenticationInterceptor(token)
|
||||||
httpClient.addInterceptor(interceptor)
|
httpClient.addInterceptor(interceptor)
|
||||||
\end{lstlisting}
|
\end{lstlisting}
|
||||||
|
|
||||||
\subsection{Anzeige der Daten in der Main Activity}\label{subsec:AnzeigeDaten}
|
\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.
|
Die Daten werden per REST-Aufruf mithilfe vom Retrofit-Framework vom Backend geholt. Um Anfragen zu senden, 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]
|
\begin{lstlisting}[language=Kotlin,caption=Retrofit Builder]
|
||||||
val builder = Retrofit.Builder()
|
val builder = Retrofit.Builder()
|
||||||
.baseUrl("http://plesk.icaotix.de:5000")
|
.baseUrl("http://plesk.icaotix.de:5000")
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
@ -84,8 +89,8 @@ val builder = Retrofit.Builder()
|
|||||||
val retrofit = builder.build()
|
val retrofit = builder.build()
|
||||||
service = retrofit.create(GeofenceService::class.java)
|
service = retrofit.create(GeofenceService::class.java)
|
||||||
\end{lstlisting}
|
\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.
|
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]
|
\begin{lstlisting}[language=Kotlin,caption=GeofenceService]
|
||||||
@POST("/login")
|
@POST("/login")
|
||||||
fun login(@Body login_data: ValuesUserLogin): Call<Void>
|
fun login(@Body login_data: ValuesUserLogin): Call<Void>
|
||||||
|
|
||||||
@ -96,14 +101,14 @@ fun getUser(): Call<ValuesUser>
|
|||||||
fun getAccounts(@Query("username") username : String): Call<EmbeddedAccounts>
|
fun getAccounts(@Query("username") username : String): Call<EmbeddedAccounts>
|
||||||
\end{lstlisting}
|
\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.
|
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]
|
\begin{lstlisting}[language=Kotlin,caption=ValuesUser]
|
||||||
class ValuesUser(firstname: String) {
|
class ValuesUser(firstname: String) {
|
||||||
@SerializedName("firstname")
|
@SerializedName("firstname")
|
||||||
var firstname = firstname
|
var firstname = firstname
|
||||||
}
|
}
|
||||||
\end{lstlisting}
|
\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.
|
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]
|
\begin{lstlisting}[language=Kotlin,caption=Callback der "getUser" Funktion]
|
||||||
val call = service.getUser()
|
val call = service.getUser()
|
||||||
call.enqueue(object : Callback<ValuesUser> {
|
call.enqueue(object : Callback<ValuesUser> {
|
||||||
override fun onResponse(call: Call<ValuesUser>, response: Response<ValuesUser>) {
|
override fun onResponse(call: Call<ValuesUser>, response: Response<ValuesUser>) {
|
||||||
@ -131,10 +136,12 @@ call.enqueue(object : Callback<ValuesUser> {
|
|||||||
\end{itemize}
|
\end{itemize}
|
||||||
|
|
||||||
\subsection{Geofencing}\label{subsec:geofence}
|
\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. \\
|
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|. \\
|
|
||||||
|
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.
|
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]
|
\begin{lstlisting}[language=Kotlin,caption=Anlegen des Geofencing Clients]
|
||||||
geofencingClient = LocationServices.getGeofencingClient(this)
|
geofencingClient = LocationServices.getGeofencingClient(this)
|
||||||
geofence = Geofence.Builder().setRequestId("Geofence")
|
geofence = Geofence.Builder().setRequestId("Geofence")
|
||||||
.setCircularRegion(lat, long, rad)
|
.setCircularRegion(lat, long, rad)
|
||||||
@ -143,14 +150,14 @@ geofence = Geofence.Builder().setRequestId("Geofence")
|
|||||||
.build()
|
.build()
|
||||||
\end{lstlisting}
|
\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.
|
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]
|
\begin{lstlisting}[language=Kotlin,caption="addGeofences" Methode]
|
||||||
geofencingClient.addGeofences(getGeofencingRequest(), geofencePendingIntent)?.run {
|
geofencingClient.addGeofences(getGeofencingRequest(), geofencePendingIntent)?.run {
|
||||||
addOnSuccessListener { ... }
|
addOnSuccessListener { ... }
|
||||||
addOnFailureListener { ... }
|
addOnFailureListener { ... }
|
||||||
}
|
}
|
||||||
\end{lstlisting}
|
\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 habe ich \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.
|
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]
|
\begin{lstlisting}[language=Kotlin,caption=Setzen der Geofence Trigger]
|
||||||
private fun getGeofencingRequest(): GeofencingRequest {
|
private fun getGeofencingRequest(): GeofencingRequest {
|
||||||
return GeofencingRequest.Builder().apply {
|
return GeofencingRequest.Builder().apply {
|
||||||
setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
|
setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
|
||||||
@ -162,22 +169,23 @@ private val geofencePendingIntent: PendingIntent by lazy {
|
|||||||
PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
\end{lstlisting}
|
\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}.\\
|
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.
|
||||||
Das Code-Beispiel zeigt die Aktion beim Betreten des Bereichs.
|
\begin{lstlisting}[language=Kotlin,caption=Ändern der Shared-Preferences]
|
||||||
\begin{lstlisting}[language=Kotlin]
|
|
||||||
context!!.getSharedPreferences("LOCATION", Context.MODE_PRIVATE)
|
context!!.getSharedPreferences("LOCATION", Context.MODE_PRIVATE)
|
||||||
?.edit()
|
?.edit()
|
||||||
?.putBoolean("ENABLED", true)
|
?.putBoolean("ENABLED", true)
|
||||||
?.apply()
|
?.apply()
|
||||||
\end{lstlisting}
|
\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. \\ \\
|
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 freigeschaltet oder gesperrt. Wenn der Benutzer den Bereich verlässt, aber noch aufzeichnet, wird dadurch die Aufzeichnung automatisch gestoppt und gespeichert.
|
||||||
|
|
||||||
\section{Funktionen der App}
|
\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.
|
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, die das Kernstück der App darstellt. Im Folgenden die Funktionalitäten der Activities Login und Main.
|
||||||
\subsection{Login Screen}
|
\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. \\
|
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.
|
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}
|
\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.
|
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.
|
||||||
@ -187,17 +195,20 @@ context!!.getSharedPreferences("LOCATION", Context.MODE_PRIVATE)
|
|||||||
\caption{Menü auf dem Hauptbildschirm}
|
\caption{Menü auf dem Hauptbildschirm}
|
||||||
\label{Abb:menu}
|
\label{Abb:menu}
|
||||||
\end{figure}
|
\end{figure}
|
||||||
Direkt unter der Top-Action-Bar wird der Benutzer mit dem Vornamen begrüßt (Abb.: \ref{Abb:main}). \\
|
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. \\
|
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.
|
||||||
Für die Anzeige der heutigen Arbeitszeiten habe ich 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 bekomme ich die nötigen Daten dafür. Bei aktiver Aufzeichnung wir ein Element angezeigt mit der Startzeit und der Info, dass das Ende offen ist.
|
|
||||||
|
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 anderen Account stoppen kann, als für den, auf dem 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 wird ein Element angezeigt mit der Startzeit und der Info, dass das Ende offen ist.
|
||||||
\begin{figure}[H]
|
\begin{figure}[H]
|
||||||
\centering
|
\centering
|
||||||
\includegraphics[width=0.4\linewidth]{img/android/main_recording}
|
\includegraphics[width=0.4\linewidth]{img/android/main_recording}
|
||||||
\caption{Laufende Aufzeichnung}
|
\caption{Laufende Aufzeichnung}
|
||||||
\label{Abb:menu}
|
\label{Abb:menu}
|
||||||
\end{figure}
|
\end{figure}
|
||||||
Der Start-Stop-Button schält die Aufzeichnung um, in dem ein Backend-Endpunkt angesprochen wird. In der App habe ich 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.
|
Der Start-Stop-Button schaltet die Aufzeichnung um, in dem ein Backend-Endpunkt angesprochen wird. In der App haben wir eine boolean-Variable \verb|running| definiert, welche speichert, 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]
|
\begin{figure}[H]
|
||||||
\centering
|
\centering
|
||||||
\includegraphics[width=0.4\linewidth]{img/android/btn_outside}
|
\includegraphics[width=0.4\linewidth]{img/android/btn_outside}
|
||||||
@ -219,7 +230,7 @@ context!!.getSharedPreferences("LOCATION", Context.MODE_PRIVATE)
|
|||||||
\includegraphics[width=0.4\linewidth]{img/android/btn_no_fence}
|
\includegraphics[width=0.4\linewidth]{img/android/btn_no_fence}
|
||||||
\caption{Nutzer hat noch keinen Geo-Daten}
|
\caption{Nutzer hat noch keinen Geo-Daten}
|
||||||
\end{figure}
|
\end{figure}
|
||||||
Um versehentliches stoppen der Aufzeichnung zu verhindern, muss der Nutzer in einem Pop-Up-Dialog seine Aktion bestätigen.
|
Um versehentliches Stoppen der Aufzeichnung zu verhindern, muss der Nutzer in einem Pop-Up-Dialog seine Aktion bestätigen.
|
||||||
\begin{figure}[H]
|
\begin{figure}[H]
|
||||||
\centering
|
\centering
|
||||||
\includegraphics[width=0.4\linewidth]{img/android/confirm_stop}
|
\includegraphics[width=0.4\linewidth]{img/android/confirm_stop}
|
||||||
@ -228,15 +239,15 @@ context!!.getSharedPreferences("LOCATION", Context.MODE_PRIVATE)
|
|||||||
\end{figure}
|
\end{figure}
|
||||||
|
|
||||||
\section{Probleme und Lösungen}\label{sec:Probleme}
|
\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. \\
|
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} mussten in der \verb|build.gradle|-Datei Kompilierungsoptionen gesetzt werden.\\
|
||||||
|
|
||||||
Zu beginn wollte ich 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. \\
|
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 sind wir auf reine Activities umgestiegen. Zwischen diesen ist das Hin- und Herschalten deutlich einfacher, es besteht jedoch kein Zugriff auf Elemente der anderen Activities.\\
|
||||||
|
|
||||||
Das wurde bemerkbar, als ich 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 habe ich den Weg über die Shared Preferences gewählt mit einem Listener in der \verb|MainActivity|. \\
|
Das wurde aber erst dann problematisch, als wir aus der Klasse \verb|GeofenceBroadcastReceiver| eine Methode der \verb|MainActivity| zur Änderung der Oberfläche aufrufen wollten. Das hat den Grund, dass Android nicht sicher sagen kann, dass 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 wollte ich 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 ich aber dazu zu spät kennengelernt habe. Deshalb habe ich 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.\\
|
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. Das wäre auch eine gute Lösung gewesen, welche wir aber zu spät entdeckt haben. Deshalb haben wir das Problem mit dem privaten Speicher gelöst. Er ist durch andere Apps und den Benutzer nicht einsehbar, bildet deshalb also kein Sicherheitsrisiko.\\
|
||||||
|
|
||||||
Unerwartet war, dass die Geofence-Funktion die normale Android Positionsbestimmung zusätzlich benötigt. Denn zuerst, hatte ich die Positionsbestimmung implementiert und dann die Geofence-Funktion, was funktioniert hat. Da in der Geofence-Funktion kein Code der normalen Positionsbestimmung referenziert wurde, dachte ich 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.
|
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}
|
\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 habe ich 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.
|
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.
|
@ -1,15 +1,316 @@
|
|||||||
\chapter{Backend}
|
\chapter{Backend}
|
||||||
|
Das Backend ist das Herzstück einer jeden Anwendung. Es muss hochverfügbar und enorm fehlertolerant sein. Aus diesem Grund haben wir uns für Technologien entschieden, die Open-Source-Software sind und eine entsprechend große Verbreitung haben. Weiter war es von Anfang an wichtig, trotz der großen Abhängigkeit zum Backend die Entwicklung der anderen Teile nicht zu blockieren. Es wurden daher frühzeitig Modelle und Protokolle erstellt, die bereits vor der Fertigstellung gemockt werden konnten.
|
||||||
\section{Technologiebeschreibung}
|
\section{Technologiebeschreibung}
|
||||||
\subsection{Spring Boot}
|
\subsection{Spring Boot}
|
||||||
|
Für die Implementierung des REST-Backends haben wir auf das Spring Framework gesetzt. Genauer gesagt, das Spring \emph{Boot} Framework. Das Wort "Boot" steht hierbei sinngemäß für "bootstrap", was uns viel Konfigurationsarbeit abgenommen hat. Alle Standard Beans und Factories waren bereits initialisiert und konnten ohne weitere Konfiguration genutzt werden.
|
||||||
|
|
||||||
|
Es wurden im Projektverlauf auch noch diverse Erweiterungen des Frameworks genutzt.
|
||||||
|
\begin{itemize}
|
||||||
|
\item \textbf{org.springframework.boot:spring-boot-starter-web} bringt einen integrierten Tomcat Application Server mit und ermöglicht das Verarbeiten von Webanfragen.
|
||||||
|
\item \textbf{org.springframework.boot:spring-boot-starter-actuator} wird genutzt, um Diagnose-Endpoints freizuschalten.
|
||||||
|
\item \textbf{org.springframework.boot:spring-boot-starter-data-jpa} bringt alle nötigen Abhängigkeiten, um mit der Java Persistence API Daten in einer Datenbank abzulegen.
|
||||||
|
\item \textbf{org.springframework.boot:spring-boot-starter-data-rest} bietet Möglichkeiten, sehr leicht Datenbank Entitäten als HTTP REST Ressourcen bereitzustellen.
|
||||||
|
\item \textbf{org.springframework.boot:spring-boot-starter-security} wird später zusammen mit der Authentifizierung über JWT genutzt.
|
||||||
|
\item \textbf{org.springframework.boot:spring-boot-starter-test} bringt Möglichkeiten, leichtgewichtig Unit Tests für Webanwendungen zu schreiben.
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
Zur weiteren Reduktion des "Boilerplate Codes" wurde zusätzlich noch das Lombok Framework\footnote{\url{https://projectlombok.org/}} genutzt. Es bietet die Möglichkeit, Getter und Setter sowie diverse Konstruktoren für Datenklassen zu generieren. Dadurch konnten die Datenklassen um etwa 80\% in der Große reduziert werden, dies fördert die Lesbarkeit und vermeidet auch Leichtsinnsfehler.
|
||||||
\subsection{MariaDB}
|
\subsection{MariaDB}
|
||||||
|
Als Datenbank wurde MariaDB eingesetzt. MariaDB ist die quelloffene Entwicklung der MySQL Datenbank und nimmt deshalb alle Befehle an, die auch MySQL annimmt. Als Alternative stand noch Postgres zur Auswahl, da wir aber keine der erweiterten Funktionen von Postgres brauchten, fiel die Auswahl auf MariaDB. MariaDB musste auf keinem Entwicklungsrechner installiert werden, da immer das offizielle Dockerimage\footnote{\url{https://hub.docker.com/_/mariadb}} genutzt wurde.
|
||||||
\subsection{Weitere Open Source Software}
|
\subsection{Weitere Open Source Software}
|
||||||
|
Eine weitere Bibliothek, die für die Authentifizierung benutzt wird, ist die Java-JWT Implementation von Auth0. Sowie die H2 In-Memory Datenbank. Diese zweite Datenbank wird während der Entwicklung genutzt, um schnell homogene Beispieldaten zu laden und Tests auf diesen durchzuführen.
|
||||||
\subsection{Spezielles Setup}
|
\subsection{Spezielles Setup}
|
||||||
|
Um produktiv zu arbeiten, mussten noch weitere Tools genutzt werden. Dazu zählt primär die IntelliJ IDEA Ultimate Entwicklungsumgebung\footnote{\url{https://www.jetbrains.com/de-de/idea/}}. Diese IDE hat sehr viele Integrationen für das Spring Framework, als auch mit unseren Docker-Setup. Es wird dadurch möglich, ausschließlich in der IDE zu arbeiten, ohne weitere Kommandozeilenfenster.
|
||||||
|
|
||||||
|
Das zweite wichtige Programm war der REST-Client Insomnia REST\footnote{\url{https://insomnia.rest/}}, welcher alle Möglichkeiten bietet, um REST APIs zu testen und Testabfragen auszuführen.
|
||||||
\section{Umsetzung}
|
\section{Umsetzung}
|
||||||
\subsection{Spring Entities}
|
\subsection{Spring Entities}
|
||||||
|
\begin{figure}[H]
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=\linewidth]{img/backend/er-modell.png}
|
||||||
|
\caption{ER-Modell}
|
||||||
|
\label{fig:er-modell}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
Das ER-Modell in Abbildung \ref{fig:er-modell} zeigt die komplette Hierarchie, wie sie unserem Konzept entspricht. Wir legen diese Definition aber nicht selbst in SQL an, sondern lassen Java Hibernate dies für uns tun. Die Grundstruktur der gespeicherten Daten ist wie folgt zu verstehen:
|
||||||
|
\begin{itemize}
|
||||||
|
\item Der \textbf{TimetrackUser} ist die Grundstruktur, die alle anderen Daten des Users zusammenhält. Sie speichert allgemeine Nutzerdaten und hält Referenzen auf die \textbf{Role} des Nutzers, seine \textbf{Location} und alle ihm gehörenden \textbf{TimetrackAccounts}.
|
||||||
|
\item Die \textbf{Role} sollte ursprünglich erlauben, zwischen einem Admins und einem normalen Nutzers zu unterscheiden, aus Zeitgründen wurde dies aber weggelassen. Die Grundstruktur ist dennoch implementiert, allerdings so, dass jeder Nutzer automatisch Administrator ist.
|
||||||
|
\item Die \textbf{Location} Entität speichert den Geofence des Nutzers. Diese Daten werden ausschließlich von der Android App genutzt, um beim Einloggen den Geofence zu setzen.
|
||||||
|
\item Der \textbf{TimetrackAccount} ist die zweite große Struktur, die alle \textbf{TimeRecords} des Nutzers verwaltet. Jeder Nutzer kann mehrere \textbf{TimetrackAccounts} besitzen, aber jeder Account kann nur einem Nutzer gehören.
|
||||||
|
\item Jede getrackte Zeitspanne wird in einem \textbf{TimeRecord} abgespeichert. Dieser Record speichert einen Typ sowie das Start- und Enddatum. Der Typ kann entweder "PAID" oder "BREAK" sein. Jeder Record gehört zu genau einem TimetrackAccount.
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
Die Umsetzung in Java wird nun am Beispiel des TimetrackUsers und des TimetrackAccounts gezeigt.
|
||||||
|
|
||||||
|
\lstinputlisting[language=Java,caption=TimetrackUser,firstline=10]{../backend/src/main/java/de/hft/geotime/entities/TimetrackUser.java}
|
||||||
|
|
||||||
|
Die komplette Klasse ist durch die Lombok Integration sehr klein gehalten. Alles weitere wird durch Annotationen geregelt, einige Beispiele sind hier:
|
||||||
|
\begin{itemize}
|
||||||
|
\item[] \textbf{@Entity} markiert die Klasse als Speicherbar in der Datenbank.
|
||||||
|
\item[] \textbf{@ManyToOne} markiert das Attribut als Fremdschlüssenrelation aus einer anderen Tabelle.
|
||||||
|
\item[] \textbf{@Id} zeichnet den Primärschlüssel der Tabelle aus.
|
||||||
|
\item[] \textbf{@Column} setzt spezielle Attribute für die Spalte in der Datenbank.
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
Die TimetrackAccounts haben zusätzlich noch die Eigenschaft, dass sie sich selbst rekursiv löschen, wenn der zugehörige User gelöscht wird. Selbiges gilt auch für die Records, wenn der zugehörige Account gelöscht wird.
|
||||||
|
|
||||||
|
\lstinputlisting[language=Java,caption=TimetrackAccount,firstline=11]{../backend/src/main/java/de/hft/geotime/entities/TimetrackAccount.java}
|
||||||
|
|
||||||
\subsection{Sicherheit durch JWT}
|
\subsection{Sicherheit durch JWT}
|
||||||
|
Da wird die Web App im Laufe des Projekts auch öffentlich in Internet stellen mussten, war eine Art Authentifizierung so gut wie unumgänglich. Damit wir keine Probleme mit Session-Affinity haben, entschieden wir uns für eine Token-Based Authentifizierung. Bei der genauen Implementation handelt es sich hier um das Json Web Token, kurz JWT.
|
||||||
|
\begin{figure}[H]
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=\linewidth]{img/backend/jwt.io.png}
|
||||||
|
\caption{Aufbau eines JWT}
|
||||||
|
\label{fig:aufbau-jwt}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
In Abbildung \ref{fig:aufbau-jwt} ist ein exemplarischer Aufbau eines JWT dargestellt. Das JWT besteht grundsätzlich aus drei Teilbereichen:
|
||||||
|
\begin{enumerate}
|
||||||
|
\item \textbf{Rot hinterlegt:} Bei diesem Teil handelt es sich um den Header, dieser beinhaltet den Typ des Tokens, als auch den Algorithmus, mit dem es verschlüsselt wurde.
|
||||||
|
\item \textbf{Lila hinterlegt:} In diesem Teil werden die eigentlichen Nutzdaten des Tokens abgelegt, dort können z.B. Nutzernamen oder Nutzer-Id sowie eine Rolle hinterlegt werden.
|
||||||
|
\item \textbf{Blau hinterlegt:} Der letzte Part ist dann noch die Signatur des Tokens.
|
||||||
|
\end{enumerate}
|
||||||
|
Jeder dieser Teile ist durch einen Punkt im Token abgetrennt. Es ist daher nicht verwunderlich, dass alle Token das selbe Präfix haben werden und nur der Mittelteil, sowie die Signatur sich ändern.
|
||||||
|
|
||||||
|
Die Implementation in Spring Boot gelang in drei, vergleichsweise einfachen, Schritten. Zunächst mussten einige Konstanten definiert werden, zur einfacheren Handhabung wurde auch das Secret in den Code platziert. Dieses könnte aber sehr leicht über eine Umgebungsvariable überschrieben werden.
|
||||||
|
|
||||||
|
\lstinputlisting[language=Java,caption=JWT Security Constants]{../backend/src/main/java/de/hft/geotime/security/SecurityConstants.java}
|
||||||
|
|
||||||
|
Die Lebensdauer eines Tokens wurde mit 10 Tagen ebenfalls sehr hoch gewählt, um die Entwicklung zu vereinfachen. Auch muss dem Token zur erfolgreichen Nutzung in anderen Systemen das Prefix "Bearer " vorangestellt werden.
|
||||||
|
|
||||||
|
Um nun die Tokens in Java zu erzeugen und Abzugleichen, musste die Filterkette von Spring Boot, welche bei jedem Request durchlaufen wird, bearbeitet werden. Jeder Endpunkt außer "/login" und "/sign-up" benötigte ab diesem Zeitpunkt eine autorisierte Anfrage.
|
||||||
|
|
||||||
|
\lstinputlisting[language=Java,linerange={30-48},caption=JWT Authentication Filter,label=code:jwt-authentication-parse]{../backend/src/main/java/de/hft/geotime/security/JWTAuthenticationFilter.java}
|
||||||
|
|
||||||
|
In Listing \ref{code:jwt-authentication-parse} ist der Schritt zu sehen, der die ankommende Anfrage versucht, in eine Loginanfrage zu parsen. Diese Anfrage wird dann in der Filterkette weitergereicht. Bis sie zum UserDetailsService kommt, welcher den User in der Datenbank abfragt und auch das Password abgleicht. Sollte die interne Autorisation erfolgreich sein, wird dieses Objekt mit den Nutzerdaten wieder an die Filterkette zurückgegeben und landet schließlich bei Listing \ref{code:jwt-authentication-create}.
|
||||||
|
|
||||||
|
\lstinputlisting[language=Java,linerange={50-62},caption=JWT Authentication Filter,label=code:jwt-authentication-create]{../backend/src/main/java/de/hft/geotime/security/JWTAuthenticationFilter.java}
|
||||||
|
|
||||||
|
Der letzte Schritt ist dann nur noch, das Token mit den erhaltenen Daten zu befüllen und dann den "Authorization" Header der Antwort auf das soeben erstelle Token zu setzen.
|
||||||
|
|
||||||
|
Ab jetzt kann sich der Client, der das Token angefragt hat, für die nächsten 10 Tage damit authentifizieren. Dies läuft sehr ähnlich ab, deshalb hier nur sehr kurz dargestellt.
|
||||||
|
|
||||||
|
\lstinputlisting[language=Java,linerange={40-55},caption=JWT Authorization Filter,label=code:jwt-authorization]{../backend/src/main/java/de/hft/geotime/security/JWTAuthorizationFilter.java}
|
||||||
|
|
||||||
|
Der eingehende Request geht wieder durch die Filterkette und wenn er an dem Filter in Listing \ref{code:jwt-authorization} ankommt, wird der User extrahiert und später im Security Manager als Autorisation für diesen Request gesetzt. Wichtig ist hier, dass keine weitere Prüfung auf die Existenz des Users durchgeführt wird, auch das Password wird nicht nochmal abgefragt. Der Grund hierfür ist, wenn es den User nicht geben würde, wie käme er dann an das Token?
|
||||||
\subsection{Repositories}
|
\subsection{Repositories}
|
||||||
|
Nachdem der Nutzer authentifiziert ist, bekommt er Zugriff auf alle REST-Repositories. Für jede Ressource, die oben im ER-Modell definiert ist gibt es ein entsprechendes Repository. Dieses wird größtenteils automatisch vom Classpath Scan von Spring automatisch implementiert. Die normalen CRUD Operationen werden für jedes angelegte Repository komplett ohne zutun implementiert. Ein solches Repository ist beispielsweise das der Location.
|
||||||
|
|
||||||
|
\lstinputlisting[language=Java,firstline=8,caption=LocationRepository,label=code:location-repository]{../backend/src/main/java/de/hft/geotime/repositories/LocationRepository.java}
|
||||||
|
|
||||||
|
Das einzige, was dort getan werden muss, ist die Angabe des Typs, der hier behandelt wird, hier Location, und der Datentyp des Primärschlüssels, hier ein Long. Die Bedeutung der Klasse "PagingAndSortingRepository" wird in einem späteren Kapitel genauer erläutert. Um den Link der Ressource anzupassen, werden die Parameter in der "RepositoryRestRessource" Annotation genutzt. Der Pfad geht immer vom Rootpfad der Applikation aus.
|
||||||
|
|
||||||
|
Werden nun noch weitere Funktionalitäten in den Repositories benötigt, können diese entweder selbst implementiert werden, oder durch gut ausgewählte Funktionsdefinitionen im Interface der Ressource deklariert werden. Spring kann die Implementierung dann aus dem Namen und den Parametern der Funktion ableiten. Als unser Maximalbeispiel dient hier das RecordRepository.
|
||||||
|
|
||||||
|
\lstinputlisting[language=Java,linerange={14-27,38-41,47-54},caption=RecordRepository,label=code:record-repository]{../backend/src/main/java/de/hft/geotime/repositories/RecordRepository.java}
|
||||||
|
|
||||||
|
In diesem Repository befinden sich diverse verschiedene Methoden, wie Datenoperationen definiert werden können, ohne dass sie aktiv implementiert werden müssen. Es beginnt mit der Funktion "findAllByStartdateBetween". Dieser Name kann direkt als Java Hibernate Statement interpretiert werden und nimmt als Parameter zwei Datumsangaben entgegen und eine Page. Die zwei Datumsangaben werden aus dem Schlüsselwort "Between" abgeleitet. Damit es sich aber um echt vergleichbare Daten handelt, müssen diese nach einem bestimmten Schema geparsed werden. Dieses Schema ist in der "DateTimeFormat" Annotation angegeben. Als Rückgabe liefert diese Funktion dann eine Menge aller Einträge zwischen diesen Daten.
|
||||||
|
|
||||||
|
Die nächste Funktion funktioniert nun ähnlich, nur dass dort über Eigenschaften mehrerer verlinkter Objekte gegangen werden kann. "findAllBy" ist wieder das selbe wie oben und zeigt an, dass eine Liste von Ergebnissen zurückgeliefert wird, aber "Account\_User\_Username" bedeutet nun folgendes: "Gehe zum Account des Records, dann zum User dieses Accounts und von diesem User dann den Username". Der gefundene Username wird dann mit dem Parameter der Funktion verglichen und die Ergebnisse entsprechend gefiltert. Weiter zeigt das "And" eine Verkettung eines weiteren Ausdrucks an. So können auch relativ komplexe Abfragen automatisch implementiert werden.
|
||||||
|
|
||||||
|
Reicht allerdings die obige Syntax nicht mehr aus, kann auch direkt eine Hibernate Abfrage über die "@Query" Annotation angegeben werden. Der Name der Funktion ist dann nicht mehr relevant für die Implementation, sondern nur noch für den Pfad, unter dem die Funktion später zu erreichen ist. Die Query an der "today" Funktion bietet nun die Möglichkeit, alle Einträge in der Records Tabelle für den aktuell anfragenden User zu bestimmen. Zusätzlich wird der Zeitraum noch auf den aktuellen Tag eingeschränkt, daher ergab sich auch der passende Name "today" für die Funktion. Der Nutzer wird automatisch über die "principal" Variable in der Abfrage eingefügt. Der Pricipal wird gesetzt, sobald der Authentication Filter den User erfolgreich eingeloggt hat. Weiter wird der aktuelle Tag über die Datenbankvariable "current\_date" abgefragt.
|
||||||
|
|
||||||
|
Zuletzt kann auch nach Standard SQL Werten wie "null" oder "not null" gefragt werden. Zu sehen ist dies in der zuletzt dargestellten Funktion.
|
||||||
|
|
||||||
|
Die Datei ist nicht vollständig abgedruckt, sondern nur ausschnittsweise, um die Grundkonzepte zu erläutern.
|
||||||
\subsection{Projections}
|
\subsection{Projections}
|
||||||
|
Projections bieten nun noch weitere Möglichkeiten, Daten vor der Rückgabe noch zu transformieren und gegebenenfalls mit Zusatzdaten anzureichern. Eine Projektion ist ebenfalls durch ein Interface definiert und bring vor allem dann Vorteile, wenn mehrere Ressourcen gebündelt angefragt werden müssen, um beispielsweise eine Übersicht zu erstellen.
|
||||||
|
|
||||||
|
\lstinputlisting[language=Java,firstline=9,caption=RecordOverviewProjection,label=code:record-overview-projection]{../backend/src/main/java/de/hft/geotime/entities/projections/RecordOverviewProjection.java}
|
||||||
|
|
||||||
|
Die "RecordOverviewProjection" (Listing \ref{code:record-overview-projection}) reichert eine normale "Record" Ressource noch zusätzlich mit dem Username und den Accountnamen an. Dadurch muss nicht für jeden Record erneut einzeln der Accountname nachgeschlagen werden. Zudem wird noch ein, bei jeder Anfrage neu berechnetes, zusätzliches Feld angefügt. Nämlich die Dauer des Records in Minuten.
|
||||||
|
|
||||||
|
Eine Projektion kann am Beispiel des Records gut verdeutlicht werden.
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=json,caption=Einzelner Record ohne Projektion,label=code:records-without-proj]
|
||||||
|
{
|
||||||
|
"startdate": "2020-05-30T18:00:00",
|
||||||
|
"enddate": "2020-05-30T19:00:00",
|
||||||
|
"type": "PAID",
|
||||||
|
"duration": 60,
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "http://localhost:5000/records/27"
|
||||||
|
},
|
||||||
|
"records": {
|
||||||
|
"href": "http://localhost:5000/records/27{?projection}",
|
||||||
|
"templated": true
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"href": "http://localhost:5000/records/27/account{?projection}",
|
||||||
|
"templated": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{lstlisting}[language=json,caption=Einzelner Record mit Projektion "overview",label=code:records-with-proj]
|
||||||
|
{
|
||||||
|
"duration": 60,
|
||||||
|
"username": "scma",
|
||||||
|
"account": "TestAccount",
|
||||||
|
"startdate": "2020-05-30T18:00:00",
|
||||||
|
"enddate": "2020-05-30T19:00:00",
|
||||||
|
"type": "PAID",
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "http://localhost:5000/records/27"
|
||||||
|
},
|
||||||
|
"records": {
|
||||||
|
"href": "http://localhost:5000/records/27{?projection}",
|
||||||
|
"templated": true
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"href": "http://localhost:5000/records/27/account{?projection}",
|
||||||
|
"templated": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
Es ist zu sehen, dass in Listing \ref{code:records-without-proj} die beiden Felder "account" und "username" fehlen, diese tauchen erst bei der Abfrage mit angewendeter, serverseitiger, Projektion auf (siehe Listing \ref{code:records-with-proj}). Die Anfrage für Listing \ref{code:records-without-proj} lautet \verb|http://localhost/records/27| und um nun die Projektion anzuhängen, wird die URL wie folgt erweitert: \verb|http://localhost/records/27?projection=overview|. Projektionen können hierbei nicht nur auf einzelne Objekte einer Ressource angewendet werden, sondern auch auf eine Menge dieser.
|
||||||
\section{Endpoints}
|
\section{Endpoints}
|
||||||
\subsection{HAL und Paging}
|
Die vier Hauptendpoints sind sicherlich die unserer Hautptressourcen: locations, accounts, records und users. Unten gibt es noch den nicht implementierten Endpoint für die Rollen ("roles"), dieser liefert aber kaum Informationen. Der "profile" Endpoint wird erst im nächsten Kapitel erläutert. Um diesen Output zu bekommen, muss der Nutzer authentifiziert sein. Dies geschieht, wie oben schon erwähnt, über den "/login" Endpoint. Da dieser aber keine Ausgaben außer dem Header mit dem Token liefert, wird er hier nicht weiter erläutert. Selbiges gilt auch für den "/sign-up" Endpoint. Alle Anfragen, die ab jetzt ausgeführt werden, geschehen immer mit vorheriger Authentifizierung.
|
||||||
|
\begin{lstlisting}[language=json,caption=Zugriff auf die Route "/" der API,label=code:main-route-api]
|
||||||
|
{
|
||||||
|
"_links": {
|
||||||
|
"locations": {
|
||||||
|
"href": "http://localhost:5000/locations{?page,size,sort}",
|
||||||
|
"templated": true
|
||||||
|
},
|
||||||
|
"accounts": {
|
||||||
|
"href": "http://localhost:5000/accounts{?page,size,sort,projection}",
|
||||||
|
"templated": true
|
||||||
|
},
|
||||||
|
"records": {
|
||||||
|
"href": "http://localhost:5000/records{?page,size,sort,projection}",
|
||||||
|
"templated": true
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"href": "http://localhost:5000/users{?page,size,sort,projection}",
|
||||||
|
"templated": true
|
||||||
|
},
|
||||||
|
"roles": {
|
||||||
|
"href": "http://localhost:5000/roles"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"href": "http://localhost:5000/profile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=json,caption=Zugriff auf die Route "/locations" der API,label=code:locations-route-api]
|
||||||
|
{
|
||||||
|
"_embedded": {
|
||||||
|
"locations": [
|
||||||
|
{
|
||||||
|
"latitude": 1.0,
|
||||||
|
"longitude": 2.0,
|
||||||
|
"radius": 3,
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "http://plesk.icaotix.de:5000/locations/1"
|
||||||
|
},
|
||||||
|
"locations": {
|
||||||
|
"href": "http://plesk.icaotix.de:5000/locations/1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "http://plesk.icaotix.de:5000/locations{?page,size,sort}",
|
||||||
|
"templated": true
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"href": "http://plesk.icaotix.de:5000/profile/locations"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"size": 20,
|
||||||
|
"totalElements": 6,
|
||||||
|
"totalPages": 1,
|
||||||
|
"number": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
Aufgrund der massiven Größe der Ausgaben der API werden die weiteren Endpoints nur noch mit ihrem Link angegeben. Alle Ressourcen unterstützen zudem die CRUD Operationen auf einzelnen Ressourcen, als auch auf der Hauptressource, deshalb werden sie aus Platzgründen ebenfalls übergangen.
|
||||||
|
|
||||||
|
\subsection*{Endpoints für Ressourcen}
|
||||||
|
\begin{itemize}
|
||||||
|
\item \verb|/locations{?page,size,sort,projection}|
|
||||||
|
\item \verb|/locations/<nr>{?projection}|
|
||||||
|
\item \verb|/roles{?page,size,sort,projection}|
|
||||||
|
\item \verb|/accounts{?page,size,sort,projection}|
|
||||||
|
\item \verb|/accounts/<nr>{?projection}|
|
||||||
|
\item \verb|/accounts/<nr>/user{?projection}|
|
||||||
|
\item \verb|/accounts/search/findByUsernameAndName{?username,account,projection}|
|
||||||
|
\item \verb|/accounts/search/findByUsername{?username,page,size,sort,projection}|
|
||||||
|
\item \verb|/users{?page,size,sort,projection}|
|
||||||
|
\item \verb|/users/<nr>{?projection}|
|
||||||
|
\item \verb|/users/<nr>/location{?projection}|
|
||||||
|
\item \verb|/users/<nr>/role{?projection}|
|
||||||
|
\item \verb|/users/search/byUsername{?username,projection}|
|
||||||
|
\item \verb|/records{?page,size,sort,projection}|
|
||||||
|
\item \verb|/records/<nr>{?projection}|
|
||||||
|
\item \verb|/records/<nr>/account{?projection}|
|
||||||
|
\item \verb|/records/search/allBetweenAndUser{?start,end,username,page,size,sort,projection}|
|
||||||
|
\item \verb|/records/search/openEntries{?page,size,sort,projection}|
|
||||||
|
\item \verb|/records/search/today{?page,size,sort,projection}|
|
||||||
|
\item \verb|/records/search/allForUser{?username,page,size,sort,projection}|
|
||||||
|
\item \verb|/records/search/allBetween{?start,end,page,size,sort,projection}|
|
||||||
|
\item \verb|/records/search/allFrom{?date,page,size,sort,projection}|
|
||||||
|
\item \verb|/records/search/allForUserAndAccount{?username,account,page,size,sort,projection}|
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
Wenn Ressourcen aktualisiert werden müssen, müssen die Daten immer im JSON Format vorliegen. Die Felder des JSON Objekts müssen mit denen der Ressource übereinstimmen. Es müssen aber nicht alle Felder Werte beinhalten. Soll eine neue Ressource erstellt werden, werden die Daten als POST abgesendet, bei einer Aktualisierung als PATCH. Links zwischen Ressourcen können über den Self Link der Ressource hergestellt werden. Weiter gibt es noch zwei komplett selbst gebaute Endpoints.
|
||||||
|
|
||||||
|
\subsection*{Der "/whoami" Endpoint}
|
||||||
|
Dieser Endpoint dient dazu, um nach dem Login schnell die Startseite der App oder der Webseite mit den Nutzerdaten zu befüllen. Es sind Daten wie der Vor- und Nachname, sowie der Username enthalten. Zusätzlich wird noch die gesetzte Location des Nutzers mitgegeben.
|
||||||
|
|
||||||
|
\subsection*{Der "/track" Endpoint}
|
||||||
|
Beim "/track" Endpoint handelt es sich um einen der wichtigsten Endpoints. Er erlaubt es, ein Recording zu erstellen, ohne Angabe des Nutzers. Lediglich der Name des Timetrack Accounts, auf den gebucht werden soll, muss angegeben werden. Der Endpoint entscheidet auf Serverseite, von welchem Nutzer die Anfrage ankam. Dazu wird der Nutzer aus dem JWT extrahiert und abhängig davon im Account des Nutzers geschaut, ob schon ein Tracking läuft oder nicht. Sollte noch kein Tracking laufen, wird ein neuer Eintrag mit der aktuellen Zeit erstellt und zurückgeliefert. Das Enddatum ist zu diesem Zeitpunkt noch leer und auch die Duration zeigt "0" an. Sollte schon ein Tracking laufen, wird dieses mit der aktuellen Zeit als Endzeit beendet und ebenfalls zurückgeliefert. Sollte der Account nicht gefunden werden, oder ein anderer Fehler auftreten, wird ein entsprechender HTTP Statuscode zurückgeliefert.
|
||||||
|
\begin{lstlisting}[language=json,caption=Aufruf von "/track" ohne laufendes Tracking,label=code:start-tracking-endpoint]
|
||||||
|
{
|
||||||
|
"duration": 0,
|
||||||
|
"username": "scma",
|
||||||
|
"account": "Demo",
|
||||||
|
"startdate": "2020-06-11T00:59:22",
|
||||||
|
"enddate": null,
|
||||||
|
"type": "PAID"
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{lstlisting}[language=json,caption=Aufruf von "/track" mit laufendem Tracking,label=code:stop-tracking-endpoint]
|
||||||
|
{
|
||||||
|
"duration": 129,
|
||||||
|
"username": "scma",
|
||||||
|
"account": "Demo",
|
||||||
|
"startdate": "2020-06-10T22:47:55",
|
||||||
|
"enddate": "2020-06-11T00:57:41",
|
||||||
|
"type": "PAID"
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\subsection{HAL, Paging und Sorting}
|
||||||
|
Die Hypertext Application Language, kurz HAL, ist eine Spezifikation, mit der APIs automatisch erkundbar gemacht werden können. Sie bietet META-Elemente an, einige davon werden auch bei uns benutzt.
|
||||||
|
\begin{itemize}
|
||||||
|
\item \textbf{"\_links"} zeigt weiterführende Links zu Ressourcen oder Informationen zum Paging an.
|
||||||
|
\item \textbf{"\_embedded"} enthält die Nutzdaten zur entsprechenden Ressource, aber auch weitere Einbettungen zu Sub-Ressourcen.
|
||||||
|
\end{itemize}
|
||||||
|
Zusätzlich dazu nutzt Spring bei der Generation der Repositories auch Teile der "Hypermedia as the Engine of Application State", kurz HATEOAS, Spezifikation. Das Listing \ref{code:main-route-api} zeigt hierfür den zusätzlichen Endpoint "profile". Unter diesem sind viele Spezifikationen zu finden, wie alle anderen Routen auf bestimmte Daten reagieren und auch antworten.
|
||||||
|
|
||||||
|
Der "profile" Endpoint zeigt zusätzlich noch alle Projektionen an, die auf eine bestimmte Ressource angewendet werden können. Der Name der Projektion wird dann durch den URL-Parameter "projection=" angehängt.
|
||||||
|
|
||||||
|
Zuletzt gilt es noch zu erwähnen, dass alle Ressourcen Paging und Sorting unterstützen. Paging ist besonders bei Web APIs wichtig, da die Geschwindigkeit sehr stark von der Menge der übertragenen Daten abhängt. Wenn eine Ressource immer alle Daten zurückliefern würde, würde dies bei mehreren hundert Einträgen sicher noch funktionieren. Aber sobald die Zahl der Einträge deutlich höher wird, muss abgeschnitten und aufgeteilt werden. Unsere Standard Seitengröße ist auf 20 Einträge gesetzt. Weiter enthält die Antwort des Servers durch die HAL Integration immer Links zur aktuellen, nächsten und vorherigen Seite als Link. Sorting wird ebenfalls unterstützt. Es kann nach jedem Feld im zurückgegebenen JSON sortiert werden, auch die Richtung ist spezifizierbar.
|
||||||
\section{Probleme und Lösungen}
|
\section{Probleme und Lösungen}
|
||||||
\section{Deployment}
|
\subsection{Einlesen in Spring}
|
||||||
|
Spring ist ein sehr komplexes Framework, weshalb es manchmal wirklich sehr schwierig war, Fehler zu verstehen, und die Gründe dahinter zu verstehen. Solange man sich aber an viele der Best-Practicies von Spring hält, ist es überhaupt nicht schwer, in relativ kurzer Zeit auch sehr komplexe APIs zu bauen. Durch die enorme Menge an Dokumentation und auch Hilfe aus der Community sowie Techtalks können viele Probleme leicht gelöst werden.
|
||||||
|
\subsection{Änderungen an den Endpoints}
|
||||||
|
Es mussten anfangs viele Endpoints immer wieder umdefiniert werden, da sie nicht Best-Practicies entsprochen haben oder nicht performant funktioniert haben. Dies wurde später aber immer einfacher, wenn man sich an die Denkweise einer REST-API gewöhnt hat. Auch zwei Wege Links zwischen Ressourcen waren bei uns nicht möglich, da sie zu Endlosrekursionen führten. Später wurde aber auch klar, das dies überhaupt nicht gewünscht ist.
|
||||||
|
\subsection{Probleme mit MariaDB}
|
||||||
|
Zu Beginn haben wir für das Docker-Image der Datenbank den "latest"-Tag benutzt. Dies war möglich, da wir keinerlei eigene Konfiguration der Datenbank und deren Tabellen vorgenommen haben. Allerdings wurde Mitte April die neue LTS-Version von Ubuntu veröffentlicht und damit auch das Basisimage von MariaDB angepasst. Durch Änderungen in Ubuntu 20.04 funktionierten nun gewisse Datenbankfunktionen nicht mehr ordnungsgemäß. Als Lösung kam dann nur ein Downgrade auf eine ältere Version in Frage.
|
||||||
|
\section{Deployment}
|
||||||
|
Das Deployment des Backends findet immer gleichzeitig mit der Datenbank und dem Frontend statt. Die Daten bleiben dabei erhalten und werden, so fern nötig, von Spring automatisch migriert. Auch beim Hinzufügen oder Entfernen von Feldern aus Entitäten aktualisiert Spring die Datenbanktabellen entsprechend den neuen Feldern. Sollten Felder wegfallen, werden diese einfach gelöscht. Kommen neue hinzu, wird entweder der Defaultwert gesetzt oder, wenn erlaubt, "Null".
|
@ -1,5 +1,8 @@
|
|||||||
\chapter{Einleitung}
|
\chapter{Einleitung}
|
||||||
Im Rahmen der Vorlesung Ubiquitous Computing ist eine Projektarbeit mit einem Thema der Allgegenwärtigkeit von Computern vorgesehen. \\
|
Im Rahmen der Vorlesung Ubiquitous Computing ist eine Projektarbeit mit dem Thema der Allgegenwärtigkeit von Computern vorgesehen.
|
||||||
Dazu fanden wir uns als Gruppe zusammen und sammelten unsere Ideen für Projektthemen. Letztendlich setzte sich der Timetracker mit Geofence gegen eine Wetterstation und eine Augmented Reality App durch. \\
|
|
||||||
Die Idee der Timetrack-Anwendung ist, dass ein Arbeitnehmer erst dann mit der Aufzeichnung seiner Arbeitszeit beginnen kann wenn er sich am Arbeitsort befindet. Wenn er diesen verlässt, wird seine Aufzeichnung beendet. Dabei hat er mehrere Zeitkonten zur Auswahl auf die er seine Arbeitszeit verbuchen kann. \\
|
Dazu fanden wir uns als Gruppe zusammen und sammelten unsere Ideen für Projektthemen. Letztendlich setzte sich der Timetracker mit Geofence gegen eine Wetterstation und eine Augmented Reality App durch.
|
||||||
Das Projekt ist in drei Teile unterteilt: Backend, Web-Frontend und Android-App. Entsprechend dem Aufwand arbeiteten am Web-Frontend zwei Studenten, am Backend und der Android-App jeweils ein Student. Die Android-App soll lediglich die Basisfunktionalitäten bieten wie Starten und Stoppen der Aufzeichnung für einen gewählten Account, wenn man sich am Arbeitsort befindet, sowie log in und log out. Im Web-Frontend sollen verschiedenste Statistiken angezeigt werden, sowie Adminfunktionaltäten um Accounts zu editieren, neue Einträge hinzufügen, Geodaten für den Arbeitnehmer setzen und Benutzer löschen. Das Backend kommuniziert mit der Datenbank, sichert die Authentifizierung der Benutzer und stellt den Oberflächen Endpoints zur Verfügung.
|
|
||||||
|
Die Idee der Timetrack-Anwendung ist, dass ein Arbeitnehmer erst dann mit der Aufzeichnung seiner Arbeitszeit beginnen kann, wenn er sich am Arbeitsort befindet. Wenn er diesen verlässt, wird seine Aufzeichnung beendet. Dabei hat er mehrere Zeitkonten zur Auswahl, auf die er seine Arbeitszeit verbuchen kann.
|
||||||
|
|
||||||
|
Das Projekt ist in drei Teile unterteilt: Backend, Web-Frontend und Android-App. Entsprechend dem Aufwand arbeiteten am Web-Frontend zwei Studenten, am Backend und der Android-App jeweils ein Student. Die Android-App soll lediglich die Basisfunktionalitäten bieten, wie Starten und Stoppen der Aufzeichnung für einen gewählten Account, wenn man sich am Arbeitsort befindet, sowie log in und log out. Im Web-Frontend sollen verschiedenste Statistiken angezeigt werden, sowie Adminfunktionaltäten, um Accounts zu editieren, neue Einträge hinzufügen, Geodaten für den Arbeitnehmer setzen und Benutzer löschen. Das Backend kommuniziert mit der Datenbank, sichert die Authentifizierung der Benutzer und stellt den Oberflächen Endpoints zur Verfügung.
|
@ -7,16 +7,16 @@
|
|||||||
\subsection{Sprints}
|
\subsection{Sprints}
|
||||||
Wir haben die Projektzeit in 5 zweiwöchige Arbeitssprints und einen einwöchigen Vorbereitungssprint aufgeteilt. Die Enddaten der Sprints waren dabei an die Treffen mit Professor Knauth angepasst.
|
Wir haben die Projektzeit in 5 zweiwöchige Arbeitssprints und einen einwöchigen Vorbereitungssprint aufgeteilt. Die Enddaten der Sprints waren dabei an die Treffen mit Professor Knauth angepasst.
|
||||||
\subsection{Code-Owners}
|
\subsection{Code-Owners}
|
||||||
In unserem Git-Reposetory haben wir mit Code Ownership gearbeitet. Dazu haben wir 3 Ownerships eingeführt. Marcel Schwarz war Code-Owner für das Backend, Tobias Wieck für die Android App und Simon Kellner, sowie Tim Zieger für das Frontend. Wenn eine Änderung im jeweiligen Gebiet gemacht wurde musste immer mindestens ein Code-Owner diese genehmigen.
|
In unserem Git-Reposetory haben wir mit Code Ownership gearbeitet. Dazu haben wir 3 Ownerships eingeführt. Marcel Schwarz war Code-Owner für das Backend, Tobias Wieck für die Android App und Simon Kellner, sowie Tim Zieger für das Frontend. Wenn eine Änderung im jeweiligen Gebiet gemacht wurde, musste immer mindestens ein Code-Owner diese genehmigen.
|
||||||
\section{Sprintziele}
|
\section{Sprintziele}
|
||||||
\subsection{Iteration 1}
|
\subsection{Iteration 1}
|
||||||
Das Ziel des ersten Sprints war die Erlernung der notwendigen Technologien und die Schnittstellendefinition.
|
Das Ziel des ersten Sprints war die Erlernung der notwendigen Technologien und die Schnittstellendefinition.
|
||||||
\subsection{Iteration 2}
|
\subsection{Iteration 2}
|
||||||
Im zweiten Sprint sollten die Designgrundlagen und Feature Scopes besprochen werden. Des weiteren sollte im Backend die Verbindung zwischen dem Backend und der Datenbank hergestellt werden. Im Frontend war geplant weiter am Grundgerüst der Seite zu arbeiten und bei der App wurde die Einarbeitung weitergeführt.
|
Im zweiten Sprint sollten die Designgrundlagen und Feature Scopes besprochen werden. Des weiteren sollte im Backend die Verbindung zwischen dem Backend und der Datenbank hergestellt werden. Im Frontend war geplant, weiter am Grundgerüst der Seite zu arbeiten und bei der App wurde die Einarbeitung weitergeführt.
|
||||||
\subsection{Iteration 3}
|
\subsection{Iteration 3}
|
||||||
Im Frontend sollte, im dritten Sprint, ein neues Designframework eingeführt werden und die
|
Im Frontend sollte im dritten Sprint ein neues Designframework eingeführt werden und die
|
||||||
Kommunikation mit dem Backend getestet werden. Der Plan fürs Backend war die Erstellung der Restcontroller. Bei der Android App sollte die Loginfunktionalität, sowie das Einlesen der Geo-Informationen realisiert werden.
|
Kommunikation mit dem Backend getestet werden. Der Plan fürs Backend war die Erstellung der Restcontroller. Bei der Android App sollte die Loginfunktionalität, sowie das Einlesen der Geo-Informationen realisiert werden.
|
||||||
\subsection{Iteration 4}
|
\subsection{Iteration 4}
|
||||||
Für den vierten Sprint war im Backend geplant die letzten Controller und Endpoints zu entwickeln. Im Frontend sollten die restlichen Seiten mitsamt Datenabholung aus dem Backend entwickelt werden. Für die App sollte der Geofence entwickelt und die Kommunikation mit dem Backend aufgebaut werden.
|
Für den vierten Sprint war im Backend geplant, die letzten Controller und Endpoints zu entwickeln. Im Frontend sollten die restlichen Seiten mitsamt Datenabholung aus dem Backend entwickelt werden. Für die App sollte der Geofence entwickelt und die Kommunikation mit dem Backend aufgebaut werden.
|
||||||
\subsection{Iteration 5}
|
\subsection{Iteration 5}
|
||||||
Das Ziel des letzten Sprint war die Fertigstellung des Projekts, die Erstellung der Dokumentation und die Vorbereitung der Präsentation.
|
Das Ziel des letzten Sprint war die Fertigstellung des Projekts, die Erstellung der Dokumentation und die Vorbereitung der Präsentation.
|
@ -1,6 +1,8 @@
|
|||||||
\begin{titlepage}
|
\begin{titlepage}
|
||||||
\centering
|
\centering
|
||||||
\leftline{\includegraphics[width=0.35\linewidth]{img/hft_logo}} \par
|
\includegraphics[width=0.35\linewidth]{img/hft_logo}
|
||||||
|
\hfill
|
||||||
|
\includegraphics[width=0.22\linewidth]{img/gt_logo} \par
|
||||||
\textcolor{lightgray}{\rule{\linewidth}{1pt}}
|
\textcolor{lightgray}{\rule{\linewidth}{1pt}}
|
||||||
\vspace{1cm}\\
|
\vspace{1cm}\\
|
||||||
{\scshape\LARGE Studiengang Informatik \\ Ubiquitous Computing \\ Sommersemester 2020 \par}
|
{\scshape\LARGE Studiengang Informatik \\ Ubiquitous Computing \\ Sommersemester 2020 \par}
|
||||||
|
Loading…
Reference in New Issue
Block a user