Compare commits

..
Author SHA1 Message Date
admin fda4b462dc Wydaj aplikację kierowcy 1.0.70 (123) 2026-07-24 12:55:12 +02:00
admin 6a1a15ade9 Add route date calendar picker 2026-07-24 12:49:32 +02:00
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
11 changed files with 211 additions and 26 deletions
+4 -4
View File
@@ -22,7 +22,7 @@ val keystoreProperties = Properties().apply {
android { android {
namespace = "pl.firmatpp.kierowca" namespace = "pl.firmatpp.kierowca"
compileSdk = 35 compileSdk = 36
fun configValue(name: String, default: String): String = fun configValue(name: String, default: String): String =
providers.gradleProperty(name) providers.gradleProperty(name)
@@ -33,9 +33,9 @@ android {
defaultConfig { defaultConfig {
applicationId = "pl.firmatpp.kierowca" applicationId = "pl.firmatpp.kierowca"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 36
versionCode = 120 versionCode = 123
versionName = "1.0.67" versionName = "1.0.70"
setProperty("archivesBaseName", "pl.firmatpp.kierowca") setProperty("archivesBaseName", "pl.firmatpp.kierowca")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+4
View File
@@ -9,6 +9,10 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<queries> <queries>
<intent> <intent>
<action android:name="android.speech.RecognitionService" /> <action android:name="android.speech.RecognitionService" />
@@ -22,17 +22,6 @@ class DeviceInfoProvider(
androidVersion = Build.VERSION.RELEASE, androidVersion = Build.VERSION.RELEASE,
sdkInt = Build.VERSION.SDK_INT, sdkInt = Build.VERSION.SDK_INT,
appVersion = BuildConfig.VERSION_NAME, appVersion = BuildConfig.VERSION_NAME,
serialNumber = readSerialNumber(), serialNumber = null,
) )
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) }
} }
@@ -229,18 +229,28 @@ class DiagnosticSnapshotProvider(private val context: Context) {
val validated = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && val validated = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
val links = activeNetwork?.let(manager::getLinkProperties) val links = activeNetwork?.let(manager::getLinkProperties)
val roaming = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
yesNo(!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING))
} else {
"niedostępne"
}
val mtu = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
links?.mtu?.toString() ?: "brak"
} else {
"niedostępne"
}
return listOf( return listOf(
DiagnosticEntry("Transport", transports.joinToString()), DiagnosticEntry("Transport", transports.joinToString()),
DiagnosticEntry("Internet zweryfikowany", yesNo(validated), if (validated) DiagnosticSeverity.Good else DiagnosticSeverity.Error), DiagnosticEntry("Internet zweryfikowany", yesNo(validated), if (validated) DiagnosticSeverity.Good else DiagnosticSeverity.Error),
DiagnosticEntry("NET_CAPABILITY_INTERNET", yesNo(capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET))), DiagnosticEntry("NET_CAPABILITY_INTERNET", yesNo(capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET))),
DiagnosticEntry("NET_CAPABILITY_VALIDATED", yesNo(capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED))), DiagnosticEntry("NET_CAPABILITY_VALIDATED", yesNo(capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED))),
DiagnosticEntry("Sieć mierzona", yesNo(!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED))), DiagnosticEntry("Sieć mierzona", yesNo(!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED))),
DiagnosticEntry("Roaming", yesNo(!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING))), DiagnosticEntry("Roaming", roaming),
DiagnosticEntry("Pasmo downstream / upstream", "${capabilities.linkDownstreamBandwidthKbps} / ${capabilities.linkUpstreamBandwidthKbps} kb/s"), DiagnosticEntry("Pasmo downstream / upstream", "${capabilities.linkDownstreamBandwidthKbps} / ${capabilities.linkUpstreamBandwidthKbps} kb/s"),
DiagnosticEntry("Interfejs", links?.interfaceName ?: "brak"), DiagnosticEntry("Interfejs", links?.interfaceName ?: "brak"),
DiagnosticEntry("Adresy IP", links?.linkAddresses?.joinToString { it.toString() }?.ifBlank { "brak" } ?: "brak"), DiagnosticEntry("Adresy IP", links?.linkAddresses?.joinToString { it.toString() }?.ifBlank { "brak" } ?: "brak"),
DiagnosticEntry("DNS", links?.dnsServers?.joinToString { it.hostAddress ?: it.toString() }?.ifBlank { "brak" } ?: "brak"), DiagnosticEntry("DNS", links?.dnsServers?.joinToString { it.hostAddress ?: it.toString() }?.ifBlank { "brak" } ?: "brak"),
DiagnosticEntry("MTU", links?.mtu?.toString() ?: "brak"), DiagnosticEntry("MTU", mtu),
DiagnosticEntry("Proxy", links?.httpProxy?.toString() ?: "brak"), DiagnosticEntry("Proxy", links?.httpProxy?.toString() ?: "brak"),
) )
} }
@@ -57,7 +57,11 @@ class LeaveRequestDecisionNotificationWorker(
.setPriority(NotificationCompat.PRIORITY_DEFAULT) .setPriority(NotificationCompat.PRIORITY_DEFAULT)
.build() .build()
NotificationManagerCompat.from(applicationContext).notify(leaveRequestId.hashCode(), notification) try {
NotificationManagerCompat.from(applicationContext).notify(leaveRequestId.hashCode(), notification)
} catch (_: SecurityException) {
return Result.success()
}
return Result.success() return Result.success()
} }
@@ -73,7 +73,11 @@ class NewRouteNotificationWorker(
.setPriority(NotificationCompat.PRIORITY_DEFAULT) .setPriority(NotificationCompat.PRIORITY_DEFAULT)
.build() .build()
NotificationManagerCompat.from(applicationContext).notify(route.id.hashCode(), notification) try {
NotificationManagerCompat.from(applicationContext).notify(route.id.hashCode(), notification)
} catch (_: SecurityException) {
return@runCatching Result.success()
}
Result.success() Result.success()
}.getOrElse { throwable -> }.getOrElse { throwable ->
throwable.rethrowIfCancellation() throwable.rethrowIfCancellation()
@@ -1,6 +1,7 @@
package pl.firmatpp.kierowca.tracking package pl.firmatpp.kierowca.tracking
import android.Manifest import android.Manifest
import android.annotation.SuppressLint
import android.app.Notification import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
@@ -160,6 +161,7 @@ class ActiveRouteTrackingService : Service(), LocationListener {
super.onDestroy() super.onDestroy()
} }
@SuppressLint("MissingPermission")
private fun requestLocationUpdates() { private fun requestLocationUpdates() {
if (!hasLocationPermission() || routeId.isNullOrBlank() || driverId.isNullOrBlank()) { if (!hasLocationPermission() || routeId.isNullOrBlank() || driverId.isNullOrBlank()) {
return return
@@ -84,6 +84,8 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -93,11 +95,13 @@ import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SelectableDates
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
@@ -145,6 +149,7 @@ import java.time.DayOfWeek
import java.time.LocalDate import java.time.LocalDate
import java.time.YearMonth import java.time.YearMonth
import java.time.ZoneId import java.time.ZoneId
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
@@ -1159,6 +1164,7 @@ private fun RoutesScreen(
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var cameraUri by remember { mutableStateOf<Uri?>(null) } var cameraUri by remember { mutableStateOf<Uri?>(null) }
var showPreciseLocationPermissionDialog by remember { mutableStateOf(false) } var showPreciseLocationPermissionDialog by remember { mutableStateOf(false) }
var showRouteDatePicker by remember { mutableStateOf(false) }
val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { ok -> val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { ok ->
val capturedUri = cameraUri val capturedUri = cameraUri
if ( if (
@@ -1239,7 +1245,10 @@ private fun RoutesScreen(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = TppTheme.colors.ink, color = TppTheme.colors.ink,
) )
DateChip(state.selectedDate) DateChip(
date = state.selectedDate,
onClick = { showRouteDatePicker = true },
)
} }
} else { } else {
Row( Row(
@@ -1253,7 +1262,10 @@ private fun RoutesScreen(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = TppTheme.colors.ink, color = TppTheme.colors.ink,
) )
DateChip(state.selectedDate) DateChip(
date = state.selectedDate,
onClick = { showRouteDatePicker = true },
)
} }
} }
} }
@@ -1333,6 +1345,19 @@ private fun RoutesScreen(
}, },
) )
} }
if (showRouteDatePicker) {
RouteDatePickerDialog(
selectedDate = state.selectedDate,
minDate = state.minRouteDate,
maxDate = state.maxRouteDate,
onDismiss = { showRouteDatePicker = false },
onDateSelected = { date ->
showRouteDatePicker = false
onDate(date)
},
)
}
} }
@Composable @Composable
@@ -2429,19 +2454,120 @@ private fun StitchHeader(height: Dp = 124.dp) {
} }
@Composable @Composable
private fun DateChip(date: String) { private fun DateChip(date: String, onClick: () -> Unit) {
val formatter = remember { DateTimeFormatter.ofPattern("d MMM", Locale("pl", "PL")) } val formatter = remember { DateTimeFormatter.ofPattern("d MMM", Locale("pl", "PL")) }
val label = remember(date) { LocalDate.parse(date).format(formatter).replace(".", "").replaceFirstChar { it.uppercase() } } val label = remember(date) { LocalDate.parse(date).format(formatter).replace(".", "").replaceFirstChar { it.uppercase() } }
Row( Row(
modifier = Modifier.background(TppTheme.colors.panel, RoundedCornerShape(12.dp)).padding(horizontal = 16.dp, vertical = 12.dp), modifier = Modifier
.background(TppTheme.colors.panel, RoundedCornerShape(12.dp))
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp), horizontalArrangement = Arrangement.spacedBy(10.dp),
) { ) {
Icon(Icons.Outlined.CalendarToday, contentDescription = null, tint = TppTheme.colors.forest) Icon(
Icons.Outlined.CalendarToday,
contentDescription = "Otwórz kalendarz",
tint = TppTheme.colors.forest,
)
Text(label, fontFamily = FontFamily.Monospace, color = TppTheme.colors.muted, fontWeight = FontWeight.SemiBold) Text(label, fontFamily = FontFamily.Monospace, color = TppTheme.colors.muted, fontWeight = FontWeight.SemiBold)
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun RouteDatePickerDialog(
selectedDate: String,
minDate: String,
maxDate: String,
onDismiss: () -> Unit,
onDateSelected: (String) -> Unit,
) {
val bounds = remember(selectedDate, minDate, maxDate) {
routeDatePickerBounds(
selectedDate = selectedDate,
minDate = minDate,
maxDate = maxDate,
)
}
val selectableDates = remember(bounds) {
object : SelectableDates {
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
val date = Instant.ofEpochMilli(utcTimeMillis).atZone(ZoneOffset.UTC).toLocalDate()
return date in bounds.minDate..bounds.maxDate
}
override fun isSelectableYear(year: Int): Boolean =
year in bounds.minDate.year..bounds.maxDate.year
}
}
val datePickerState = rememberDatePickerState(
initialSelectedDateMillis = bounds.selectedDate
.atStartOfDay(ZoneOffset.UTC)
.toInstant()
.toEpochMilli(),
yearRange = bounds.minDate.year..bounds.maxDate.year,
selectableDates = selectableDates,
)
DatePickerDialog(
onDismissRequest = onDismiss,
confirmButton = {
TextButton(
onClick = {
datePickerState.selectedDateMillis
?.let { Instant.ofEpochMilli(it).atZone(ZoneOffset.UTC).toLocalDate() }
?.takeIf { it in bounds.minDate..bounds.maxDate }
?.let { onDateSelected(it.toString()) }
},
enabled = datePickerState.selectedDateMillis != null,
) {
Text("Wybierz")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Anuluj")
}
},
) {
DatePicker(
state = datePickerState,
title = {
Text(
"Wybierz dzień kursów",
modifier = Modifier.padding(start = 24.dp, end = 24.dp, top = 16.dp),
)
},
showModeToggle = false,
)
}
}
internal data class RouteDatePickerBounds(
val selectedDate: LocalDate,
val minDate: LocalDate,
val maxDate: LocalDate,
)
internal fun routeDatePickerBounds(
selectedDate: String,
minDate: String,
maxDate: String,
): RouteDatePickerBounds {
val selected = runCatching { LocalDate.parse(selectedDate) }.getOrDefault(LocalDate.now())
val parsedMin = runCatching { LocalDate.parse(minDate) }.getOrDefault(selected)
val parsedMax = runCatching { LocalDate.parse(maxDate) }.getOrDefault(selected)
val rangeMin = minOf(parsedMin, parsedMax)
val rangeMax = maxOf(parsedMin, parsedMax)
return RouteDatePickerBounds(
selectedDate = selected.coerceIn(rangeMin, rangeMax),
minDate = rangeMin,
maxDate = rangeMax,
)
}
@Composable @Composable
private fun RouteDateSelector(selectedDate: String, minDate: String, maxDate: String, onDate: (String) -> Unit) { private fun RouteDateSelector(selectedDate: String, minDate: String, maxDate: String, onDate: (String) -> Unit) {
val dates = remember(minDate, maxDate) { val dates = remember(minDate, maxDate) {
@@ -1,6 +1,7 @@
package pl.firmatpp.kierowca.ui package pl.firmatpp.kierowca.ui
import android.Manifest import android.Manifest
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.location.Location import android.location.Location
@@ -39,6 +40,7 @@ internal suspend fun currentPhotoLocation(context: Context): Location? {
return bestLastKnownLocation(manager) return bestLastKnownLocation(manager)
} }
@SuppressLint("MissingPermission")
private suspend fun LocationManager.awaitSingleLocation( private suspend fun LocationManager.awaitSingleLocation(
context: Context, context: Context,
provider: String, provider: String,
@@ -75,6 +77,7 @@ private fun hasAnyLocationPermission(context: Context): Boolean =
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
@SuppressLint("MissingPermission")
private fun bestLastKnownLocation(manager: LocationManager): Location? { private fun bestLastKnownLocation(manager: LocationManager): Location? {
val providers = listOf( val providers = listOf(
LocationManager.GPS_PROVIDER, LocationManager.GPS_PROVIDER,
@@ -0,0 +1,43 @@
package pl.firmatpp.kierowca.ui
import java.time.LocalDate
import org.junit.Assert.assertEquals
import org.junit.Test
class RouteDatePickerBoundsTest {
@Test
fun usesTheSameMinAndMaxDatesAsTheRouteSelector() {
val bounds = routeDatePickerBounds(
selectedDate = "2026-07-24",
minDate = "2026-07-17",
maxDate = "2026-07-26",
)
assertEquals(LocalDate.parse("2026-07-24"), bounds.selectedDate)
assertEquals(LocalDate.parse("2026-07-17"), bounds.minDate)
assertEquals(LocalDate.parse("2026-07-26"), bounds.maxDate)
}
@Test
fun clampsSelectedDateToAllowedRange() {
val bounds = routeDatePickerBounds(
selectedDate = "2026-07-30",
minDate = "2026-07-17",
maxDate = "2026-07-26",
)
assertEquals(LocalDate.parse("2026-07-26"), bounds.selectedDate)
}
@Test
fun normalizesAccidentallyReversedServerBounds() {
val bounds = routeDatePickerBounds(
selectedDate = "2026-07-24",
minDate = "2026-07-26",
maxDate = "2026-07-17",
)
assertEquals(LocalDate.parse("2026-07-17"), bounds.minDate)
assertEquals(LocalDate.parse("2026-07-26"), bounds.maxDate)
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
[versions] [versions]
agp = "8.7.3" agp = "8.10.1"
kotlin = "2.1.10" kotlin = "2.1.10"
composeBom = "2026.03.00" composeBom = "2026.03.00"
activityCompose = "1.9.3" activityCompose = "1.9.3"