Compare commits

..
93 Commits
Author SHA1 Message Date
admin ea44cf5d2e Wydaj aplikację kierowcy 1.0.69 (122) 2026-07-24 01:30:07 +02:00
admin 984feb170a Wydaj aplikację kierowcy 1.0.68 (121) 2026-07-23 15:14:50 +02:00
admin 5691c87e91 Dostosuj aplikację do Androida 16 2026-07-23 15:14:05 +02:00
admin 227e08a671 Wydaj aplikację kierowcy 1.0.67 (120) 2026-07-23 15:03:18 +02:00
admin 3bfb5f9788 Usuń nieaktualny konflikt zakończenia kursu 2026-07-23 15:02:21 +02:00
admin bf1cf3865a Napraw synchronizację statusu kursu kierowcy 2026-07-23 14:15:34 +02:00
admin 0c3bed6213 Dostosuj lokalizację kursu do ustawień 2026-07-23 12:51:26 +02:00
admin f1cb2d618d Wydaj aplikację kierowcy 1.0.66 (119) 2026-07-23 11:35:47 +02:00
admin 8c9a3b5b37 Aktualizuj wersję aplikacji w sesji kierowcy 2026-07-23 11:33:47 +02:00
admin de8510bc79 Wydaj aplikację kierowcy 1.0.65 (118) 2026-07-23 11:33:17 +02:00
admin 34d01b312e Obsłuż adres SSH w skrypcie wydania Androida 2026-07-23 11:19:10 +02:00
admin f89332b28c Dodaj uwagi i dyktowanie na etapach trasy 2026-07-23 11:05:49 +02:00
admin 8f12bf32bc Wydaj aplikację kierowcy 1.0.64 (117) 2026-07-18 17:37:53 +02:00
admin b9ae128842 Napraw obsługę kodów OTP podczas logowania 2026-07-18 17:33:40 +02:00
admin a86dca50b8 Wydaj aplikację kierowcy 1.0.63 (116) 2026-07-18 16:34:32 +02:00
admin fc49eae512 Zatrzymaj aktywny kurs po wygaśnięciu sesji 2026-07-18 16:29:34 +02:00
admin ffbae9edfd Wydaj aplikację kierowcy 1.0.62 (115) 2026-07-18 02:57:39 +02:00
admin 2268c64250 Zautomatyzuj publikację aplikacji w Google Play 2026-07-18 02:57:03 +02:00
admin c954fd9bb8 chore: release android driver 1.0.61 (114) 2026-07-18 02:34:10 +02:00
admin b9757348f2 Dodaj aktualizacje aplikacji kierowcy przez Google Play 2026-07-18 02:32:31 +02:00
admin 130f8bbecc chore: release android driver 1.0.60 (113) 2026-07-17 14:10:03 +02:00
admin 4c00d46a14 Napraw otwieranie nawigacji do geofence 2026-07-17 14:05:11 +02:00
admin 07fc4021e1 chore: release android driver 1.0.59 (112) 2026-07-17 13:33:46 +02:00
admin 431436d0db Napraw synchronizację kursu w aplikacji kierowcy 2026-07-17 13:28:49 +02:00
admin 905711d8de chore: release android driver 1.0.58 (111) 2026-07-16 11:06:24 +02:00
admin 7c9cb3fb0d Dodaj timeout i ręczny tryb offline przy uruchamianiu 2026-07-16 11:04:36 +02:00
admin ae0d1b2d3d chore: release android driver 1.0.57 (110) 2026-07-16 10:31:11 +02:00
admin c5b3c5dae9 Przeliczaj wagi kierowcy z kilogramów na tony 2026-07-16 10:28:30 +02:00
admin 3fa3b4eac1 chore: release android driver 1.0.56 (109) 2026-07-12 22:34:20 +02:00
admin 197bcc6f5e Przypomnij o zgodzie na powiadomienia Android 2026-07-12 22:33:52 +02:00
admin f4f5cec2be chore: release android driver 1.0.55 (108) 2026-07-12 22:04:46 +02:00
admin 5a630f8401 Użyj origin realtime z bootstrapu 2026-07-12 21:59:46 +02:00
admin 54a3c58a22 chore: release android driver 1.0.54 (107) 2026-07-12 21:57:39 +02:00
admin 1c6a044c37 Dodaj Origin dla WebSocket Reverb 2026-07-12 21:55:01 +02:00
admin fcc022db64 chore: release android driver 1.0.53 (106) 2026-07-11 00:35:40 +02:00
admin a908a2da18 fix: avoid release verification pipe failure 2026-07-11 00:35:21 +02:00
admin 2c25647fa5 chore: ignore backup Android resources 2026-07-11 00:29:47 +02:00
admin 956f33140e chore: automate Android release builds 2026-07-11 00:27:54 +02:00
admin 7bba4dc878 fix: stop orphaned GPS synchronization 2026-07-11 00:27:52 +02:00
admin 89235fbb46 feat: harden offline sync and add diagnostics 2026-07-10 23:58:07 +02:00
admin 0564101e9e chore: release android driver 1.0.51 2026-07-10 22:35:04 +02:00
admin 2baa544b32 fix: reconcile leave requests in realtime 2026-07-10 22:32:13 +02:00
admin f9c3a2e305 Sortuj kursy kierowcy według dnia 2026-07-10 21:31:17 +02:00
admin e49e7c358e Obsłuż wielodniowe kursy kierowcy 2026-07-10 14:50:38 +02:00
admin 3267fee439 Usprawnij wybór urlopu przy konfliktach 2026-07-09 00:31:06 +02:00
admin 8a0651e578 Rozdziel załadunek i dodaj kalendarz urlopów 2026-07-09 00:05:57 +02:00
admin 32d45f6430 Bump driver app version to 51 2026-07-08 02:03:33 +02:00
admin 4a06e9da8b Retry route actions after photo upload 2026-07-08 02:01:37 +02:00
admin d6b36a1d3a Fix driver route crash on null photos 2026-07-08 00:02:15 +02:00
admin e811b4bc43 Limit confirmed route actions in driver projection 2026-07-07 23:18:10 +02:00
admin 5d3b355163 Bump driver app version code to 48 2026-07-07 23:07:31 +02:00
admin 81f3b05653 Keep confirmed route actions in driver status projection 2026-07-07 23:03:58 +02:00
admin 37407e2737 Fix offline route lifecycle projection 2026-07-07 22:29:35 +02:00
admin 13159c576e Implement driver route lifecycle flow 2026-07-07 12:29:12 +02:00
admin 1834763f19 Add route detail photo tile loaders 2026-07-05 23:29:49 +02:00
admin 62c0469669 Add photo preview loading state 2026-07-05 23:14:07 +02:00
admin 3d6e1624e9 Fix leave request detail refresh navigation 2026-07-05 11:39:18 +02:00
admin b1a34105f1 Harden driver app realtime release 2026-07-05 03:15:54 +02:00
admin 72db88b25c Add Firebase Crashlytics diagnostics 2026-07-05 00:06:47 +02:00
admin 97f0eb56d5 Polish driver leave request status labels 2026-07-04 23:41:59 +02:00
admin c6d6392656 Refine driver leave request mobile flow 2026-07-04 23:26:26 +02:00
admin 958fb6eaa6 Bump Android driver app to 1.0.38 2026-07-04 23:18:42 +02:00
admin 2c806ef14e Add driver leave request screens 2026-07-04 23:11:08 +02:00
admin 0bbdbdb4f1 chore: bump Android app to 1.0.37 2026-07-04 12:06:46 +02:00
admin fe2e2e589a feat: add dispatch sheet reminder to driver app 2026-07-04 12:03:34 +02:00
admin e0115e85fc Pin live update toast over routes 2026-07-02 16:29:49 +02:00
admin 0e15d5f9a4 Show live day updates and route date chip 2026-07-02 16:24:06 +02:00
admin 5555676395 Shorten driver Reverb heartbeat 2026-07-02 16:08:24 +02:00
admin ca512f5993 Load Reverb config from mobile bootstrap 2026-07-02 15:33:42 +02:00
admin 64d6f5a41d Add new route push notifications 2026-07-02 15:25:54 +02:00
admin a3d39764e2 Report driver Reverb connection status 2026-07-02 14:58:12 +02:00
admin 705b8904a4 Reconcile confirmed photo uploads with server state 2026-07-02 14:42:52 +02:00
admin 541dda292f Add photo upload failure details 2026-07-01 22:47:13 +02:00
admin fa98451533 Add photo upload retry controls 2026-07-01 22:39:47 +02:00
admin ee9d8cdbaf Update driver app photo handling 2026-07-01 19:26:28 +02:00
admin 76326bbc27 Fix launcher icon clipping for 1.0.22 2026-07-01 14:49:11 +02:00
admin ed02da031b Refine launcher icon for 1.0.21 2026-07-01 14:44:06 +02:00
admin a4e24da717 Fix launcher icon safe area for 1.0.20 2026-07-01 14:20:54 +02:00
admin ab84205bec Update Android app icon for 1.0.18 2026-07-01 12:46:38 +02:00
admin 9ba1b1fa13 Dodaj live sync i kolejkę zdjęć 2026-07-01 10:59:21 +02:00
admin 06557e77b8 Dodaj usuwanie świeżo zapisanych zdjęć 2026-07-01 02:43:52 +02:00
admin a08fe31d3d Popraw obsługę galerii zdjęć kierowcy 2026-07-01 02:37:43 +02:00
admin 060896d12b Dodaj pewny outbox uploadu zdjęć 2026-07-01 02:28:44 +02:00
admin 1a26817d2c Dodaj ekran startowy podczas sprawdzania sesji 2026-07-01 01:24:59 +02:00
admin 693f879078 Odśwież ekran logowania i ikonę aplikacji 2026-07-01 01:11:51 +02:00
admin 0bc592f357 Automatyzuj logowanie po wpisaniu OTP 2026-07-01 00:07:26 +02:00
admin 9b6564eedd Wysyłaj dane urządzenia przy logowaniu 2026-06-30 23:57:50 +02:00
admin a66351a6f4 Ogranicz zdjęcia do dzisiejszych kursów 2026-06-30 22:44:14 +02:00
admin b034384ab2 Dodaj nawigacje i odswiezanie szczegolow trasy 2026-06-30 22:16:49 +02:00
admin 621745a93c Wysylaj lokalizacje zdjec z aparatu 2026-06-30 22:10:07 +02:00
admin bf6e76dcbf Odwzoruj szczegoly trasy ze Stitch 2026-06-30 22:00:59 +02:00
admin 18baec5020 Ukryj galerie wedlug ustawien 2026-06-30 21:52:29 +02:00
admin 2531ff2b9f Dodaj odswiezanie i wybor daty kursow 2026-06-30 21:47:10 +02:00
132 changed files with 17591 additions and 364 deletions
+8
View File
@@ -1,5 +1,13 @@
.gradle/
build/
local.properties
app/google-services.json
app/google-services.json:Zone.Identifier
*/build/
*.iml
keystore.properties
*.jks
release-artifacts/
app/src/main/res_bak/
scripts/*.json
scripts/*.json:Zone.Identifier
+62
View File
@@ -21,3 +21,65 @@ Natywna aplikacja Android Jetpack Compose dla kierowcow TPP.
```bash
./gradlew :app:testDebugUnitTest
```
## Release Gitea i Google Play
Przed wydaniem zapisz wszystkie zmiany źródłowe w commicie. Następnie uruchom:
```bash
./scripts/release-android.sh
```
Skrypt automatycznie zwiększa `versionCode` i ostatni człon `versionName`, uruchamia testy,
buduje podpisany APK oraz AAB, a po poprawnym buildzie pyta, czy wysłać aktualizację do
Google Play. Po odpowiedzi „tak” publikuje AAB w skonfigurowanym teście zamkniętym i
wysyła zmiany do weryfikacji. Następnie tworzy commit wersji, tag, push i Gitea Release.
Artefakty zostają również zapisane lokalnie w `release-artifacts/<tag>/`.
Do publikacji Google Play konto serwisowe musi mieć dla aplikacji
`pl.firmatpp.kierowca` uprawnienia „Wyświetlanie informacji o aplikacji (tylko do odczytu)”
oraz „Publikowanie aplikacji na ścieżkach testowych”. Pobrany klucz JSON umieść
bezpośrednio w folderze `scripts`, na przykład:
```text
scripts/google-play-service-account.json
```
Skrypt sam znajduje jedyny plik `scripts/*.json`, sprawdza, czy jest prawidłowym kluczem
konta serwisowego, i używa go do autoryzacji. Pliki `scripts/*.json` są ignorowane przez
Git i nie mogą zostać przypadkowo dodane do repozytorium. Jeśli w folderze znajduje się
więcej niż jeden prawidłowy klucz, skrypt przerwie działanie i poprosi o pozostawienie
jednego.
`GOOGLE_PLAY_TRACK` musi być identyfikatorem istniejącego testu zamkniętego. Domyślna
wartość to `alpha`. Opcjonalnie można ustawić `GOOGLE_PLAY_PACKAGE_NAME` i
`GOOGLE_PLAY_RELEASE_LANGUAGE` (domyślnie `pl-PL`). Zmienna
`GOOGLE_PLAY_SERVICE_ACCOUNT_CREDENTIALS` pozostaje opcjonalnym sposobem wskazania klucza
spoza folderu `scripts`. Skrypt nie zapisuje tokenu ani klucza w repozytorium; prywatny
klucz tymczasowy jest usuwany po zakończeniu.
Przed zatwierdzeniem skrypt waliduje edycję Google Play. Commit edycji jawnie wysyła
zmiany do weryfikacji. Jeżeli inne zmiany są już sprawdzane, skrypt nie anuluje ich —
kończy się błędem i można go wznowić po zakończeniu poprzedniej weryfikacji.
Jeżeli na koncie jest włączone publikowanie zarządzane, po akceptacji Google nadal trzeba
wybrać „Opublikuj zmiany” w Play Console; bez publikowania zarządzanego zaakceptowane
wydanie zostanie udostępnione testerom automatycznie.
Awaryjne wydanie wyłącznie do Gitea, bez Google Play:
```bash
./scripts/release-android.sh --skip-google-play
```
W środowisku bez interaktywnego terminala decyzję trzeba podać jawnie:
```bash
./scripts/release-android.sh --publish-google-play
```
Do autoryzacji Gitea używany jest `GITEA_TOKEN` albo dane zapisane w Git credential helper.
Jeśli publikacja została przerwana już po utworzeniu commita wersji, można ją wznowić:
```bash
./scripts/release-android.sh --reuse-current
```
+65 -4
View File
@@ -1,22 +1,65 @@
import java.io.File
import java.util.Properties
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.ksp)
}
if (file("google-services.json").exists()) {
apply(plugin = "com.google.gms.google-services")
apply(plugin = "com.google.firebase.crashlytics")
}
val keystorePropertiesFile = rootProject.file("keystore.properties")
val keystoreProperties = Properties().apply {
if (keystorePropertiesFile.exists()) {
keystorePropertiesFile.inputStream().use(::load)
}
}
android {
namespace = "pl.firmatpp.kierowca"
compileSdk = 35
compileSdk = 36
fun configValue(name: String, default: String): String =
providers.gradleProperty(name)
.orElse(providers.environmentVariable(name))
.orElse(default)
.get()
defaultConfig {
applicationId = "pl.firmatpp.kierowca"
minSdk = 26
targetSdk = 35
versionCode = 5
versionName = "1.0.4"
targetSdk = 36
versionCode = 122
versionName = "1.0.69"
setProperty("archivesBaseName", "pl.firmatpp.kierowca")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "API_BASE_URL", "\"https://api-intranet.firmatpp.pl/api/\"")
buildConfigField("String", "REVERB_APP_KEY", "\"${configValue("REVERB_APP_KEY", "")}\"")
buildConfigField("String", "REVERB_WS_BASE_URL", "\"${configValue("REVERB_WS_BASE_URL", "wss://api-intranet.firmatpp.pl/app/")}\"")
}
signingConfigs {
create("release") {
fun requiredSigningProperty(name: String): String =
keystoreProperties.getProperty(name)
?: throw GradleException("Missing '$name' in keystore.properties")
val configuredStoreFile = requiredSigningProperty("storeFile")
storeFile = if (File(configuredStoreFile).isAbsolute) {
file(configuredStoreFile)
} else {
rootProject.file(configuredStoreFile)
}
storePassword = requiredSigningProperty("storePassword")
keyAlias = requiredSigningProperty("keyAlias")
keyPassword = requiredSigningProperty("keyPassword")
}
}
buildTypes {
@@ -24,7 +67,11 @@ android {
isMinifyEnabled = false
}
release {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = false
ndk {
debugSymbolLevel = "SYMBOL_TABLE"
}
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
@@ -45,6 +92,10 @@ android {
compose = true
buildConfig = true
}
lint {
disable += "NullSafeMutableLiveData"
}
}
dependencies {
@@ -57,6 +108,8 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.room.ktx)
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.camera.camera2)
implementation(libs.camera.core)
@@ -64,15 +117,21 @@ dependencies {
implementation(libs.camera.view)
implementation(libs.coil.compose)
implementation(libs.compose.material.icons.extended)
implementation("androidx.compose.material:material")
implementation(libs.compose.material3)
implementation(libs.compose.ui)
implementation(libs.compose.ui.graphics)
implementation(libs.compose.ui.tooling.preview)
implementation(libs.coroutines.android)
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.crashlytics)
implementation(libs.firebase.messaging)
implementation(libs.okhttp)
implementation(libs.okhttp.logging)
implementation(libs.play.services.auth)
implementation(libs.play.services.auth.api.phone)
implementation(libs.play.app.update)
implementation(libs.play.app.update.ktx)
implementation(libs.retrofit)
implementation(libs.retrofit.gson)
@@ -80,4 +139,6 @@ dependencies {
testImplementation(libs.junit)
testImplementation(libs.coroutines.test)
ksp(libs.androidx.room.compiler)
}
+32
View File
@@ -2,10 +2,29 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<queries>
<intent>
<action android:name="android.speech.RecognitionService" />
</intent>
</queries>
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:name=".DriverApplication"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.TppKierowca">
<activity
@@ -26,5 +45,18 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<service
android:name=".sync.DriverFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<service
android:name=".tracking.ActiveRouteTrackingService"
android:exported="false"
android:foregroundServiceType="location" />
</application>
</manifest>
@@ -0,0 +1,26 @@
package pl.firmatpp.kierowca
import android.app.Application
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import pl.firmatpp.kierowca.data.upload.OfflineOutboxManager
import pl.firmatpp.kierowca.diagnostics.AppDiagnostics
class DriverApplication : Application() {
private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun onCreate() {
super.onCreate()
AppDiagnostics.installFirebaseCrashlytics()
AppDiagnostics.log("app_started")
applicationScope.launch {
val outboxManager = OfflineOutboxManager(this@DriverApplication)
runCatching { outboxManager.repairOperationalQueues() }
.onFailure { AppDiagnostics.reportNonFatal(it, "repair_operational_queues") }
runCatching { outboxManager.repairGpsQueue() }
.onFailure { AppDiagnostics.reportNonFatal(it, "repair_gps_queue") }
}
}
}
@@ -3,19 +3,88 @@ package pl.firmatpp.kierowca
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import pl.firmatpp.kierowca.ui.DriverApp
import pl.firmatpp.kierowca.ui.DriverViewModel
import pl.firmatpp.kierowca.ui.theme.TppKierowcaTheme
import pl.firmatpp.kierowca.update.PlayAppUpdateState
import pl.firmatpp.kierowca.update.PlayInAppUpdateController
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
TppKierowcaTheme {
val viewModel: DriverViewModel = viewModel()
DriverApp(viewModel = viewModel)
}
private var notificationRouteId by mutableStateOf<String?>(null)
private var notificationRouteTarget by mutableStateOf<String?>(null)
private var notificationLeaveRequestId by mutableStateOf<String?>(null)
private var playAppUpdateState by mutableStateOf(PlayAppUpdateState())
private lateinit var playInAppUpdates: PlayInAppUpdateController
private val appUpdateLauncher = registerForActivityResult(
ActivityResultContracts.StartIntentSenderForResult(),
) { result ->
if (::playInAppUpdates.isInitialized) {
playInAppUpdates.onUpdateFlowResult(result.resultCode)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
notificationRouteId = intent.getStringExtra(EXTRA_ROUTE_ID)
notificationRouteTarget = intent.getStringExtra(EXTRA_ROUTE_TARGET)
notificationLeaveRequestId = intent.getStringExtra(EXTRA_LEAVE_REQUEST_ID)
playInAppUpdates = PlayInAppUpdateController(
activity = this,
launcher = appUpdateLauncher,
onStateChanged = { playAppUpdateState = it },
)
setContent {
val viewModel: DriverViewModel = viewModel()
val state by viewModel.state.collectAsStateWithLifecycle()
TppKierowcaTheme(themeMode = state.themeMode) {
DriverApp(
viewModel = viewModel,
initialRouteId = notificationRouteId,
initialRouteTarget = notificationRouteTarget,
initialLeaveRequestId = notificationLeaveRequestId,
playAppUpdateState = playAppUpdateState,
onStartAppUpdate = playInAppUpdates::startUpdate,
onCompleteAppUpdate = playInAppUpdates::completeDownloadedUpdate,
)
}
}
playInAppUpdates.checkForUpdate()
}
override fun onResume() {
super.onResume()
if (::playInAppUpdates.isInitialized) {
playInAppUpdates.checkForUpdate()
}
}
override fun onDestroy() {
if (::playInAppUpdates.isInitialized) {
playInAppUpdates.dispose()
}
super.onDestroy()
}
override fun onNewIntent(intent: android.content.Intent) {
super.onNewIntent(intent)
setIntent(intent)
notificationRouteId = intent.getStringExtra(EXTRA_ROUTE_ID)
notificationRouteTarget = intent.getStringExtra(EXTRA_ROUTE_TARGET)
notificationLeaveRequestId = intent.getStringExtra(EXTRA_LEAVE_REQUEST_ID)
}
companion object {
const val EXTRA_ROUTE_ID = "pl.firmatpp.kierowca.EXTRA_ROUTE_ID"
const val EXTRA_ROUTE_TARGET = "pl.firmatpp.kierowca.EXTRA_ROUTE_TARGET"
const val EXTRA_LEAVE_REQUEST_ID = "pl.firmatpp.kierowca.EXTRA_LEAVE_REQUEST_ID"
const val ROUTE_TARGET_DETAIL = "detail"
const val ROUTE_TARGET_FINISH = "finish"
}
}
@@ -0,0 +1,125 @@
package pl.firmatpp.kierowca.data
import java.io.IOException
import java.net.UnknownHostException
import kotlin.coroutines.cancellation.CancellationException
import retrofit2.HttpException
enum class ApiErrorKind {
Network,
Auth,
Forbidden,
Validation,
Conflict,
RateLimited,
Server,
Unknown,
}
data class ApiError(
val kind: ApiErrorKind,
val message: String,
val retryable: Boolean,
val code: String? = null,
val statusCode: Int? = null,
val responseBody: String? = null,
)
object ApiErrorMapper {
fun map(throwable: Throwable): ApiError =
when (throwable) {
is UnknownHostException -> ApiError(
kind = ApiErrorKind.Network,
message = "Brak internetu lub połączenia z serwerem. Sprawdź zasięg i spróbuj ponownie.",
retryable = true,
)
is IOException -> ApiError(
kind = ApiErrorKind.Network,
message = "Nie udało się połączyć z serwerem. Operacja nie została potwierdzona.",
retryable = true,
)
is HttpException -> mapHttpException(throwable)
else -> ApiError(
kind = ApiErrorKind.Unknown,
message = throwable.message ?: "Wystąpił błąd. Operacja nie została potwierdzona.",
retryable = false,
)
}
fun shouldReportNonFatal(throwable: Throwable): Boolean =
map(throwable).kind != ApiErrorKind.Network
fun mapHttpStatus(statusCode: Int, body: String?): ApiError {
val code = body?.let {
""""code"\s*:\s*"([^"]+)"""".toRegex().find(it)?.groupValues?.getOrNull(1)
?: """"reason"\s*:\s*"([^"]+)"""".toRegex().find(it)?.groupValues?.getOrNull(1)
}
val message = body?.let { """"message"\s*:\s*"([^"]+)"""".toRegex().find(it)?.groupValues?.getOrNull(1) }
val retryable = body?.let { """"retryable"\s*:\s*(true|false)""".toRegex().find(it)?.groupValues?.getOrNull(1)?.toBooleanStrictOrNull() }
return mapProblem(
statusCode = statusCode,
code = code,
message = message,
retryable = retryable,
responseBody = body,
)
}
fun mapProblem(
statusCode: Int,
code: String?,
message: String?,
retryable: Boolean?,
responseBody: String? = null,
): ApiError {
val kind = when (statusCode) {
401 -> ApiErrorKind.Auth
403 -> ApiErrorKind.Forbidden
400, 422 -> ApiErrorKind.Validation
409 -> ApiErrorKind.Conflict
429 -> ApiErrorKind.RateLimited
in 500..599 -> ApiErrorKind.Server
else -> ApiErrorKind.Unknown
}
val defaultRetryable = statusCode == 429 || statusCode in 500..599
val resolvedRetryable = retryable ?: defaultRetryable
val resolvedMessage = when {
code == "INVALID_OTP" -> "Kod jest nieprawidłowy lub wygasł. Użyj kodu z najnowszego SMS-a."
kind == ApiErrorKind.RateLimited -> "Za dużo prób. Odczekaj chwilę i spróbuj ponownie."
!message.isNullOrBlank() -> message
else -> when (kind) {
ApiErrorKind.Auth -> "Sesja wygasła. Zaloguj się ponownie."
ApiErrorKind.Forbidden -> "Brak uprawnień do tej operacji."
ApiErrorKind.Validation -> "Serwer odrzucił operację. Nie została zapisana."
ApiErrorKind.Conflict -> "Operacja jest w konflikcie z aktualnym stanem danych."
ApiErrorKind.RateLimited -> "Za dużo prób. Odczekaj chwilę i spróbuj ponownie."
ApiErrorKind.Server -> "Serwer nie potwierdził operacji. Aplikacja spróbuje ponownie."
ApiErrorKind.Network -> "Nie udało się połączyć z serwerem. Operacja nie została potwierdzona."
ApiErrorKind.Unknown -> "Wystąpił błąd. Operacja nie została potwierdzona."
}
}
return ApiError(
kind = kind,
message = resolvedMessage,
retryable = resolvedRetryable,
code = code,