From 04369a78e33c1f5ee02feecede2ad9b1b8a10ddd Mon Sep 17 00:00:00 2001
From: Steffen Heger <71780550+steffenheger@users.noreply.github.com>
Date: Fri, 1 Nov 2024 15:58:51 +0100
Subject: [PATCH] Login driver app (#166)
* basic andriod app setup
* remove .gradle from VC
* update .gitignore
* login and basic navigation
* prevent back navigation from login screen
---
.gitignore | 1 +
driver-app/app/build.gradle.kts | 13 +-
driver-app/app/src/main/AndroidManifest.xml | 6 +-
.../app/src/main/java/de/motis/prima/Home.kt | 126 +++++++++++
.../app/src/main/java/de/motis/prima/Login.kt | 203 ++++++++++++++++++
.../main/java/de/motis/prima/MainActivity.kt | 6 +-
.../app/src/main/java/de/motis/prima/Nav.kt | 51 +++++
.../app/src/main/java/de/motis/prima/Tours.kt | 100 +++++++++
.../src/main/java/de/motis/prima/Vehicles.kt | 100 +++++++++
.../java/de/motis/prima/app/DriversApp.kt | 16 ++
.../main/java/de/motis/prima/services/Api.kt | 33 +++
.../de/motis/prima/services/CookieStore.kt | 49 +++++
.../de/motis/prima/services/OkHttpClient.kt | 27 +++
.../app/src/main/res/values/strings.xml | 7 +-
driver-app/gradle/libs.versions.toml | 9 +-
15 files changed, 739 insertions(+), 8 deletions(-)
create mode 100644 driver-app/app/src/main/java/de/motis/prima/Home.kt
create mode 100644 driver-app/app/src/main/java/de/motis/prima/Login.kt
create mode 100644 driver-app/app/src/main/java/de/motis/prima/Nav.kt
create mode 100644 driver-app/app/src/main/java/de/motis/prima/Tours.kt
create mode 100644 driver-app/app/src/main/java/de/motis/prima/Vehicles.kt
create mode 100644 driver-app/app/src/main/java/de/motis/prima/app/DriversApp.kt
create mode 100644 driver-app/app/src/main/java/de/motis/prima/services/Api.kt
create mode 100644 driver-app/app/src/main/java/de/motis/prima/services/CookieStore.kt
create mode 100644 driver-app/app/src/main/java/de/motis/prima/services/OkHttpClient.kt
diff --git a/.gitignore b/.gitignore
index 2a8f5ae..db3896b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@ booking-generator/data/stops.txt
booking-generator/test
.vscode
/driver-app/.gradle/
+/driver-app/.idea
diff --git a/driver-app/app/build.gradle.kts b/driver-app/app/build.gradle.kts
index e79df9d..6371c51 100644
--- a/driver-app/app/build.gradle.kts
+++ b/driver-app/app/build.gradle.kts
@@ -20,7 +20,7 @@ android {
buildTypes {
debug {
- buildConfigField("String", "BASE_URL", "\"http://130.83.165.211:8080\"")
+ buildConfigField("String", "BASE_URL", "\"http://130.83.165.211:7777\"")
}
release {
isMinifyEnabled = false
@@ -28,10 +28,12 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
+ buildConfigField("String", "BASE_URL", "\"https://prima.motis-project.de\"")
}
}
buildFeatures {
buildConfig = true
+ compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.0"
@@ -49,7 +51,14 @@ dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
+ implementation(libs.androidx.runtime.android)
+ implementation(libs.androidx.navigation.compose)
+ implementation("com.squareup.okhttp3:okhttp:4.12.0")
+ implementation("com.squareup.retrofit2:retrofit:2.11.0")
+ implementation("com.squareup.retrofit2:converter-gson:2.9.0")
+ implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1")
+ implementation(libs.androidx.material3.android)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
-}
\ No newline at end of file
+}
diff --git a/driver-app/app/src/main/AndroidManifest.xml b/driver-app/app/src/main/AndroidManifest.xml
index 6421789..f5cd14c 100644
--- a/driver-app/app/src/main/AndroidManifest.xml
+++ b/driver-app/app/src/main/AndroidManifest.xml
@@ -2,7 +2,11 @@
+
+
-
\ No newline at end of file
+
diff --git a/driver-app/app/src/main/java/de/motis/prima/Home.kt b/driver-app/app/src/main/java/de/motis/prima/Home.kt
new file mode 100644
index 0000000..bed66d2
--- /dev/null
+++ b/driver-app/app/src/main/java/de/motis/prima/Home.kt
@@ -0,0 +1,126 @@
+package de.motis.prima
+
+import android.util.Log
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.outlined.ExitToApp
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.navigation.NavController
+import de.motis.prima.app.DriversApp
+import de.motis.prima.services.CookieStore
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.launch
+
+class HomeViewModel : ViewModel() {
+ private val cookieStore: CookieStore = CookieStore(DriversApp.instance)
+
+ private val _logoutEvent = MutableSharedFlow()
+ val logoutEvent = _logoutEvent.asSharedFlow()
+
+ fun logout() {
+ viewModelScope.launch {
+ try {
+ cookieStore.clearCookies()
+ _logoutEvent.emit(Unit)
+ } catch (e: Exception) {
+ Log.d("Logout", "Error while logout.")
+ }
+ }
+ }
+}
+
+@Composable
+fun Home(
+ navController: NavController,
+ viewModel: HomeViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
+) {
+ LaunchedEffect(key1 = viewModel) {
+ launch {
+ viewModel.logoutEvent.collect {
+ Log.d("Logout", "Logout event triggered.")
+ navController.navigate("login") {
+ launchSingleTop = true
+ }
+ }
+ }
+ }
+
+ Scaffold(
+ modifier = Modifier
+ .fillMaxSize()
+ ) { contentPadding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(contentPadding)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.End
+ ) {
+ TextButton(onClick = { viewModel.logout() }) {
+ Icon(
+ Icons.AutoMirrored.Outlined.ExitToApp, contentDescription = null
+ )
+ }
+ }
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Button(
+ modifier = Modifier.width(300.dp),
+ onClick = {
+ navController.navigate("vehicles") {}
+ }
+ ) {
+ Text(
+ text = "Fahrzeug auswählen", fontSize = 24.sp
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Button(
+ modifier = Modifier.width(300.dp),
+ onClick = {
+ navController.navigate("tours") {}
+ }
+ ) {
+ Text(
+ text = "Aufträge", fontSize = 24.sp
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/driver-app/app/src/main/java/de/motis/prima/Login.kt b/driver-app/app/src/main/java/de/motis/prima/Login.kt
new file mode 100644
index 0000000..315ca3d
--- /dev/null
+++ b/driver-app/app/src/main/java/de/motis/prima/Login.kt
@@ -0,0 +1,203 @@
+package de.motis.prima
+
+import android.app.Activity
+import android.util.Log
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.Text
+import androidx.compose.material3.Button
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.constraintlayout.compose.ConstraintLayout
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.navigation.NavController
+import de.motis.prima.services.Api
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.launch
+
+class LoginViewModel : ViewModel() {
+ // Event which will be omitted to the Login component, indicating success of the login operation
+ private val _navigationEvent = MutableSharedFlow()
+ val navigationEvent = _navigationEvent.asSharedFlow()
+
+ private val _loginErrorEvent = MutableSharedFlow()
+ val loginErrorEvent = _loginErrorEvent.asSharedFlow()
+
+ private val _networkErrorEvent = MutableSharedFlow()
+ val networkErrorEvent = _networkErrorEvent.asSharedFlow()
+
+ fun login(email: String, password: String) {
+ viewModelScope.launch {
+ try {
+ val response = Api.apiService.login(email, password)
+ Log.d("Login Response", response.toString())
+ if (response.status == 302) {
+ // successful login
+ _navigationEvent.emit(true)
+ } else {
+ _loginErrorEvent.emit(true)
+ }
+ } catch (e: Exception) {
+ Log.d("Login Response Network Error", e.message!!)
+ _networkErrorEvent.emit(Unit)
+ }
+ }
+ }
+}
+
+@Composable
+fun Login(
+ navController: NavController,
+ viewModel: LoginViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
+) {
+ val snackbarHostState = remember { SnackbarHostState() }
+
+ var isLoginFailed by remember { mutableStateOf(false) }
+
+ val networkErrorMessage = stringResource(id = R.string.network_error_message)
+
+ val activity = (LocalContext.current as? Activity)
+ BackHandler {
+ activity?.finish()
+ }
+
+ LaunchedEffect(key1 = viewModel) {
+ // Catching successful login event and navigation to the next screen
+ launch {
+ viewModel.navigationEvent.collect { shouldNavigate ->
+ Log.d("Navigation event", "Navigation triggered.")
+ if (shouldNavigate) {
+ Log.d("Navigation event", "Navigating to vehicle selection.")
+ navController.navigate("home") {
+ popUpTo("login") {
+ inclusive = true
+ }
+ }
+ }
+ }
+ }
+
+ // Catching event when login failed due to incorrect login data
+ launch {
+ viewModel.loginErrorEvent.collect { error ->
+ isLoginFailed = error
+ }
+ }
+
+ // Catching event when a network error occurs and displaying of error message
+ launch {
+ viewModel.networkErrorEvent.collect {
+ snackbarHostState.showSnackbar(message = networkErrorMessage)
+ }
+ }
+ }
+
+ Scaffold(
+ modifier = Modifier.fillMaxSize(),
+ snackbarHost = {
+ BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
+ SnackbarHost(
+ hostState = snackbarHostState,
+ modifier = Modifier
+ .fillMaxSize()
+ .wrapContentHeight(Alignment.Top)
+ .padding(top = maxHeight * 0.25f)
+ )
+ }
+ }
+ ) { contentPadding ->
+ ConstraintLayout(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(contentPadding)
+ ) {
+ var email by remember {
+ mutableStateOf("")
+ }
+
+ var password by remember {
+ mutableStateOf("")
+ }
+
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ when {
+ isLoginFailed ->
+ Text(
+ text = stringResource(id = R.string.wrong_login_data),
+ color = Color.Red,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(bottom = 8.dp)
+ )
+ }
+ OutlinedTextField(
+ value = email,
+ onValueChange = {
+ email = it
+ isLoginFailed = false
+ },
+ label = { Text(stringResource(id = R.string.email_label)) },
+ maxLines = 1,
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
+ isError = isLoginFailed
+ )
+ Spacer(modifier = Modifier.height(12.dp))
+ OutlinedTextField(
+ value = password,
+ onValueChange = {
+ password = it
+ isLoginFailed = false
+ },
+ label = { Text(stringResource(id = R.string.password_label)) },
+ maxLines = 1,
+ visualTransformation = PasswordVisualTransformation(),
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
+ isError = isLoginFailed
+ )
+ Spacer(modifier = Modifier.height(20.dp))
+ Button(
+ onClick = {
+ isLoginFailed = false
+ Log.d("Login", "E-Mail: ${email}, Password: $password")
+ viewModel.login(email, password)
+ }
+ ) {
+ Text(
+ text = stringResource(id = R.string.login_button_text), fontSize = 18.sp
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/driver-app/app/src/main/java/de/motis/prima/MainActivity.kt b/driver-app/app/src/main/java/de/motis/prima/MainActivity.kt
index 82713e7..eed0be8 100644
--- a/driver-app/app/src/main/java/de/motis/prima/MainActivity.kt
+++ b/driver-app/app/src/main/java/de/motis/prima/MainActivity.kt
@@ -1,6 +1,7 @@
package de.motis.prima
import android.os.Bundle
+import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
@@ -9,5 +10,8 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
+ setContent {
+ Nav()
+ }
}
-}
\ No newline at end of file
+}
diff --git a/driver-app/app/src/main/java/de/motis/prima/Nav.kt b/driver-app/app/src/main/java/de/motis/prima/Nav.kt
new file mode 100644
index 0000000..814dabe
--- /dev/null
+++ b/driver-app/app/src/main/java/de/motis/prima/Nav.kt
@@ -0,0 +1,51 @@
+package de.motis.prima
+
+import android.util.Log
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import de.motis.prima.app.DriversApp
+import de.motis.prima.services.CookieStore
+
+@Composable
+fun Nav() {
+
+ val navController = rememberNavController()
+
+ // Check before render of any component whether user is authenticated.
+ val startDestination by remember {
+ derivedStateOf {
+ val cookieStore = CookieStore(DriversApp.instance)
+ if (cookieStore.isEmpty()) {
+ Log.d("Cookie", "No Cookie found. Navigating to Login.")
+ "login"
+ } else {
+ Log.d("Cookie", "Cookie found. Navigating to Journeys.")
+ "home"
+ }
+ }
+ }
+
+ NavHost(navController = navController, startDestination = startDestination) {
+
+ composable(route = "login") {
+ Login(navController)
+ }
+
+ composable(route = "home") {
+ Home(navController)
+ }
+
+ composable(route = "vehicles") {
+ Vehicles(navController)
+ }
+
+ composable(route = "tours") {
+ Tours(navController)
+ }
+ }
+}
diff --git a/driver-app/app/src/main/java/de/motis/prima/Tours.kt b/driver-app/app/src/main/java/de/motis/prima/Tours.kt
new file mode 100644
index 0000000..dfc9353
--- /dev/null
+++ b/driver-app/app/src/main/java/de/motis/prima/Tours.kt
@@ -0,0 +1,100 @@
+package de.motis.prima
+
+import android.util.Log
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.outlined.ExitToApp
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.navigation.NavController
+import de.motis.prima.app.DriversApp
+import de.motis.prima.services.CookieStore
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.launch
+
+class ToursViewModel : ViewModel() {
+ private val cookieStore: CookieStore = CookieStore(DriversApp.instance)
+
+ private val _logoutEvent = MutableSharedFlow()
+ val logoutEvent = _logoutEvent.asSharedFlow()
+
+ fun logout() {
+ viewModelScope.launch {
+ try {
+ cookieStore.clearCookies()
+ _logoutEvent.emit(Unit)
+ } catch (e: Exception) {
+ Log.d("Logout", "Error while logout.")
+ }
+ }
+ }
+}
+
+@Composable
+fun Tours(
+ navController: NavController,
+ viewModel: VehiclesViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
+) {
+ LaunchedEffect(key1 = viewModel) {
+ launch {
+ viewModel.logoutEvent.collect {
+ Log.d("Logout", "Logout event triggered.")
+ navController.navigate("login") {
+ launchSingleTop = true
+ }
+ }
+ }
+ }
+
+ Scaffold(
+ modifier = Modifier
+ .fillMaxSize()
+ ) { contentPadding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(contentPadding)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.End
+ ) {
+ TextButton(onClick = { viewModel.logout() }) {
+ Icon(
+ Icons.AutoMirrored.Outlined.ExitToApp, contentDescription = null
+ )
+ }
+ }
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = "Liste der Aufträge", fontSize = 24.sp
+ )
+ }
+ }
+ }
+}
diff --git a/driver-app/app/src/main/java/de/motis/prima/Vehicles.kt b/driver-app/app/src/main/java/de/motis/prima/Vehicles.kt
new file mode 100644
index 0000000..67b939b
--- /dev/null
+++ b/driver-app/app/src/main/java/de/motis/prima/Vehicles.kt
@@ -0,0 +1,100 @@
+package de.motis.prima
+
+import android.util.Log
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.outlined.ExitToApp
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.navigation.NavController
+import de.motis.prima.app.DriversApp
+import de.motis.prima.services.CookieStore
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.launch
+
+class VehiclesViewModel : ViewModel() {
+ private val cookieStore: CookieStore = CookieStore(DriversApp.instance)
+
+ private val _logoutEvent = MutableSharedFlow()
+ val logoutEvent = _logoutEvent.asSharedFlow()
+
+ fun logout() {
+ viewModelScope.launch {
+ try {
+ cookieStore.clearCookies()
+ _logoutEvent.emit(Unit)
+ } catch (e: Exception) {
+ Log.d("Logout", "Error while logout.")
+ }
+ }
+ }
+}
+
+@Composable
+fun Vehicles(
+ navController: NavController,
+ viewModel: VehiclesViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
+) {
+ LaunchedEffect(key1 = viewModel) {
+ launch {
+ viewModel.logoutEvent.collect {
+ Log.d("Logout", "Logout event triggered.")
+ navController.navigate("login") {
+ launchSingleTop = true
+ }
+ }
+ }
+ }
+
+ Scaffold(
+ modifier = Modifier
+ .fillMaxSize()
+ ) { contentPadding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(contentPadding)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.End
+ ) {
+ TextButton(onClick = { viewModel.logout() }) {
+ Icon(
+ Icons.AutoMirrored.Outlined.ExitToApp, contentDescription = null
+ )
+ }
+ }
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = "Liste der Fahrzeuge", fontSize = 24.sp
+ )
+ }
+ }
+ }
+}
diff --git a/driver-app/app/src/main/java/de/motis/prima/app/DriversApp.kt b/driver-app/app/src/main/java/de/motis/prima/app/DriversApp.kt
new file mode 100644
index 0000000..af82d60
--- /dev/null
+++ b/driver-app/app/src/main/java/de/motis/prima/app/DriversApp.kt
@@ -0,0 +1,16 @@
+package de.motis.prima.app
+
+import android.app.Application
+
+class DriversApp: Application() {
+
+ companion object {
+ lateinit var instance: DriversApp
+ private set
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ instance = this
+ }
+}
diff --git a/driver-app/app/src/main/java/de/motis/prima/services/Api.kt b/driver-app/app/src/main/java/de/motis/prima/services/Api.kt
new file mode 100644
index 0000000..c688533
--- /dev/null
+++ b/driver-app/app/src/main/java/de/motis/prima/services/Api.kt
@@ -0,0 +1,33 @@
+package de.motis.prima.services
+
+import de.motis.prima.BuildConfig.BASE_URL
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import retrofit2.http.Field
+import retrofit2.http.FormUrlEncoded
+import retrofit2.http.POST
+
+interface ApiService {
+ @POST("login")
+ @FormUrlEncoded
+ suspend fun login(
+ @Field("email") email: String,
+ @Field("password") password: String
+ ): LoginResponse
+}
+
+object Api {
+ private val retrofit: Retrofit = Retrofit.Builder()
+ .baseUrl(BASE_URL)
+ .client(okHttpClient().build())
+ .addConverterFactory(GsonConverterFactory.create())
+ .build()
+
+ val apiService: ApiService = retrofit.create(ApiService::class.java)
+}
+
+data class LoginResponse(
+ val type: String,
+ val status: Int,
+ val data: String
+)
diff --git a/driver-app/app/src/main/java/de/motis/prima/services/CookieStore.kt b/driver-app/app/src/main/java/de/motis/prima/services/CookieStore.kt
new file mode 100644
index 0000000..217bebf
--- /dev/null
+++ b/driver-app/app/src/main/java/de/motis/prima/services/CookieStore.kt
@@ -0,0 +1,49 @@
+package de.motis.prima.services
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.util.Log
+import okhttp3.Cookie
+import okhttp3.CookieJar
+import okhttp3.HttpUrl
+
+class CookieStore(context: Context) : CookieJar {
+ private val preferences: SharedPreferences = context.getSharedPreferences("cookies", Context.MODE_PRIVATE)
+
+ override fun saveFromResponse(url: HttpUrl, cookies: List) {
+ val editor = preferences.edit()
+ val serializedCookie = cookies.joinToString(";") { cookie -> cookie.toString() }
+ editor.putString(url.host, serializedCookie)
+ editor.apply()
+ }
+
+ override fun loadForRequest(url: HttpUrl): List {
+ val cookiesString = preferences.getString(url.host, null)
+ if (cookiesString.isNullOrEmpty()) {
+ Log.d("Cookie", "No stored cookie found.")
+ return listOf()
+ }
+ Log.d("cookie", "Cookie is $cookiesString")
+
+ val cookie = Cookie.parse(url, cookiesString)
+ if (cookie == null) {
+ Log.d("Cookie", "No cookie for host found.")
+ return listOf()
+ }
+ return listOf(cookie)
+ }
+
+ fun clearCookies(host: String? = null) {
+ val editor = preferences.edit()
+ if (host != null) {
+ editor.remove(host)
+ } else {
+ editor.clear()
+ }
+ editor.apply()
+ }
+
+ fun isEmpty(): Boolean {
+ return preferences.all.isEmpty()
+ }
+}
diff --git a/driver-app/app/src/main/java/de/motis/prima/services/OkHttpClient.kt b/driver-app/app/src/main/java/de/motis/prima/services/OkHttpClient.kt
new file mode 100644
index 0000000..32120f8
--- /dev/null
+++ b/driver-app/app/src/main/java/de/motis/prima/services/OkHttpClient.kt
@@ -0,0 +1,27 @@
+package de.motis.prima.services
+
+import de.motis.prima.BuildConfig
+import de.motis.prima.app.DriversApp
+import okhttp3.Interceptor
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+
+/**
+ * The login api accepts only requests with the following headers set explicitly
+ * This must be changed when CORS settings will be changed.
+ */
+fun okHttpClient() = OkHttpClient().newBuilder()
+ .cookieJar(CookieStore(DriversApp.instance))
+ .addInterceptor(
+ object : Interceptor {
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val request: Request = chain.request()
+ .newBuilder()
+ .header("x-sveltekit-action", "true")
+ .header("Origin", BuildConfig.BASE_URL)
+ .build()
+ return chain.proceed(request)
+ }
+ }
+ )
diff --git a/driver-app/app/src/main/res/values/strings.xml b/driver-app/app/src/main/res/values/strings.xml
index 1139b02..49c49cc 100644
--- a/driver-app/app/src/main/res/values/strings.xml
+++ b/driver-app/app/src/main/res/values/strings.xml
@@ -1,3 +1,8 @@
Prima+ÖV
-
\ No newline at end of file
+ Anmelden
+ Passwort oder E-Mail sind inkorrekt.
+ E-Mail
+ Passwort
+ Etwas ist schiefgelaufen. Bitte versuchen Sie es später erneut.
+
diff --git a/driver-app/gradle/libs.versions.toml b/driver-app/gradle/libs.versions.toml
index ec9c467..3669c45 100644
--- a/driver-app/gradle/libs.versions.toml
+++ b/driver-app/gradle/libs.versions.toml
@@ -7,7 +7,9 @@ junitVersion = "1.2.1"
espressoCore = "3.6.1"
appcompat = "1.7.0"
material = "1.12.0"
-
+runtimeAndroid = "1.7.4"
+navigationCompose = "2.8.3"
+material3Android = "1.3.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -16,9 +18,10 @@ androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "j
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
-
+androidx-runtime-android = { group = "androidx.compose.runtime", name = "runtime-android", version.ref = "runtimeAndroid" }
+androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
+androidx-material3-android = { group = "androidx.compose.material3", name = "material3-android", version.ref = "material3Android" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
-