Compare commits

..

9 Commits

Author SHA1 Message Date
PoliEcho dbd29bf421 add working speed-o-meter 2026-02-13 16:41:05 +01:00
PoliEcho c3f8dd4066 revert GTFSUtils.kt from 67fea519 2026-02-13 14:37:17 +01:00
PoliEcho 5970dce0d2 works? 2026-02-13 14:20:30 +01:00
PoliEcho 0e2208d104 add more optimalization and speed mareker 2026-02-13 12:28:19 +01:00
PoliEcho 67fea51949 tweaks 2026-01-28 22:34:09 +01:00
PoliEcho 4263a7eff0 add more optimalization 2026-01-28 22:05:30 +01:00
PoliEcho d1314a1b32 fix ANR 2026-01-28 21:55:22 +01:00
PoliEcho 93148fcf36 make sure all stops are requested 2026-01-28 21:55:08 +01:00
PoliEcho ced8e8173d add stops 2026-01-28 19:08:35 +01:00
7 changed files with 332 additions and 111 deletions
+1
View File
@@ -13,3 +13,4 @@
.externalNativeBuild
.cxx
local.properties
test-data
+1 -1
View File
@@ -4,7 +4,7 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-01-19T17:27:16.298528612Z">
<DropdownSelection timestamp="2026-02-13T13:44:07.200825088Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/home/lukas/.config/.android/avd/Medium_Phone_OSS.avd" />
@@ -14,33 +14,36 @@ import kotlinx.serialization.Serializable
import okhttp3.OkHttpClient
import okhttp3.Request
import ovh.plrapps.mapcompose.api.addMarker
import ovh.plrapps.mapcompose.api.removeMarker
import ovh.plrapps.mapcompose.ui.state.markers.model.RenderingStrategy
import java.net.URL
@Serializable
data class GTFSRoute(
val agency_id: String,
val agency_id: String?,
val is_night: Boolean,
val route_color: String,
val route_color: String?,
val route_desc: String? = null,
val route_id: String,
val route_long_name: String,
val route_short_name: String,
val route_text_color: String,
val route_id: String?,
val route_long_name: String?,
val route_short_name: String?,
val route_text_color: String?,
val route_type: Int,
val route_url: String,
val route_url: String?,
val is_regional: Boolean,
val is_substitute_transport: Boolean
)
// Type alias for array of GTFS routes
typealias GTFSRoutes = List<GTFSRoute>
typealias GTFSRoutes = MutableList<GTFSRoute>
var gtfsRoutes: GTFSRoutes = mutableListOf()
var gtfsRoutes: GTFSRoutes? = null
@Serializable
data class GTFSStopGeometry(
val coordinates: List<Double>,
val type: String
val type: String?
)
@Serializable
@@ -49,7 +52,7 @@ data class GTFSStopProperties(
val parent_station: String? = null,
val platform_code: String? = null,
val stop_id: String,
val stop_name: String,
val stop_name: String?,
val wheelchair_boarding: Int,
val zone_id: String? = null,
val level_id: String? = null
@@ -59,16 +62,16 @@ data class GTFSStopProperties(
data class GTFSStop(
val geometry: GTFSStopGeometry,
val properties: GTFSStopProperties,
val type: String
val type: String?
)
@Serializable
data class GTFSStops(
val features: List<GTFSStop>,
val type: String
val features: MutableList<GTFSStop>,
val type: String?
)
var gtfsStops: GTFSStops? = null
var gtfsStops: GTFSStops = GTFSStops(mutableListOf<GTFSStop>(), "FeatureCollection")
// OkHttpClient for making HTTP requests
@@ -104,34 +107,24 @@ private fun fetchGTFSPosition(gtfsUrl: URL,api_key: String, callee: Activity) {
}
}
// Start the timer with a URL:
fun startGTFSTracking(url: String, api_key: String, callee: Activity) {
if (GTFSIsTracking) return
GTFSIsTracking = true
GTFSTrackingThread = Thread {
var gtfsUrl = URL(url);
val gtfsUrl = URL(url);
try {
if(gtfsRoutes == null) {
val request = Request.Builder()
.url("${gtfsUrl.protocol}://${gtfsUrl.host}${callee.getString(R.string.routes_path)}")
.header(
"User-Agent",
"${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} (Android)"
)
.header("X-Access-Token", api_key).build();
if(gtfsRoutes.isEmpty()) {
GTFSMainHandler.post {
Toast.makeText(callee, "Fetching routes...", Toast.LENGTH_SHORT).show()}
gtfshttpClient.newCall(request).execute().use { response ->
var message = if (response.isSuccessful) {
response.body?.string() ?: "Empty response"
} else {
"Error: ${response.code} ${response.message}"
}
gtfsRoutes = json.decodeFromString<GTFSRoutes>(message)
}}
if(gtfsStops == null) {
var messageParsed: GTFSRoutes;
var offset = 0;
do {
val request = Request.Builder()
.url("${gtfsUrl.protocol}://${gtfsUrl.host}${callee.getString(R.string.stops_path)}")
.url("${gtfsUrl.protocol}://${gtfsUrl.host}${callee.getString(R.string.routes_path)}?offset=${offset}")
.header(
"User-Agent",
"${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} (Android)"
@@ -144,27 +137,63 @@ fun startGTFSTracking(url: String, api_key: String, callee: Activity) {
} else {
"Error: ${response.code} ${response.message}"
}
gtfsStops = json.decodeFromString<GTFSStops>(message)
messageParsed = json.decodeFromString<GTFSRoutes>(message)
gtfsRoutes.addAll(messageParsed)
offset += messageParsed.size
}} while (messageParsed.isNotEmpty())
}
if(gtfsStops.features.isEmpty()) {
GTFSMainHandler.post {
Toast.makeText(callee, "Fetching stops...", Toast.LENGTH_SHORT).show()}
// put all stops on the map
for (stop in gtfsStops!!.features) {
GTFSMainHandler.post {
mapState.addMarker(stop.properties.stop_id, x = longitudeToXNormalized(stop.geometry.coordinates[0]), y = latitudeToYNormalized(stop.geometry.coordinates[0])) {
Icon(
painter = painterResource(id = R.drawable.outline_directions_bus_24),
contentDescription = null,
modifier = Modifier.size(17.dp),
tint = Color(0xFF0000FF)
)
}
var messageParsed: GTFSStops;
var offset = 0;
do {
val request = Request.Builder()
.url("${gtfsUrl.protocol}://${gtfsUrl.host}${callee.getString(R.string.stops_path)}?offset=${offset}")
.header(
"User-Agent",
"${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} (Android)"
)
.header("X-Access-Token", api_key).build();
}
gtfshttpClient.newCall(request).execute().use { response ->
var message = if (response.isSuccessful) {
response.body?.string() ?: "Empty response"
} else {
"Error: ${response.code} ${response.message}"
}
messageParsed = json.decodeFromString<GTFSStops>(message)
gtfsStops.features.addAll(messageParsed.features)
}
offset += messageParsed.features.size;
}while(messageParsed.features.isNotEmpty())
}
// put all stops on the map
GTFSMainHandler.post {
Toast.makeText(callee, "Drawing stops...", Toast.LENGTH_SHORT).show()}
for (stop in gtfsStops!!.features) {
if (stop.properties.parent_station == null) {
mapState.addMarker(stop.properties.stop_id, x = longitudeToXNormalized(stop.geometry.coordinates[0]), y = latitudeToYNormalized(stop.geometry.coordinates[1]), renderingStrategy = RenderingStrategy.LazyLoading("default")) {
Icon(
painter = painterResource(id = R.drawable.outline_directions_bus_24),
contentDescription = null,
modifier = Modifier.size(12.dp),
tint = Color(0xFF0000FF)
)
}}
} catch(e: Exception) {
}
GTFSMainHandler.post {
Toast.makeText(callee, "Done drawing ${gtfsStops.features.size} stops", Toast.LENGTH_SHORT).show()}
} catch(e: Exception) {
GTFSMainHandler.post {
Toast.makeText(callee, e.message ?: "Unknown error", Toast.LENGTH_SHORT).show()
GTFSMainHandler.post { stopGTFSTracking() }
return@Thread
}
GTFSMainHandler.post { stopGTFSTracking() }
return@Thread
}
while (GTFSIsTracking) {
fetchGTFSPosition(gtfsUrl,api_key, callee)
@@ -179,6 +208,11 @@ fun startGTFSTracking(url: String, api_key: String, callee: Activity) {
fun stopGTFSTracking() {
GTFSIsTracking = false
for (stop in gtfsStops!!.features) {
if (stop.properties.parent_station == null) {
mapState.removeMarker(stop.properties.stop_id);
}
}
GTFSTrackingThread?.interrupt()
GTFSTrackingThread = null
}
@@ -49,15 +49,38 @@ private fun fetchHaukPosition(haukUrl: String, callee: Activity) {
message = "{" + message.substringAfter("{");
val hauk_response = json.decodeFromString<HaukResponse>(message)
val lat = hauk_response.points.last()?.getOrNull(0)
val lon = hauk_response.points.last()?.getOrNull(1)
val lat = hauk_response.points.last().getOrNull(0)
val lon = hauk_response.points.last().getOrNull(1)
if (lat != null && lon != null) {
HaukMainHandler.post {
mapState.moveMarker("target_position", x = longitudeToXNormalized(lon), y = latitudeToYNormalized(lat))
}
val lastlat = hauk_response.points.getOrNull(hauk_response.points.size - 2)?.getOrNull(0)
val lastlon = hauk_response.points.getOrNull(hauk_response.points.size - 2)?.getOrNull(1)
val newspeed =
if (lastlat != null && lastlon != null) {
var fulldistance: Double = 0.0
for (i in 1 until hauk_response.points.size -1) {
try{
fulldistance += haversineDistance(hauk_response.points[i-1][0]!!, hauk_response.points[i-1][1]!!, hauk_response.points[i][0]!!, hauk_response.points[i][1]!!)
} catch (e: Exception) {
// Ignore
}
}
(fulldistance / (hauk_response.interval * hauk_response.points.size)*3600)
} else {
0.0
}
HaukMainHandler.post {
try {
mapState.moveMarker("target_position", x = longitudeToXNormalized(lon), y = latitudeToYNormalized(lat))
target_speed = "${"%.1f".format(newspeed)}km/h"
} catch (e: Exception) {
// Marker might not exist yet, ignore
}
}
} else {
Toast.makeText(callee, "TARGET LOCATION NOT AVAILABLE", Toast.LENGTH_SHORT).show()
HaukMainHandler.post {
Toast.makeText(callee, "TARGET LOCATION NOT AVAILABLE", Toast.LENGTH_SHORT).show()
}
}}
} catch (e: Exception) {
// Post UI update to main thread
@@ -4,6 +4,27 @@ import kotlinx.serialization.json.Json
import kotlin.math.PI
import kotlin.math.ln
import kotlin.math.tan
import kotlin.math.*
fun haversineDistance(
lat1: Double,
lon1: Double,
lat2: Double,
lon2: Double
): Double {
val R = 6371.0 // Earth radius in kilometers
val phi1 = lat1 * PI / 180
val phi2 = lat2 * PI / 180
val deltaPhi = (lat2 - lat1) * PI / 180
val deltaLambda = (lon2 - lon1) * PI / 180
val a = sin(deltaPhi / 2).pow(2) +
cos(phi1) * cos(phi2) * sin(deltaLambda / 2).pow(2)
val c = 2 * atan2(sqrt(a), sqrt(1 - a))
return R * c
}
val json = Json {
coerceInputValues = true // Coerce nulls to default values if applicable
@@ -2,7 +2,6 @@ package org.pupes.mhdrunpathfinder
import android.app.Activity
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -30,8 +29,10 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import kotlinx.coroutines.launch
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
@@ -40,12 +41,20 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.edit
import ovh.plrapps.mapcompose.api.addLayer
import ovh.plrapps.mapcompose.api.addLazyLoader
import ovh.plrapps.mapcompose.api.addMarker
import ovh.plrapps.mapcompose.api.scrollTo
import ovh.plrapps.mapcompose.core.TileStreamProvider
import ovh.plrapps.mapcompose.ui.MapUI
import ovh.plrapps.mapcompose.ui.state.MapState
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.net.URL
import android.os.StrictMode
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.sp
import ovh.plrapps.mapcompose.api.getMarkerInfo
// MapState configuration for OpenStreetMap
@@ -58,14 +67,84 @@ val mapState = MapState(
fullHeight = 67108864,
tileSize = 256
)
var target_speed by mutableStateOf("0.0km/h");
/**
* Creates a cached tile stream provider for OpenStreetMap tiles
* Tiles are cached in the app's cache directory for offline access and faster loading
* Cache expires after 30 days to ensure map updates are fetched
*/
fun createCachedTileStreamProvider(context: Context, cacheExpiryDays: Int = 30): TileStreamProvider {
val cacheDir = File(context.cacheDir, "map_tiles")
if (!cacheDir.exists()) {
cacheDir.mkdirs()
}
val cacheExpiryMillis = cacheExpiryDays * 24 * 60 * 60 * 1000L
return TileStreamProvider { row, col, zoomLvl ->
try {
val tileFile = File(cacheDir, "tile_${zoomLvl}_${col}_${row}.png")
val shouldUseCache = tileFile.exists() &&
(System.currentTimeMillis() - tileFile.lastModified()) < cacheExpiryMillis
if (shouldUseCache) {
return@TileStreamProvider FileInputStream(tileFile)
}
// Since mapcompose runs this on a background thread, we can do networking here.
try {
val url = URL("https://tile.openstreetmap.org/$zoomLvl/$col/$row.png")
val connection = url.openConnection()
// Set User-Agent header as required by OSM tile usage policy
val userAgent = "${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} (Android) (Contact: poliecho@pupes.org)"
connection.setRequestProperty("User-Agent", userAgent)
connection.connectTimeout = 5000
connection.readTimeout = 10000
connection.getInputStream().use { inputStream ->
FileOutputStream(tileFile).use { outputStream ->
inputStream.copyTo(outputStream)
}
}
FileInputStream(tileFile)
} catch (e: Exception) {
e.printStackTrace()
// If download fails, try to use an expired cached tile if it exists
if (tileFile.exists()) {
FileInputStream(tileFile)
} else {
null
}
}
} catch (e: Exception) {
e.printStackTrace()
null
}
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Configure StrictMode to detect threading violations (debug mode only)
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog() // Log violations instead of crashing
.build()
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectAll()
.penaltyLog()
.build()
)
}
setContent {
MaterialTheme {
@@ -91,25 +170,55 @@ class MainActivity : ComponentActivity() {
@Composable
fun MainScreen(callee: Activity) {
val sharedPreferences = remember { callee.getSharedPreferences("PREFERENCES", Context.MODE_PRIVATE) }
val coroutineScope = rememberCoroutineScope()
var showSettings by remember { mutableStateOf(false) }
var haukUrl by remember { mutableStateOf("") }
var golemioAPIkey by remember { mutableStateOf(sharedPreferences.getString("golemioAPIkey", "")?: "" ) }
var haukState by remember { mutableStateOf(false) }
var golemioState by remember { mutableStateOf(false)}
var golemioState by remember { mutableStateOf(golemioAPIkey.isNotEmpty())}
if (golemioAPIkey.isNotEmpty()) {
golemioState = true
startGTFSTracking(callee.getString(R.string.golemio_base_url), golemioAPIkey, callee);
LaunchedEffect(Unit) {
if (golemioState) {
startGTFSTracking(callee.getString(R.string.golemio_base_url), golemioAPIkey, callee)
}
}
Box(modifier = Modifier.fillMaxSize()) {
OpenStreetMapScreen()
SettingsButton(
OpenStreetMapScreen(callee)
IconButton( // settings button
onClick = { showSettings = true },
modifier = Modifier
.align(Alignment.TopEnd)
.padding(end = 5.dp, bottom = 5.dp)
.padding(end = 5.dp, bottom = 5.dp),
IconID = R.drawable.baseline_settings_24
)
Column(modifier = Modifier.align(Alignment.TopStart)) {
IconButton( // look at target button
onClick = {
coroutineScope.launch {
mapState.getMarkerInfo("target_position")?.let { marker ->
mapState.scrollTo(marker.x, marker.y, destScale = 0.1)
}
}
},
modifier = Modifier
.padding(start = 5.dp, bottom = 5.dp),
IconID = R.drawable.target
)
IconButton( // look at my location button
onClick = {
coroutineScope.launch {
mapState.getMarkerInfo("current_position")?.let { marker ->
mapState.scrollTo(marker.x, marker.y, destScale = 0.1)
}
}
},
modifier = Modifier
.padding(start = 5.dp, bottom = 5.dp),
IconID = R.drawable.user_location
)
}
if (showSettings) {
ShowSettingsMenu(
onDismiss = { showSettings = false },
@@ -168,7 +277,7 @@ fun MainScreen(callee: Activity) {
@Composable
// When a composable with parameters is used with @Preview,
// a default value must be provided for the preview to render.
fun SettingsButton(modifier: Modifier = Modifier, onClick: () -> Unit = {}) {
fun IconButton(modifier: Modifier = Modifier, onClick: () -> Unit = {}, IconID: Int = R.drawable.baseline_settings_24) {
Button(
onClick = { onClick() },
colors = ButtonDefaults.buttonColors(containerColor = Color(0xCFFFFFFF)),
@@ -176,7 +285,7 @@ fun SettingsButton(modifier: Modifier = Modifier, onClick: () -> Unit = {}) {
) {
Icon(
painter = painterResource(id = R.drawable.baseline_settings_24),
painter = painterResource(id = IconID),
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = Color(0xFF000000)
@@ -211,6 +320,7 @@ fun ShowSettingsMenu(
value = haukUrl,
onValueChange = onHaukUrlChange,
label = { Text("Target Hauk URL") },
singleLine = true,
modifier = Modifier
.width(250.dp)
)
@@ -224,6 +334,7 @@ fun ShowSettingsMenu(
onCheckedChange = { onGolemioStateChange(it) }
)
TextField(value = golemioAPIkey, onValueChange = onGolemioAPIChange, label = { Text("Golemio API Key") },
singleLine = true,
modifier = Modifier
.width(250.dp));
ElevatedButton(onClick = onGolemioApply, modifier = Modifier.padding(start = 5.dp), shape = RectangleShape) {
@@ -235,56 +346,86 @@ fun ShowSettingsMenu(
@Preview
@Composable
fun OpenStreetMapScreen() {
// Create tile stream provider for OpenStreetMap with User-Agent
val tileStreamProvider = TileStreamProvider { row, col, zoomLvl ->
try {
val url = URL("https://tile.openstreetmap.org/$zoomLvl/$col/$row.png")
val connection = url.openConnection()
// Set User-Agent header as required by OSM tile usage policy
// Format: AppId/Version (Platform) (Contact: email)
val userAgent = "${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} (Android) (Contact: poliecho@pupes.org)"
connection.setRequestProperty("User-Agent", userAgent)
connection.getInputStream()
} catch (e: Exception) {
e.printStackTrace()
null
fun OpenStreetMapScreen(context: Context? = null) {
// Create tile stream provider for OpenStreetMap with caching
val tileStreamProvider = remember(context) {
context?.let {
createCachedTileStreamProvider(it)
} ?: TileStreamProvider { row, col, zoomLvl ->
// Fallback for preview mode without context
try {
val url = URL("https://tile.openstreetmap.org/$zoomLvl/$col/$row.png")
val connection = url.openConnection()
val userAgent = "MHDrunPathfinder/Preview (Android)"
connection.setRequestProperty("User-Agent", userAgent)
connection.getInputStream()
} catch (e: Exception) {
e.printStackTrace()
null
}
}
}
// Track if map has been initialized to prevent duplicate operations
var mapInitialized by remember { mutableStateOf(false) }
/* Add a marker at the center of the map */
mapState.addMarker("target_position", x = longitudeToXNormalized(14.4058031), y = latitudeToYNormalized(50.0756083)) {
Icon(
painter = painterResource(id = R.drawable.target),
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = Color(0xFFFF0000)
)
}
/* Add a marker for current position */
mapState.addMarker("current_position", x = longitudeToXNormalized(14.4378), y = latitudeToYNormalized(50.0755)) {
Icon(
painter = painterResource(id = R.drawable.user_location),
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = Color(0xFF0000FF) // Blue color for current position
)
}
// Add the tile layer and set initial position
// Add the tile layer, markers, and set initial position - all in LaunchedEffect to avoid blocking main thread
LaunchedEffect(Unit) {
mapState.addLayer(tileStreamProvider)
if (!mapInitialized) {
// Add lazy loader first
mapState.addLazyLoader("default")
// Scroll to Prague, Czech Republic
// Prague coordinates: latitude 50.0755, longitude 14.4378
val normalizedX = longitudeToXNormalized(14.4378)
val normalizedY = latitudeToYNormalized(50.0755)
// Add tile layer
mapState.addLayer(tileStreamProvider)
// Use a higher scale to zoom in more (closer to 1.0 = more zoomed in)
mapState.scrollTo(normalizedX, normalizedY, destScale = 0.8)
// Add a marker for target position
mapState.addMarker("target_position", x = longitudeToXNormalized(14.0), y = latitudeToYNormalized(50.0), Offset(-0.5f,-0.25f)) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
painter = painterResource(id = R.drawable.target),
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = Color(0xFFFF0000)
)
Surface(
color = Color.Red,
shadowElevation = 4.dp,
shape = MaterialTheme.shapes.small,
modifier = Modifier.padding(1.dp)
) {
Text(
text = target_speed,
modifier = Modifier.padding(1.dp),
style = MaterialTheme.typography.bodyMedium,
fontSize = 8.sp,
color = Color.Blue
)
}
}
}
/* Add a marker for current position */
mapState.addMarker("current_position", x = longitudeToXNormalized(14.0), y = latitudeToYNormalized(50.0),
Offset(-0.5f,-0.5f)
) {
Icon(
painter = painterResource(id = R.drawable.user_location),
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = Color(0xFF004802)
)
}
// Scroll to Prague, Czech Republic
// Prague coordinates: latitude 50.0755, longitude 14.4378
val normalizedX = longitudeToXNormalized(14.4378)
val normalizedY = latitudeToYNormalized(50.0755)
// Use a higher scale to zoom in more (closer to 1.0 = more zoomed in)
mapState.scrollTo(normalizedX, normalizedY, destScale = 0.1)
mapInitialized = true
}
}
// Display the map
+1
View File
@@ -3,4 +3,5 @@
<string name="routes_path">/v2/gtfs/routes</string>
<string name="stops_path">/v2/gtfs/stops</string>
<string name="golemio_base_url">https://api.golemio.cz</string>
<string name="golemio_realtime_path"></string>
</resources>