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" } -