Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fa3b4eac1 | ||
|
|
197bcc6f5e | ||
|
|
f4f5cec2be | ||
|
|
5a630f8401 | ||
|
|
54a3c58a22 | ||
|
|
1c6a044c37 |
@@ -34,8 +34,8 @@ android {
|
||||
applicationId = "pl.firmatpp.kierowca"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 106
|
||||
versionName = "1.0.53"
|
||||
versionCode = 109
|
||||
versionName = "1.0.56"
|
||||
setProperty("archivesBaseName", "pl.firmatpp.kierowca")
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
|
||||
@@ -117,6 +117,7 @@ data class RealtimeConfigDto(
|
||||
val reverbEnabled: Boolean = false,
|
||||
val reverbAppKey: String? = null,
|
||||
val reverbWsBaseUrl: String? = null,
|
||||
val reverbOrigin: String? = null,
|
||||
)
|
||||
|
||||
data class RoutesBucketDto(
|
||||
|
||||
@@ -3,6 +3,7 @@ package pl.firmatpp.kierowca.sync
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import java.net.URI
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -42,16 +43,33 @@ class DriverRepositoryLiveSyncGateway(
|
||||
}
|
||||
|
||||
interface LiveWebSocketFactory {
|
||||
fun newWebSocket(url: String, listener: WebSocketListener): WebSocket
|
||||
fun newWebSocket(url: String, origin: String?, listener: WebSocketListener): WebSocket
|
||||
}
|
||||
|
||||
class OkHttpLiveWebSocketFactory(
|
||||
private val client: OkHttpClient,
|
||||
) : LiveWebSocketFactory {
|
||||
override fun newWebSocket(url: String, listener: WebSocketListener): WebSocket =
|
||||
client.newWebSocket(Request.Builder().url(url).build(), listener)
|
||||
override fun newWebSocket(url: String, origin: String?, listener: WebSocketListener): WebSocket =
|
||||
client.newWebSocket(
|
||||
Request.Builder()
|
||||
.url(url)
|
||||
.header("Origin", origin?.takeIf { it.isNotBlank() } ?: websocketOrigin(url))
|
||||
.build(),
|
||||
listener,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun websocketOrigin(url: String): String =
|
||||
URI(
|
||||
url
|
||||
.replaceFirst("wss://", "https://")
|
||||
.replaceFirst("ws://", "http://")
|
||||
).let { uri ->
|
||||
val defaultPort = (uri.scheme == "https" && uri.port == 443) || (uri.scheme == "http" && uri.port == 80)
|
||||
val port = if (uri.port > 0 && !defaultPort) ":${uri.port}" else ""
|
||||
"${uri.scheme}://${uri.host}$port"
|
||||
}
|
||||
|
||||
enum class LiveSyncConnectionState {
|
||||
Stopped,
|
||||
WaitingForNetwork,
|
||||
@@ -230,7 +248,7 @@ class DriverLiveSyncClient(
|
||||
}
|
||||
|
||||
private fun connectNow(resetAttempt: Boolean = false) {
|
||||
val wsUrl = synchronized(lock) {
|
||||
val (wsUrl, origin) = synchronized(lock) {
|
||||
val config = realtimeConfig ?: return
|
||||
val id = driverId ?: return
|
||||
if (!desiredActive || !foregroundActive || !networkAvailable || !isConfigUsable(config)) return
|
||||
@@ -244,11 +262,15 @@ class DriverLiveSyncClient(
|
||||
|
||||
val appKey = config.reverbAppKey.orEmpty()
|
||||
val wsBaseUrl = config.reverbWsBaseUrl.orEmpty()
|
||||
wsBaseUrl.trimEnd('/') + "/" + appKey + "?protocol=7&client=android&version=1.0&flash=false"
|
||||
Pair(
|
||||
wsBaseUrl.trimEnd('/') + "/" + appKey + "?protocol=7&client=android&version=1.0&flash=false",
|
||||
config.reverbOrigin,
|
||||
)
|
||||
}
|
||||
|
||||
val socket = webSocketFactory.newWebSocket(
|
||||
wsUrl,
|
||||
origin,
|
||||
object : WebSocketListener() {
|
||||
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||
handleMessage(webSocket, text)
|
||||
|
||||
@@ -228,6 +228,22 @@ fun DriverApp(
|
||||
viewModel.openLeaveRequest(initialLeaveRequestId)
|
||||
}
|
||||
}
|
||||
LaunchedEffect(state.screen, state.notifyNewRoutes, state.notificationPermissionDenied, appInForeground) {
|
||||
if (!appInForeground || state.screen != DriverScreen.Profile || !state.notifyNewRoutes) return@LaunchedEffect
|
||||
|
||||
if (canPostNotifications(context)) {
|
||||
if (state.notificationPermissionDenied) {
|
||||
viewModel.clearNotificationPermissionWarning()
|
||||
}
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !hasPostNotificationsRuntimePermission(context)) {
|
||||
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
} else {
|
||||
viewModel.markNotificationPermissionBlocked()
|
||||
}
|
||||
}
|
||||
|
||||
Box(Modifier.fillMaxSize().background(TppTheme.colors.surface)) {
|
||||
when (state.screen) {
|
||||
@@ -2238,13 +2254,18 @@ private fun ProfileScreen(
|
||||
Text("Zmiana powiadomień wymaga połączenia z internetem.", color = TppTheme.colors.muted, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
if (state.notificationPermissionDenied) {
|
||||
Text(
|
||||
"Android blokuje powiadomienia dla tej aplikacji. Token FCM może być zarejestrowany, ale powiadomienie nie pojawi się na telefonie, dopóki nie włączysz zgody systemowej.",
|
||||
color = TppTheme.colors.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
Button(
|
||||
onClick = onOpenNotificationSettings,
|
||||
modifier = Modifier.fillMaxWidth().height(48.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = TppTheme.colors.navy),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
) {
|
||||
Text("Przejdź do ustawień", fontWeight = FontWeight.Bold)
|
||||
Text("Otwórz ustawienia powiadomień", fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
HorizontalDivider(color = TppTheme.colors.outline.copy(alpha = 0.55f))
|
||||
|
||||
@@ -639,6 +639,24 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
|
||||
}
|
||||
}
|
||||
|
||||
fun markNotificationPermissionBlocked() {
|
||||
_state.update {
|
||||
it.copy(
|
||||
notificationPermissionDenied = true,
|
||||
error = "Powiadomienia są włączone w aplikacji, ale Android blokuje ich wyświetlanie. Włącz powiadomienia w ustawieniach aplikacji.",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearNotificationPermissionWarning() {
|
||||
_state.update {
|
||||
it.copy(
|
||||
notificationPermissionDenied = false,
|
||||
error = if (it.error?.contains("powiadom", ignoreCase = true) == true) null else it.error,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setThemeMode(themeMode: AppThemeMode) {
|
||||
_state.update { it.copy(themeMode = themeMode) }
|
||||
viewModelScope.launch {
|
||||
|
||||
@@ -23,8 +23,26 @@ class DriverLiveSyncClientTest {
|
||||
reverbEnabled = true,
|
||||
reverbAppKey = "app-key",
|
||||
reverbWsBaseUrl = "wss://example.test/app",
|
||||
reverbOrigin = "https://bootstrap-origin.test",
|
||||
)
|
||||
|
||||
@Test
|
||||
fun derivesBrowserCompatibleOriginFromWebSocketUrl() {
|
||||
assertEquals("https://api-intranet.firmatpp.pl", websocketOrigin("wss://api-intranet.firmatpp.pl/ws/app/app-key"))
|
||||
assertEquals("https://example.test:8443", websocketOrigin("wss://example.test:8443/app/app-key"))
|
||||
assertEquals("http://localhost:8080", websocketOrigin("ws://localhost:8080/app/app-key"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun passesBootstrapOriginToWebSocketFactory() = runTest {
|
||||
val factory = FakeWebSocketFactory()
|
||||
val client = liveClient(factory = factory, scope = backgroundScope)
|
||||
|
||||
client.start("driver-1", config)
|
||||
|
||||
assertEquals("https://bootstrap-origin.test", factory.origins.single())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun reconnectsWithBackoffAfterSocketFailure() = runTest {
|
||||
val factory = FakeWebSocketFactory()
|
||||
@@ -204,10 +222,12 @@ class DriverLiveSyncClientTest {
|
||||
|
||||
private class FakeWebSocketFactory : LiveWebSocketFactory {
|
||||
val sockets = mutableListOf<FakeWebSocket>()
|
||||
val origins = mutableListOf<String?>()
|
||||
|
||||
override fun newWebSocket(url: String, listener: WebSocketListener): WebSocket {
|
||||
override fun newWebSocket(url: String, origin: String?, listener: WebSocketListener): WebSocket {
|
||||
val socket = FakeWebSocket(listener)
|
||||
sockets += socket
|
||||
origins += origin
|
||||
return socket
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user