Compare commits

..
12 Commits
36 changed files with 1357 additions and 135 deletions
+7 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId = "pl.firmatpp.kierowca" applicationId = "pl.firmatpp.kierowca"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 2 versionCode = 13
versionName = "1.0.1" versionName = "1.0.12"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "API_BASE_URL", "\"https://api-intranet.firmatpp.pl/api/\"") buildConfigField("String", "API_BASE_URL", "\"https://api-intranet.firmatpp.pl/api/\"")
@@ -45,6 +45,10 @@ android {
compose = true compose = true
buildConfig = true buildConfig = true
} }
lint {
disable += "NullSafeMutableLiveData"
}
} }
dependencies { dependencies {
@@ -64,6 +68,7 @@ dependencies {
implementation(libs.camera.view) implementation(libs.camera.view)
implementation(libs.coil.compose) implementation(libs.coil.compose)
implementation(libs.compose.material.icons.extended) implementation(libs.compose.material.icons.extended)
implementation("androidx.compose.material:material")
implementation(libs.compose.material3) implementation(libs.compose.material3)
implementation(libs.compose.ui) implementation(libs.compose.ui)
implementation(libs.compose.ui.graphics) implementation(libs.compose.ui.graphics)
+4
View File
@@ -2,10 +2,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application <application
android:allowBackup="false" android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.TppKierowca"> android:theme="@style/Theme.TppKierowca">
<activity <activity
@@ -0,0 +1,38 @@
package pl.firmatpp.kierowca.data
import android.content.Context
import android.os.Build
import android.provider.Settings
import pl.firmatpp.kierowca.BuildConfig
import pl.firmatpp.kierowca.data.model.DriverDeviceInfo
class DeviceInfoProvider(
private val context: Context,
private val tokenStore: TokenStore,
) {
suspend fun currentDeviceInfo(): DriverDeviceInfo =
DriverDeviceInfo(
deviceId = tokenStore.deviceId(),
androidId = Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID),
brand = Build.BRAND,
manufacturer = Build.MANUFACTURER,
model = Build.MODEL,
device = Build.DEVICE,
product = Build.PRODUCT,
androidVersion = Build.VERSION.RELEASE,
sdkInt = Build.VERSION.SDK_INT,
appVersion = BuildConfig.VERSION_NAME,
serialNumber = readSerialNumber(),
)
private fun readSerialNumber(): String? =
runCatching {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Build.getSerial()
} else {
@Suppress("DEPRECATION")
Build.SERIAL
}
}.getOrNull()
?.takeUnless { it.isBlank() || it.equals(Build.UNKNOWN, ignoreCase = true) }
}
@@ -19,32 +19,52 @@ class DriverRepository(
private val context: Context, private val context: Context,
private val api: MobileDriverApi = ApiFactory.mobileDriverApi(), private val api: MobileDriverApi = ApiFactory.mobileDriverApi(),
private val tokenStore: TokenStore = TokenStore(context), private val tokenStore: TokenStore = TokenStore(context),
private val deviceInfoProvider: DeviceInfoProvider = DeviceInfoProvider(context, tokenStore),
) { ) {
suspend fun requestOtp(phoneNumber: String): OtpResponse = suspend fun requestOtp(phoneNumber: String): OtpResponse =
api.requestOtp(RequestOtpBody(phoneNumber)) api.requestOtp(RequestOtpBody(phoneNumber))
suspend fun verifyOtp(phoneNumber: String, code: String, deviceName: String): DriverDto { suspend fun verifyOtp(phoneNumber: String, code: String, deviceName: String): DriverDto {
val response = api.verifyOtp(VerifyOtpBody(phoneNumber, code, deviceName)) val response = api.verifyOtp(VerifyOtpBody(phoneNumber, code, deviceName, deviceInfoProvider.currentDeviceInfo()))
val token = response.accessToken ?: error("Brak tokenu sesji.") val token = response.accessToken ?: error("Brak tokenu sesji.")
tokenStore.save(token) tokenStore.save(token)
return response.driver ?: error("Brak danych kierowcy.") return response.driver ?: error("Brak danych kierowcy.")
} }
suspend fun bootstrap(): BootstrapResponse = suspend fun bootstrap(date: String? = null): BootstrapResponse =
api.bootstrap(authHeader(requireToken())) api.bootstrap(authHeader(requireToken()), date)
suspend fun route(routeId: String): RouteResponse = suspend fun route(routeId: String): RouteResponse =
api.route(authHeader(requireToken()), routeId) api.route(authHeader(requireToken()), routeId)
suspend fun uploadPhoto(routeId: String, uri: Uri, source: String): PhotoUploadResponse { suspend fun uploadPhoto(
routeId: String,
uri: Uri,
source: String,
metadata: PhotoUploadMetadata = PhotoUploadMetadata(),
): PhotoUploadResponse {
val resolver = context.contentResolver val resolver = context.contentResolver
val mimeType = resolver.getType(uri) ?: "image/jpeg" val mimeType = resolver.getType(uri) ?: "image/jpeg"
val bytes = resolver.openInputStream(uri)?.use { it.readBytes() } ?: error("Nie mozna odczytac zdjecia.") val bytes = resolver.openInputStream(uri)?.use { it.readBytes() } ?: error("Nie mozna odczytac zdjecia.")
val body = bytes.toRequestBody(mimeType.toMediaTypeOrNull()) val body = bytes.toRequestBody(mimeType.toMediaTypeOrNull())
val photo = MultipartBody.Part.createFormData("photo", "ladunek.jpg", body) val photo = MultipartBody.Part.createFormData("photo", "ladunek.jpg", body)
val sourceBody = source.toRequestBody("text/plain".toMediaTypeOrNull()) val sourceBody = source.toRequestBody("text/plain".toMediaTypeOrNull())
val metadataParts = metadata.toMultipartTextParts()
return api.uploadPhoto(authHeader(requireToken()), routeId, photo, sourceBody) return api.uploadPhoto(
authHeader(requireToken()),
routeId,
photo,
sourceBody,
metadataParts["takenAt"],
metadataParts["latitude"],
metadataParts["longitude"],
metadataParts["locationAccuracyMeters"],
)
}
suspend fun deletePhoto(photoId: String) {
api.deletePhoto(authHeader(requireToken()), photoId)
} }
suspend fun logout() { suspend fun logout() {
@@ -0,0 +1,21 @@
package pl.firmatpp.kierowca.data
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
data class PhotoUploadMetadata(
val takenAt: String? = null,
val latitude: Double? = null,
val longitude: Double? = null,
val locationAccuracyMeters: Double? = null,
)
fun PhotoUploadMetadata.toMultipartTextParts(): Map<String, RequestBody> = buildMap {
takenAt?.let { put("takenAt", it.toPlainTextBody()) }
latitude?.let { put("latitude", it.toString().toPlainTextBody()) }
longitude?.let { put("longitude", it.toString().toPlainTextBody()) }
locationAccuracyMeters?.let { put("locationAccuracyMeters", it.toString().toPlainTextBody()) }
}
private fun String.toPlainTextBody(): RequestBody = toRequestBody("text/plain".toMediaTypeOrNull())
@@ -6,11 +6,13 @@ import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import java.util.UUID
private val Context.driverDataStore by preferencesDataStore(name = "driver_session") private val Context.driverDataStore by preferencesDataStore(name = "driver_session")
class TokenStore(private val context: Context) { class TokenStore(private val context: Context) {
private val tokenKey = stringPreferencesKey("access_token") private val tokenKey = stringPreferencesKey("access_token")
private val deviceIdKey = stringPreferencesKey("device_id")
suspend fun save(token: String) { suspend fun save(token: String) {
context.driverDataStore.edit { preferences -> context.driverDataStore.edit { preferences ->
@@ -22,6 +24,21 @@ class TokenStore(private val context: Context) {
.map { it[tokenKey] } .map { it[tokenKey] }
.first() .first()
suspend fun deviceId(): String {
val existing = context.driverDataStore.data
.map { it[deviceIdKey] }
.first()
if (!existing.isNullOrBlank()) return existing
val generated = UUID.randomUUID().toString()
context.driverDataStore.edit { preferences ->
preferences[deviceIdKey] = generated
}
return generated
}
suspend fun clear() { suspend fun clear() {
context.driverDataStore.edit { it.remove(tokenKey) } context.driverDataStore.edit { it.remove(tokenKey) }
} }
@@ -10,12 +10,14 @@ import pl.firmatpp.kierowca.data.model.RouteResponse
import pl.firmatpp.kierowca.data.model.VerifyOtpBody import pl.firmatpp.kierowca.data.model.VerifyOtpBody
import pl.firmatpp.kierowca.data.model.VerifyOtpResponse import pl.firmatpp.kierowca.data.model.VerifyOtpResponse
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Header import retrofit2.http.Header
import retrofit2.http.Multipart import retrofit2.http.Multipart
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.Part import retrofit2.http.Part
import retrofit2.http.Path import retrofit2.http.Path
import retrofit2.http.Query
interface MobileDriverApi { interface MobileDriverApi {
@POST("mobile/driver/auth/request-otp") @POST("mobile/driver/auth/request-otp")
@@ -28,7 +30,10 @@ interface MobileDriverApi {
suspend fun logout(@Header("Authorization") authorization: String): Map<String, Boolean> suspend fun logout(@Header("Authorization") authorization: String): Map<String, Boolean>
@GET("mobile/driver/bootstrap") @GET("mobile/driver/bootstrap")
suspend fun bootstrap(@Header("Authorization") authorization: String): BootstrapResponse suspend fun bootstrap(
@Header("Authorization") authorization: String,
@Query("date") date: String?,
): BootstrapResponse
@GET("mobile/driver/routes/{routeId}") @GET("mobile/driver/routes/{routeId}")
suspend fun route( suspend fun route(
@@ -43,5 +48,15 @@ interface MobileDriverApi {
@Path("routeId") routeId: String, @Path("routeId") routeId: String,
@Part photo: MultipartBody.Part, @Part photo: MultipartBody.Part,
@Part("source") source: RequestBody, @Part("source") source: RequestBody,
@Part("takenAt") takenAt: RequestBody?,
@Part("latitude") latitude: RequestBody?,
@Part("longitude") longitude: RequestBody?,
@Part("locationAccuracyMeters") locationAccuracyMeters: RequestBody?,
): PhotoUploadResponse ): PhotoUploadResponse
@DELETE("mobile/driver/photos/{photoId}")
suspend fun deletePhoto(
@Header("Authorization") authorization: String,
@Path("photoId") photoId: String,
): Map<String, Boolean>
} }
@@ -6,6 +6,21 @@ data class VerifyOtpBody(
val phoneNumber: String, val phoneNumber: String,
val code: String, val code: String,
val deviceName: String, val deviceName: String,
val device: DriverDeviceInfo,
)
data class DriverDeviceInfo(
val deviceId: String,
val androidId: String?,
val brand: String?,
val manufacturer: String?,
val model: String?,
val device: String?,
val product: String?,
val androidVersion: String?,
val sdkInt: Int,
val appVersion: String?,
val serialNumber: String?,
) )
data class OtpResponse( data class OtpResponse(
@@ -30,6 +45,17 @@ data class DriverDto(
data class BootstrapResponse( data class BootstrapResponse(
val session: DriverSessionDto, val session: DriverSessionDto,
val routes: RoutesBucketDto, val routes: RoutesBucketDto,
val driverAppSettings: DriverAppSettingsDto?,
)
data class DriverAppSettingsDto(
val minDate: String,
val maxDate: String,
val today: String,
val selectedDate: String,
val maxPastDays: Int,
val autoRefreshSeconds: Int,
val allowGalleryUploads: Boolean?,
) )
data class DriverSessionDto( data class DriverSessionDto(
@@ -58,15 +84,25 @@ data class DriverRouteDto(
val distanceKm: Double?, val distanceKm: Double?,
val notes: String?, val notes: String?,
val truck: TruckDto?, val truck: TruckDto?,
val originNavigation: NavigationPointDto? = null,
val destinationNavigation: NavigationPointDto? = null,
val photos: List<RoutePhotoDto> = emptyList(), val photos: List<RoutePhotoDto> = emptyList(),
) )
data class TruckDto( data class TruckDto(
val id: String, val id: String,
val registration: String?, val registration: String?,
val trailerRegistration: String? = null,
val name: String?, val name: String?,
) )
data class NavigationPointDto(
val id: String,
val label: String?,
val latitude: Double?,
val longitude: Double?,
)
data class RoutePhotoDto( data class RoutePhotoDto(
val id: String, val id: String,
val routeId: String, val routeId: String,
@@ -74,7 +110,13 @@ data class RoutePhotoDto(
val mimeType: String?, val mimeType: String?,
val size: Long, val size: Long,
val url: String, val url: String,
val takenAt: String?,
val latitude: Double?,
val longitude: Double?,
val locationAccuracyMeters: Double?,
val createdAt: String?, val createdAt: String?,
val canDelete: Boolean = false,
val deleteUntil: String? = null,
) )
data class PhotoUploadResponse( data class PhotoUploadResponse(
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,6 @@
package pl.firmatpp.kierowca.ui
import java.time.LocalDate
fun canManageRoutePhotos(selectedDate: String, today: LocalDate = LocalDate.now()): Boolean =
runCatching { LocalDate.parse(selectedDate).isEqual(today) }.getOrDefault(false)
@@ -8,21 +8,29 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.LocalDate
import pl.firmatpp.kierowca.data.DriverRepository import pl.firmatpp.kierowca.data.DriverRepository
import pl.firmatpp.kierowca.data.PhotoUploadMetadata
import pl.firmatpp.kierowca.data.model.DriverDto import pl.firmatpp.kierowca.data.model.DriverDto
import pl.firmatpp.kierowca.data.model.DriverRouteDto import pl.firmatpp.kierowca.data.model.DriverRouteDto
import pl.firmatpp.kierowca.data.model.RoutePhotoDto import pl.firmatpp.kierowca.data.model.RoutePhotoDto
enum class DriverScreen { Phone, Otp, Routes, Detail, Photo } enum class DriverScreen { Phone, Otp, Routes, Profile, Detail, Photo }
data class DriverUiState( data class DriverUiState(
val screen: DriverScreen = DriverScreen.Phone, val screen: DriverScreen = DriverScreen.Phone,
val loading: Boolean = false, val loading: Boolean = false,
val refreshing: Boolean = false,
val phone: String = "", val phone: String = "",
val otpCode: String = "", val otpCode: String = "",
val maskedPhone: String = "", val maskedPhone: String = "",
val driver: DriverDto? = null, val driver: DriverDto? = null,
val routes: List<DriverRouteDto> = emptyList(), val routes: List<DriverRouteDto> = emptyList(),
val selectedDate: String = LocalDate.now().toString(),
val minRouteDate: String = LocalDate.now().minusDays(7).toString(),
val maxRouteDate: String = LocalDate.now().plusDays(1).toString(),
val autoRefreshSeconds: Int = 60,
val allowGalleryUploads: Boolean = true,
val selectedRoute: DriverRouteDto? = null, val selectedRoute: DriverRouteDto? = null,
val selectedPhoto: RoutePhotoDto? = null, val selectedPhoto: RoutePhotoDto? = null,
val imageAuthHeader: String? = null, val imageAuthHeader: String? = null,
@@ -32,6 +40,7 @@ data class DriverUiState(
class DriverViewModel(application: Application) : AndroidViewModel(application) { class DriverViewModel(application: Application) : AndroidViewModel(application) {
private val repository = DriverRepository(application) private val repository = DriverRepository(application)
private val _state = MutableStateFlow(DriverUiState(loading = true)) private val _state = MutableStateFlow(DriverUiState(loading = true))
private val otpAutoSubmitPolicy = OtpAutoSubmitPolicy()
val state: StateFlow<DriverUiState> = _state val state: StateFlow<DriverUiState> = _state
init { init {
@@ -42,6 +51,7 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
fun requestOtp(phone: String) = runLoading { fun requestOtp(phone: String) = runLoading {
val response = repository.requestOtp(phone) val response = repository.requestOtp(phone)
otpAutoSubmitPolicy.reset()
_state.update { _state.update {
it.copy( it.copy(
screen = DriverScreen.Otp, screen = DriverScreen.Otp,
@@ -60,19 +70,48 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
} }
fun updateOtpCode(code: String) { fun updateOtpCode(code: String) {
_state.update { it.copy(otpCode = code.filter(Char::isDigit).take(10)) } val sanitized = code.filter(Char::isDigit).take(6)
_state.update { it.copy(otpCode = sanitized) }
if (otpAutoSubmitPolicy.shouldSubmit(sanitized, _state.value.loading)) {
verifyOtp(sanitized)
}
} }
fun refreshRoutes() = runLoading { fun refreshRoutes() = loadRoutes(date = _state.value.selectedDate, showLoading = true, navigateToRoutes = true)
val response = repository.bootstrap()
_state.update { fun refreshRoutesSilently() = loadRoutes(date = _state.value.selectedDate, showLoading = false, navigateToRoutes = false)
it.copy(
screen = DriverScreen.Routes, fun selectRouteDate(date: String) = loadRoutes(date = date, showLoading = true, navigateToRoutes = true)
driver = response.session.driver,
routes = response.routes.today, private fun loadRoutes(date: String?, showLoading: Boolean, navigateToRoutes: Boolean) {
imageAuthHeader = repository.imageAuthHeader(), viewModelScope.launch {
error = null, _state.update { it.copy(loading = showLoading, refreshing = !showLoading, error = null) }
)
runCatching {
val response = repository.bootstrap(date)
val settings = response.driverAppSettings
_state.update {
it.copy(
screen = if (navigateToRoutes) DriverScreen.Routes else it.screen,
driver = response.session.driver,
routes = response.routes.today,
selectedDate = settings?.selectedDate ?: date ?: it.selectedDate,
minRouteDate = settings?.minDate ?: it.minRouteDate,
maxRouteDate = settings?.maxDate ?: it.maxRouteDate,
autoRefreshSeconds = settings?.autoRefreshSeconds ?: it.autoRefreshSeconds,
allowGalleryUploads = settings?.allowGalleryUploads ?: it.allowGalleryUploads,
imageAuthHeader = repository.imageAuthHeader(),
error = null,
)
}
}.onFailure { throwable ->
_state.update { it.copy(error = throwable.message ?: "Wystapil blad.") }
}
_state.update {
it.copy(loading = false, refreshing = false)
}
} }
} }
@@ -81,9 +120,43 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
_state.update { it.copy(screen = DriverScreen.Detail, selectedRoute = response.route, error = null) } _state.update { it.copy(screen = DriverScreen.Detail, selectedRoute = response.route, error = null) }
} }
fun uploadPhoto(uri: Uri, source: String) = runLoading { fun refreshSelectedRoute() {
val routeId = _state.value.selectedRoute?.id ?: return
viewModelScope.launch {
_state.update { it.copy(refreshing = true, error = null) }
runCatching { repository.route(routeId) }
.onSuccess { response ->
_state.update {
it.copy(
selectedRoute = response.route,
routes = it.routes.map { route -> if (route.id == routeId) response.route else route },
error = null,
)
}
}
.onFailure { throwable ->
_state.update { it.copy(error = throwable.message ?: "Wystapil blad.") }
}
_state.update { it.copy(refreshing = false) }
}
}
fun openProfile() {
_state.update { it.copy(screen = DriverScreen.Profile, error = null) }
}
fun uploadPhoto(uri: Uri, source: String, metadata: PhotoUploadMetadata = PhotoUploadMetadata()) = runLoading {
val route = _state.value.selectedRoute ?: return@runLoading val route = _state.value.selectedRoute ?: return@runLoading
repository.uploadPhoto(route.id, uri, source) repository.uploadPhoto(route.id, uri, source, metadata)
openRoute(route.id)
}
fun deletePhoto(photo: RoutePhotoDto) = runLoading {
val route = _state.value.selectedRoute ?: return@runLoading
repository.deletePhoto(photo.id)
openRoute(route.id) openRoute(route.id)
} }
@@ -96,6 +169,7 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
when (it.screen) { when (it.screen) {
DriverScreen.Photo -> it.copy(screen = DriverScreen.Detail, selectedPhoto = null) DriverScreen.Photo -> it.copy(screen = DriverScreen.Detail, selectedPhoto = null)
DriverScreen.Detail -> it.copy(screen = DriverScreen.Routes, selectedRoute = null) DriverScreen.Detail -> it.copy(screen = DriverScreen.Routes, selectedRoute = null)
DriverScreen.Profile -> it.copy(screen = DriverScreen.Routes)
DriverScreen.Otp -> it.copy(screen = DriverScreen.Phone) DriverScreen.Otp -> it.copy(screen = DriverScreen.Phone)
else -> it else -> it
} }
@@ -0,0 +1,27 @@
package pl.firmatpp.kierowca.ui
class OtpAutoSubmitPolicy {
private var lastSubmittedCode: String? = null
fun shouldSubmit(code: String, loading: Boolean): Boolean {
if (code.length < OTP_LENGTH) {
lastSubmittedCode = null
return false
}
if (loading || code.length != OTP_LENGTH || code == lastSubmittedCode) {
return false
}
lastSubmittedCode = code
return true
}
fun reset() {
lastSubmittedCode = null
}
private companion object {
const val OTP_LENGTH = 6
}
}
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#F7FAF8" />
</shape>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1010 KiB

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">TPP Kierowca</string> <string name="app_name">Firma TPP - Kierowca</string>
</resources> </resources>
@@ -0,0 +1,38 @@
package pl.firmatpp.kierowca.data
import okio.Buffer
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Test
class PhotoUploadMetadataTest {
@Test
fun convertsPresentCameraMetadataToMultipartTextParts() {
val parts = PhotoUploadMetadata(
takenAt = "2026-06-30T07:38:12Z",
latitude = 50.0619474,
longitude = 19.9368564,
locationAccuracyMeters = 8.5,
).toMultipartTextParts()
assertEquals("2026-06-30T07:38:12Z", parts.getValue("takenAt").readUtf8())
assertEquals("50.0619474", parts.getValue("latitude").readUtf8())
assertEquals("19.9368564", parts.getValue("longitude").readUtf8())
assertEquals("8.5", parts.getValue("locationAccuracyMeters").readUtf8())
}
@Test
fun skipsLocationPartsWhenMetadataIsEmpty() {
val parts = PhotoUploadMetadata().toMultipartTextParts()
assertFalse(parts.containsKey("latitude"))
assertFalse(parts.containsKey("longitude"))
assertFalse(parts.containsKey("locationAccuracyMeters"))
}
private fun okhttp3.RequestBody.readUtf8(): String {
val buffer = Buffer()
writeTo(buffer)
return buffer.readUtf8()
}
}
@@ -0,0 +1,23 @@
package pl.firmatpp.kierowca.ui
import java.time.LocalDate
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class DriverUiRulesTest {
private val today = LocalDate.parse("2026-06-30")
@Test
fun allowsPhotoActionsOnlyForTodayRouteDate() {
assertTrue(canManageRoutePhotos("2026-06-30", today))
assertFalse(canManageRoutePhotos("2026-06-29", today))
assertFalse(canManageRoutePhotos("2026-07-01", today))
}
@Test
fun blocksPhotoActionsWhenDateIsInvalid() {
assertFalse(canManageRoutePhotos("", today))
assertFalse(canManageRoutePhotos("not-a-date", today))
}
}
@@ -0,0 +1,41 @@
package pl.firmatpp.kierowca.ui
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class OtpAutoSubmitPolicyTest {
@Test
fun submitsWhenSixthDigitIsEntered() {
val policy = OtpAutoSubmitPolicy()
assertFalse(policy.shouldSubmit("12345", loading = false))
assertTrue(policy.shouldSubmit("123456", loading = false))
}
@Test
fun submitsSmsRetrieverCodeOnlyOnceWhileItStaysTheSame() {
val policy = OtpAutoSubmitPolicy()
assertTrue(policy.shouldSubmit("987654", loading = false))
assertFalse(policy.shouldSubmit("987654", loading = false))
}
@Test
fun allowsRetryAfterCodeIsEditedBelowSixDigits() {
val policy = OtpAutoSubmitPolicy()
assertTrue(policy.shouldSubmit("111111", loading = false))
assertFalse(policy.shouldSubmit("111111", loading = false))
assertFalse(policy.shouldSubmit("11111", loading = false))
assertTrue(policy.shouldSubmit("111111", loading = false))
}
@Test
fun doesNotSubmitWhileLoadingOrForLongerCodes() {
val policy = OtpAutoSubmitPolicy()
assertFalse(policy.shouldSubmit("123456", loading = true))
assertFalse(policy.shouldSubmit("1234567", loading = false))
}
}
+27
View File
@@ -0,0 +1,27 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = resolve(__dirname, '..');
function readSource(path) {
return readFileSync(resolve(root, path), 'utf8');
}
test('driver app sends Android device metadata during OTP verification', () => {
const models = readSource('app/src/main/java/pl/firmatpp/kierowca/data/model/DriverModels.kt');
const repository = readSource('app/src/main/java/pl/firmatpp/kierowca/data/DriverRepository.kt');
const deviceInfo = readSource('app/src/main/java/pl/firmatpp/kierowca/data/DeviceInfoProvider.kt');
assert.match(models, /data class DriverDeviceInfo/);
assert.match(models, /val device: DriverDeviceInfo/);
assert.match(repository, /DeviceInfoProvider/);
assert.match(repository, /deviceInfoProvider\.currentDeviceInfo/);
assert.match(deviceInfo, /Settings\.Secure\.ANDROID_ID/);
assert.match(deviceInfo, /Build\.MANUFACTURER/);
assert.match(deviceInfo, /Build\.MODEL/);
assert.match(deviceInfo, /BuildConfig\.VERSION_NAME/);
});