Bimba.git

commit 28e71396b2ed6cee7c88b62de6788b39344689ff

Author: Adam <git@apiote.xyz>

cleanup
groomed todos, removed unnecessary lint suppresses, made IDs consistent, fixed warnings, set content-description for text and extracted string resources

%!v(PANIC=String method: strings: negative Repeat count)


diff --git a/3imba.svg b/3imba.svg
index c50a215f7f5cf61a33bb8a381d247af1ab7eebc0..55d9af1cd0196dbde99fbb5c3272c388fdf9c2c1 100644
--- a/3imba.svg
+++ b/3imba.svg
@@ -3,7 +3,6 @@     xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:cc="http://creativecommons.org/ns#"
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
    xmlns="http://www.w3.org/2000/svg"
    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
@@ -22,7 +21,6 @@          rdf:about="">
         <dc:format>image/svg+xml</dc:format>
         <dc:type
            rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-        <dc:title></dc:title>
       </cc:Work>
     </rdf:RDF>
   </metadata>




diff --git a/app/build.gradle b/app/build.gradle
index f83613c9bb0a0fadee2f4a6202fe11327e3cae4d..17900722ec9d39dd0fec456fe6dd9ddf51c1d563 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -38,7 +38,7 @@ }
 
 dependencies {
     implementation 'androidx.core:core-ktx:1.9.0'
-    implementation 'androidx.appcompat:appcompat:1.6.0'
+    implementation 'androidx.appcompat:appcompat:1.6.1'
     implementation 'com.google.android.material:material:1.8.0'
     implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
     implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'




diff --git a/app/src/androidTest/java/ml/adamsprogs/bimba/ExampleInstrumentedTest.kt b/app/src/androidTest/java/ml/adamsprogs/bimba/ExampleInstrumentedTest.kt
deleted file mode 100644
index 12072519495305fc9bdc3e93cc96af9ed1cfafbe..0000000000000000000000000000000000000000
--- a/app/src/androidTest/java/ml/adamsprogs/bimba/ExampleInstrumentedTest.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package ml.adamsprogs.bimba
-
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.ext.junit.runners.AndroidJUnit4
-
-import org.junit.Test
-import org.junit.runner.RunWith
-
-import org.junit.Assert.*
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-@RunWith(AndroidJUnit4::class)
-class ExampleInstrumentedTest {
-	@Test
-	fun useAppContext() {
-		// Context of the app under test.
-		val appContext = InstrumentationRegistry.getInstrumentation().targetContext
-		assertEquals("ml.adamsprogs.bimba", appContext.packageName)
-	}
-}
\ No newline at end of file




diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7de0435db26c3d4d5babfa875d01e60bf4614892..6e86b57b4ced88504298a3b09380d33dd243e046 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -7,7 +7,6 @@ 	
 	<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
 	<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
 
-	<!--suppress AndroidUnknownAttribute -->
 	<application
 		android:name=".Bimba"
 		android:allowBackup="true"
@@ -41,21 +40,16 @@
 				<category android:name="android.intent.category.DEFAULT" />
 				<category android:name="android.intent.category.BROWSABLE" />
 
-				<!--suppress AndroidUnknownAttribute -->
 				<data android:scheme="http" />
-				<!--suppress AndroidUnknownAttribute -->
 				<data android:scheme="https" />
-				<!--suppress AndroidUnknownAttribute -->
 				<data android:host="www.peka.poznan.pl" />
-				<!--suppress AndroidUnknownAttribute -->
-				<data android:pathPrefix="/vm" />  <!-- todo(intents) test with ?przystanek=... -->
+				<data android:pathPrefix="/vm" />
 			</intent-filter>
 		</activity>
 		<activity
 			android:name=".search.ResultsActivity"
 			android:exported="false"
 			android:label="@string/title_activity_results" />
-		<!--suppress AndroidUnknownAttribute -->
 		<activity
 			android:name=".dashboard.MainActivity"
 			android:exported="false"




diff --git a/app/src/main/java/ml/adamsprogs/bimba/api/Api.kt b/app/src/main/java/ml/adamsprogs/bimba/api/Api.kt
index 019ce0dc48a04d71befc67fc281b27a0be5db554..7f89c0ec8c12615e02ded718ef99a59e139cdfc8 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/api/Api.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/api/Api.kt
@@ -12,6 +12,8 @@ import java.net.HttpURLConnection
 import java.net.URL
 import java.net.URLEncoder
 
+// todo [3.1] constants
+
 data class Server(val host: String, val token: String, val feeds: String) {
 	companion object {
 		fun get(context: Context): Server {
@@ -29,7 +31,6 @@ data class Result(val stream: InputStream?, val error: Error?)
 
 data class Error(val statusCode: Int, val stringResource: Int, val imageResource: Int)
 
-@Suppress("BlockingMethodInNonBlockingContext")
 suspend fun getFeeds(cm: ConnectivityManager, server: Server): Result {
 	return rawRequest(
 		URL("${hostWithScheme(server.host)}/api/"),
@@ -55,7 +56,12 @@ suspend fun locateItems(cm: ConnectivityManager, server: Server, near: Position): Result {
 	return request(server, "items", mapOf("near" to near.toString()), cm)
 }
 
-suspend fun getLocatablesIn(cm: ConnectivityManager, server: Server, bl: Position, tr: Position): Result {
+suspend fun getLocatablesIn(
+	cm: ConnectivityManager,
+	server: Server,
+	bl: Position,
+	tr: Position
+): Result {
 	return request(server, "locatables", mapOf("lb" to bl.toString(), "rt" to tr.toString()), cm)
 }
 
@@ -72,11 +78,9 @@ 	}
 	return request(server, "departures", params, cm)
 }
 
-@Suppress("BlockingMethodInNonBlockingContext")
 suspend fun rawRequest(url: URL, server: Server, cm: ConnectivityManager): Result {
-	@Suppress("DEPRECATION")  // fix_later(API_29, API_23) https://developer.android.com/reference/android/net/ConnectivityManager#getActiveNetwork()
+	@Suppress("DEPRECATION")  // fixme later(API_29, API_23) https://developer.android.com/reference/android/net/ConnectivityManager#getActiveNetwork()
 	if (cm.activeNetworkInfo == null) {
-		// todo check false-positives
 		return Result(null, Error(0, R.string.error_offline, R.drawable.error_net))
 	}
 	return withContext(Dispatchers.IO) {
@@ -91,10 +95,7 @@ 				val (string, image) = when (c.responseCode) {
 					400 -> Pair(R.string.error_400, R.drawable.error_app)
 					401 -> Pair(R.string.error_401, R.drawable.error_sec)
 					403 -> Pair(R.string.error_403, R.drawable.error_sec)
-					404 -> Pair(
-						R.string.error_404,
-						R.drawable.error_search
-					) // todo check if server returns 404
+					404 -> Pair(R.string.error_404, R.drawable.error_search)
 					429 -> Pair(R.string.error_429, R.drawable.error_limit)
 					500 -> Pair(R.string.error_50x, R.drawable.error_server)
 					502 -> Pair(R.string.error_50x, R.drawable.error_server)
@@ -110,7 +111,6 @@ 		}
 	}
 }
 
-@Suppress("BlockingMethodInNonBlockingContext")
 suspend fun request(
 	server: Server,
 	resource: String,
@@ -118,7 +118,7 @@ 	params: Map,
 	cm: ConnectivityManager
 ): Result {
 	return withContext(Dispatchers.IO) {
-		val url = URL(
+		val url = URL( // todo [3.1] scheme, host, path, constructed query
 			"${hostWithScheme(server.host)}/api/${server.feeds}/$resource${
 				params.map {
 					"${it.key}=${




diff --git a/app/src/main/java/ml/adamsprogs/bimba/api/Responses.kt b/app/src/main/java/ml/adamsprogs/bimba/api/Responses.kt
index ab40e38e878d814f8f80c812ff731b812883f8d1..d1b585e68275c8a693cf9c3e2f8be95594fd0c4b 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/api/Responses.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/api/Responses.kt
@@ -15,7 +15,7 @@ 				1UL -> {
 					DeparturesSuccess.unmarshal(stream)
 				}
 				else -> {
-					TODO("throw unknown tag")
+					TODO("[api-freeze] throw unknown tag")
 				}
 			}
 		}
@@ -61,7 +61,7 @@ 				1UL -> {
 					ItemsSuccess.unmarshal(stream)
 				}
 				else -> {
-					TODO("throw unknown tag")
+					TODO("[api-freeze] throw unknown tag")
 				}
 			}
 		}
@@ -83,7 +83,7 @@ 					1UL -> {
 						items.add(Line.unmarshal(stream))
 					}
 					else -> {
-						TODO("throw unknown tag")
+						TODO("[api-freeze] throw unknown tag")
 					}
 				}
 			}
@@ -104,7 +104,7 @@ 				1UL -> {
 					FeedsSuccess.unmarshal(stream)
 				}
 				else -> {
-					TODO("throw unknown tag")
+					TODO("[api-freeze] throw unknown tag")
 				}
 			}
 		}
@@ -145,7 +145,7 @@ 				1UL -> {
 					LocatablesSuccess.unmarshal(stream)
 				}
 				else -> {
-					TODO("throw unknown tag")
+					TODO("[api-freeze] throw unknown tag")
 				}
 			}
 		}
@@ -167,7 +167,7 @@ 					1UL -> {
 						locatables.add(Vehicle.unmarshal(stream))
 					}
 					else -> {
-						TODO("throw unknown tag")
+						TODO("[api-freeze] throw unknown tag")
 					}
 				}
 			}




diff --git a/app/src/main/java/ml/adamsprogs/bimba/api/Structs.kt b/app/src/main/java/ml/adamsprogs/bimba/api/Structs.kt
index d339afa3b5746d64e5e54d5f6bd7b349791bda5d..99a5abc5900a4e81e2955d885f3eb2ae76032a02 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/api/Structs.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/api/Structs.kt
@@ -5,6 +5,7 @@ import android.graphics.*
 import android.graphics.drawable.BitmapDrawable
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.LayerDrawable
+import android.text.format.DateUtils
 import androidx.appcompat.content.res.AppCompatResources
 import androidx.core.graphics.ColorUtils.HSLToColor
 import androidx.core.graphics.drawable.toBitmap
@@ -13,6 +14,7 @@ import ml.adamsprogs.bimba.dpToPixel
 import ml.adamsprogs.bimba.dpToPixelI
 import xyz.apiote.fruchtfleisch.Reader
 import java.io.InputStream
+import java.util.*
 import java.util.zip.Adler32
 import kotlin.math.abs
 import kotlin.math.pow
@@ -188,6 +190,31 @@ 	}
 
 	override fun location(): Position = Position
 
+	fun congestion(context: Context): String {
+		return when (CongestionLevel.toUInt()) { // todo enum
+			0u -> context.getString(R.string.congestion_unknown)
+			1u -> context.getString(R.string.congestion_smooth)
+			2u -> context.getString(R.string.congestion_stop_and_go)
+			3u -> context.getString(R.string.congestion_congestion)
+			4u -> context.getString(R.string.congestion_jams)
+			else -> TODO("throw invalid congestion")
+		}
+	}
+
+	fun occupancy(context: Context): String {
+		return when (OccupancyStatus.toUInt()) { // todo enum
+			0u -> context.getString(R.string.occupancy_unknown)
+			1u -> context.getString(R.string.occupancy_empty)
+			2u -> context.getString(R.string.occupancy_many_seats)
+			3u -> context.getString(R.string.occupancy_few_seats)
+			4u -> context.getString(R.string.occupancy_standing_only)
+			5u -> context.getString(R.string.occupancy_crowded)
+			6u -> context.getString(R.string.occupancy_full)
+			7u -> context.getString(R.string.occupancy_wont_let)
+			else -> TODO("throw invalid occupancy")
+		}
+	}
+
 	companion object {
 		fun unmarshal(stream: InputStream): Vehicle {
 			val reader = Reader(stream)
@@ -220,7 +247,7 @@ 			val reader = Reader(stream)
 			val colour = Colour.unmarshal(stream)
 			val type = reader.readUInt()
 			val name = reader.readString()
-			return LineStub(name = name, colour = colour, type = LineType(type.toUInt()))
+			return LineStub(name = name, colour = colour, type = LineType.of(type.toUInt()))
 		}
 	}
 
@@ -240,6 +267,58 @@ 	val stopOrder: String,
 	val vehicle: Vehicle,
 	val boarding: UByte
 ) {
+
+	fun statusText(context: Context?): String {
+		val now = Calendar.getInstance()
+		val departureTime = Calendar.getInstance().apply {
+			set(Calendar.HOUR_OF_DAY, this@Departure.time.Hour.toInt())
+			set(Calendar.MINUTE, this@Departure.time.Minute.toInt())
+			set(Calendar.SECOND, this@Departure.time.Second.toInt())
+			set(
+				Calendar.ZONE_OFFSET,
+				TimeZone.getTimeZone(this@Departure.time.Zone).getOffset(now.timeInMillis)
+			)
+			roll(Calendar.DAY_OF_MONTH, this@Departure.time.DayOffset.toInt())
+		}
+		return when (status.toInt()) {
+			0 -> DateUtils.getRelativeTimeSpanString(
+				departureTime.timeInMillis,
+				now.timeInMillis,
+				DateUtils.MINUTE_IN_MILLIS,
+				DateUtils.FORMAT_ABBREV_RELATIVE
+			)
+				.toString()
+			1 -> context?.getString(R.string.departure_momentarily) ?: "momentarily"
+			2 -> context?.getString(R.string.departure_now) ?: "now"
+			3 -> context?.getString(R.string.departure_departed) ?: "departed"
+			else -> TODO("throw invalid")
+		}
+	}
+
+	fun timeString(context: Context): String {
+		return if (isRealtime) {
+			context.getString(
+				R.string.at_time_realtime,
+				time.Hour.toInt(),
+				time.Minute.toInt(),
+				time.Second.toInt()
+			)
+		} else {
+			context.getString(R.string.at_time, time.Hour.toInt(), time.Minute.toInt())
+		}
+	}
+
+	fun boardingText(context: Context): String {
+		// todo [3.x] probably should take into account (on|off)-boarding only, on demand
+		return when {
+			(boarding.and(0b1010u) != (0b0).toUByte()) -> context.getString(R.string.on_demand)
+			(boarding.and(0b0101u) == (0b0101).toUByte()) -> context.getString(R.string.no_boarding)
+			(boarding.and(0b0100u) == (0b0100).toUByte()) -> context.getString(R.string.on_boarding)
+			(boarding.and(0b0001u) == (0b0001).toUByte()) -> context.getString(R.string.off_boarding)
+			else -> context.getString(R.string.boarding)
+		}
+	}
+
 	companion object {
 		fun unmarshal(stream: InputStream): Departure {
 			val reader = Reader(stream)
@@ -307,10 +386,25 @@ 			result += "${chOpt.line} → ${chOpt.headsign}\n"
 		return result
 	}
 
-	fun changeOptions(): String {
-		return changeOptions.groupBy { it.line }
+	fun changeOptions(context: Context): Pair<String,String> {
+		return Pair(changeOptions.groupBy { it.line }
 			.map { Pair(it.key, it.value.joinToString { co -> co.headsign }) }
-			.joinToString { "${it.first} » ${it.second}" }
+			.joinToString {
+				context.getString(
+					R.string.vehicle_headsign,
+					it.first,
+					it.second
+				)
+			},
+			changeOptions.groupBy { it.line }
+				.map { Pair(it.key, it.value.joinToString { co -> co.headsign }) }
+				.joinToString {
+					context.getString(
+						R.string.vehicle_headsign_content_description,
+						it.first,
+						it.second
+					)
+				})
 	}
 
 	companion object {
@@ -390,7 +484,7 @@ 			LineType.TRAM -> R.drawable.tram_black
 			LineType.UNKNOWN -> R.drawable.vehicle_black
 		}
 		val icon =
-			AppCompatResources.getDrawable(context, iconID)?.mutate()  // todo(code) move context out
+			AppCompatResources.getDrawable(context, iconID)?.mutate()
 				?.apply {
 					setTint(textColour(colour))
 				}?.toBitmap(dpToPixelI(19.2f / scale), dpToPixelI(19.2f / scale), Bitmap.Config.ARGB_8888)
@@ -437,15 +531,11 @@ 	val graphBack: LineGraph,
 	val name: String
 ) : Item, LineAbstract {
 	override fun toString(): String {
-		return "$name ($type) [${textColour()}/$colour]\n→ [${headsignsThere.joinToString()}]\n→ [${headsignsBack.joinToString()}]\n"
+		return "$name ($type) [$colour]\n→ [${headsignsThere.joinToString()}]\n→ [${headsignsBack.joinToString()}]\n"
 	}
 
 	fun icon(context: Context, scale: Float = 1f): Bitmap {
 		return super.icon(context, type, colour, scale)
-	}
-
-	fun textColour(): Int {
-		return super.textColour(colour)
 	}
 
 	companion object {
@@ -467,7 +557,7 @@ 			val graphThere = LineGraph.unmarshal(stream)
 			val graphBack = LineGraph.unmarshal(stream)
 			val name = reader.readString()
 			return Line(
-				name = name, colour = colour, type = LineType(type.toUInt()),
+				name = name, colour = colour, type = LineType.of(type.toUInt()),
 				headsignsThere = headsignsThere, headsignsBack = headsignsBack, graphThere = graphThere,
 				graphBack = graphBack
 			)
@@ -476,14 +566,16 @@ 	}
 }
 
 enum class LineType {
-	TRAM, BUS, UNKNOWN
-}
+	TRAM, BUS, UNKNOWN;
 
-fun LineType(type: UInt): LineType {
-	return when (type) {
-		0U -> LineType.valueOf("TRAM")
-		3U -> LineType.valueOf("BUS")
-		else -> LineType.valueOf("UNKNOWN")
+	companion object {
+		fun of(type: UInt): LineType {
+			return when (type) {
+				0U -> valueOf("TRAM")
+				3U -> valueOf("BUS")
+				else -> valueOf("UNKNOWN")
+			}
+		}
 	}
 }
 




diff --git a/app/src/main/java/ml/adamsprogs/bimba/dashboard/MainActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/dashboard/MainActivity.kt
index fd757e49612e6dd6e73f627dc48f909d4535e94c..676af3a60b6b9d8c5d09ea3ab0c679246f640f5d 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/dashboard/MainActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/dashboard/MainActivity.kt
@@ -85,7 +85,7 @@
 	@Suppress(
 		"OVERRIDE_DEPRECATION",
 		"DEPRECATION"
-	)  // fix_later https://developer.android.com/reference/androidx/activity/OnBackPressedDispatcher
+	)  // fixme later https://developer.android.com/reference/androidx/activity/OnBackPressedDispatcher
 	override fun onBackPressed() {
 		if (binding.container.isDrawerOpen(binding.navigationDrawer)) {
 			binding.container.closeDrawer(binding.navigationDrawer)
@@ -102,7 +102,7 @@ 			binding.container.openDrawer(binding.navigationDrawer)
 		}
 	}
 
-	fun onGpsClicked(fab: View, fragment: Fragment) {
+	fun onGpsClicked(fragment: Fragment) {
 		when (PackageManager.PERMISSION_GRANTED) {
 			ContextCompat.checkSelfPermission(
 				this,
@@ -139,7 +139,7 @@ 				}
 				startActivity(intent)
 			}
 			is Line -> {
-				TODO("start line graph actvity")
+				TODO("[3.1] start line graph activity")
 			}
 		}
 	}
@@ -149,7 +149,7 @@ 		showResults(ResultsActivity.Mode.MODE_SEARCH, text.toString())
 	}
 
 	private fun showResults(mode: ResultsActivity.Mode, query: String = "") {
-		/* todo(ux,low) animation
+		/* todo [3.1] (ux,low) animation
 			https://developer.android.com/guide/fragments/animate
 			https://github.com/raheemadamboev/fab-explosion-animation-app
 		*/
@@ -161,7 +161,8 @@ 		startActivity(intent)
 	}
 
 	private fun setNavbarIcons(f: Fragment) {
-		binding.bottomNavigation.menu[2].setIcon(R.drawable.voyage_outline)
+		// todo [voyage-planning]
+		// binding.bottomNavigation.menu[2].setIcon(R.drawable.voyage_outline)
 		binding.bottomNavigation.menu[1].setIcon(R.drawable.home_outline)
 		binding.bottomNavigation.menu[0].setIcon(R.drawable.map_outline)
 		when (f) {




diff --git a/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/home/HomeFragment.kt b/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/home/HomeFragment.kt
index 2470cf45ad7dc2c49c7bb1a634f08da1178f5487..d410a5a2a0735ee7203a81126953ea57b8e23be0 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/home/HomeFragment.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/home/HomeFragment.kt
@@ -18,7 +18,7 @@ import ml.adamsprogs.bimba.dashboard.MainActivity
 import ml.adamsprogs.bimba.api.Item
 import ml.adamsprogs.bimba.databinding.FragmentHomeBinding
 
-// todo search: https://github.com/material-components/material-components-android/blob/master/docs/components/Search.md
+// todo [3.1] search: https://github.com/material-components/material-components-android/blob/master/docs/components/Search.md
 
 class HomeFragment : Fragment() {
 	private var _binding: FragmentHomeBinding? = null
@@ -78,15 +78,9 @@ 		})
 
 		binding.floatingActionButton.setOnClickListener {
 			binding.searchBar.clearSuggestions()
-			(context as MainActivity).onGpsClicked(it, this)
+			(context as MainActivity).onGpsClicked(this)
 		}
-		/* todo(ux,low) on searchbar focus && if != '' -> populate suggestions
-		binding.searchBar.searchEditText.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus ->
-			Log.v("Focus", "$hasFocus")
-			if(binding.searchBar.text != "" && hasFocus) {
-				binding.searchBar.lastSuggestions = lastSuggestions
-			}
-		}*/
+		// todo [3.1] (ux,low) on searchbar focus && if != '' -> populate suggestions
 
 		return binding.root
 	}




diff --git a/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/home/HomeViewModel.kt b/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/home/HomeViewModel.kt
index abaafb36ea21072a914aec949226368972bad7ea..1b3144477b7a8512448f37c5b962ed63b422b9b1 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/home/HomeViewModel.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/home/HomeViewModel.kt
@@ -69,7 +69,7 @@ 				)
 			}
 			handler.postDelayed(
 				workRunnable,
-				1000
+				750
 			) // todo(ux,low) make good time (probably between 500, 1000ms)
 		}
 	}




diff --git a/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/map/MapFragment.kt b/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/map/MapFragment.kt
index a94ead33ad554857e69409f65813f9240ee46759..5146115ec74ba17dbb13cd2d2355f219d7d2cb7d 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/map/MapFragment.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/map/MapFragment.kt
@@ -37,10 +37,8 @@ import org.osmdroid.views.overlay.TilesOverlay
 import org.osmdroid.views.overlay.gestures.RotationGestureOverlay
 import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider
 import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
-import java.io.File
-
 
-// todo empty state on no network and long time on entry
+// todo empty state on no network
 
 class MapFragment : Fragment() {
 
@@ -53,7 +51,7 @@
 	private val handler = Handler(Looper.getMainLooper())
 	private var workRunnable = Runnable {}
 
-	private var snackbar: Snackbar? = null
+	private var snack: Snackbar? = null
 
 	@SuppressLint("ClickableViewAccessibility")
 	override fun onCreateView(
@@ -96,7 +94,7 @@ 			locationOverlay.setPersonAnchor(.5f, .5f)
 		}
 
 		binding.floatingActionButton.setOnClickListener {
-			(context as MainActivity).onGpsClicked(it, this)
+			(context as MainActivity).onGpsClicked(this)
 		}
 
 		binding.map.addMapListener(object : MapListener {
@@ -118,7 +116,7 @@ 		return root
 	}
 
 	private fun onMapMove(): Boolean {
-		snackbar?.dismiss()
+		snack?.dismiss()
 		return delayGetLocatables()
 	}
 
@@ -161,8 +159,9 @@ 		}
 	}
 
 	fun showLocation() {
-		snackbar = Snackbar.make(binding.root, "waiting for position", Snackbar.LENGTH_INDEFINITE)
-		snackbar!!.show()
+		snack =
+			Snackbar.make(binding.root, getString(R.string.waiting_position), Snackbar.LENGTH_INDEFINITE)
+		snack!!.show()
 		binding.floatingActionButton.hide()
 		binding.map.overlays.removeAll {
 			it is MyLocationNewOverlay
@@ -170,7 +169,7 @@ 		}
 		locationOverlay.enableFollowLocation()
 		binding.map.overlays.add(locationOverlay)
 		locationOverlay.runOnFirstFix {
-			snackbar?.dismiss()
+			snack?.dismiss()
 		}
 	}
 




diff --git a/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/map/MapViewModel.kt b/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/map/MapViewModel.kt
index def30777abd8ca73bb7e73a16581c5818d124dfb..0e92face1169bc1ed59c86d5299fe75638b4fd27 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/map/MapViewModel.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/map/MapViewModel.kt
@@ -13,6 +13,7 @@ import android.widget.Button
 import android.widget.ImageView
 import android.widget.TextView
 import android.widget.Toast
+import androidx.constraintlayout.widget.Group
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
@@ -60,6 +61,95 @@ 	companion object {
 		const val TAG = "MapBottomSheet"
 	}
 
+	private fun showVehicle(content: View, vehicle: Vehicle) {
+		content.findViewById<Group>(R.id.stop_group).visibility = View.GONE
+		content.findViewById<Group>(R.id.vehicle_group).visibility = View.VISIBLE
+
+		context?.let { ctx ->
+			content.findViewById<TextView>(R.id.title).apply {
+				text = ctx.getString(R.string.vehicle_headsign, vehicle.Line.name, vehicle.Headsign)
+				contentDescription = ctx.getString(
+					R.string.vehicle_headsign_content_description,
+					vehicle.Line.name,
+					vehicle.Headsign
+				)
+			}
+			// todo units -- [3.1] settings or system-based
+			content.findViewById<TextView>(R.id.speed_text).text =
+				ctx.getString(R.string.speed_in_km_per_h, vehicle.Speed * 3.6)
+			content.findViewById<TextView>(R.id.congestion_text).text = vehicle.congestion(ctx)
+			content.findViewById<TextView>(R.id.occupancy_text).text = vehicle.occupancy(ctx)
+			content.findViewById<ImageView>(R.id.ac).visibility =
+				if (vehicle.getCapability(Vehicle.Capability.AC)) {
+					View.VISIBLE
+				} else {
+					View.GONE
+				}
+			content.findViewById<ImageView>(R.id.bike).visibility =
+				if (vehicle.getCapability(Vehicle.Capability.BIKE)) {
+					View.VISIBLE
+				} else {
+					View.GONE
+				}
+			content.findViewById<ImageView>(R.id.voice).visibility =
+				if (vehicle.getCapability(Vehicle.Capability.VOICE)) {
+					View.VISIBLE
+				} else {
+					View.GONE
+				}
+			content.findViewById<ImageView>(R.id.ticket).visibility =
+				if (vehicle.let {
+						it.getCapability(Vehicle.Capability.TICKET_DRIVER) || it.getCapability(Vehicle.Capability.TICKET_MACHINE)
+					}) {
+					View.VISIBLE
+				} else {
+					View.GONE
+				}
+			content.findViewById<ImageView>(R.id.usb).visibility =
+				if (vehicle.getCapability(Vehicle.Capability.USB_CHARGING)) {
+					View.VISIBLE
+				} else {
+					View.GONE
+				}
+		}
+	}
+
+	private fun showStop(content: View, stop: Stop) {
+		context?.let { ctx ->
+			content.findViewById<Group>(R.id.stop_group).visibility = View.VISIBLE
+			content.findViewById<Group>(R.id.vehicle_group).visibility = View.GONE
+			content.findViewById<TextView>(R.id.title).text =
+				context?.getString(R.string.stop_title, stop.name, stop.code)
+			content.findViewById<Button>(R.id.departures_button).setOnClickListener {
+				val intent = Intent(ctx, DeparturesActivity::class.java).apply {
+					putExtra("code", stop.code)
+					putExtra("name", stop.name)
+				}
+				startActivity(intent)
+			}
+			content.findViewById<Button>(R.id.navigation_button).setOnClickListener {
+				try {
+					startActivity(
+						Intent(
+							Intent.ACTION_VIEW,
+							Uri.parse("geo:${stop.location().latitude},${stop.location().longitude}")
+						)
+					)
+				} catch (_: ActivityNotFoundException) {
+					Toast.makeText(context, ctx.getString(R.string.no_map_app), Toast.LENGTH_SHORT).show()
+				}
+			}
+
+			stop.changeOptions(ctx).let { changeOptions ->
+				content.findViewById<TextView>(R.id.change_options).apply {
+					text = changeOptions.first
+					contentDescription = changeOptions.second
+				}
+			}
+		}
+	}
+
+
 	override fun onCreateView(
 		inflater: LayoutInflater,
 		container: ViewGroup?,
@@ -69,111 +159,10 @@ 		val content = inflater.inflate(R.layout.map_bottom_sheet, container, false)
 		content.apply {
 			when (locatable) {
 				is Vehicle -> {
-					findViewById<TextView>(R.id.title).apply {
-						text = "${locatable.Line.name} » ${locatable.Headsign}"
-						contentDescription = "${locatable.Line.name} towards ${locatable.Headsign}"
-					}
-					findViewById<TextView>(R.id.change_options).visibility = View.GONE
-					findViewById<Button>(R.id.departures_button).visibility = View.GONE
-					findViewById<Button>(R.id.navigation_button).visibility = View.GONE
-					findViewById<TextView>(R.id.speed_text).apply {
-						// todo units
-						val speed = locatable.Speed * 1.703
-						text = "%.3f Vl".format(speed)
-					}
-					findViewById<TextView>(R.id.congestion_text).apply {
-						text = when (locatable.CongestionLevel.toUInt()) {
-							0u -> "unknown"
-							1u -> "smooth traffic"
-							2u -> "stop and go"
-							3u -> "congestion"
-							4u -> "severe jams"
-							else -> TODO("throw invalid congestion")
-						}
-					}
-					findViewById<TextView>(R.id.occupancy_text).apply {
-						text = when (locatable.OccupancyStatus.toUInt()) {
-							0u -> "unknown"
-							1u -> "empty"
-							2u -> "many seats"
-							3u -> "few seats"
-							4u -> "standing only"
-							5u -> "crowded"
-							6u -> "full"
-							7u -> "won’t accept passengers"  // todo shorten
-							else -> TODO("throw invalid congestion")
-						}
-					}
-
-					findViewById<ImageView>(R.id.ac).visibility =
-						if (locatable.getCapability(Vehicle.Capability.AC)) {
-							View.VISIBLE
-						} else {
-							View.GONE
-						}
-					findViewById<ImageView>(R.id.bike).visibility =
-						if (locatable.getCapability(Vehicle.Capability.BIKE)) {
-							View.VISIBLE
-						} else {
-							View.GONE
-						}
-					findViewById<ImageView>(R.id.voice).visibility =
-						if (locatable.getCapability(Vehicle.Capability.VOICE)) {
-							View.VISIBLE
-						} else {
-							View.GONE
-						}
-					findViewById<ImageView>(R.id.ticket).visibility =
-						if (locatable.let {
-								it.getCapability(Vehicle.Capability.TICKET_DRIVER) || it.getCapability(Vehicle.Capability.TICKET_MACHINE)
-							}) {
-							View.VISIBLE
-						} else {
-							View.GONE
-						}
-					findViewById<ImageView>(R.id.usb).visibility =
-						if (locatable.getCapability(Vehicle.Capability.USB_CHARGING)) {
-							View.VISIBLE
-						} else {
-							View.GONE
-						}
+					showVehicle(this, locatable)
 				}
 				is Stop -> {
-					findViewById<TextView>(R.id.title).text = "${locatable.name} [${locatable.code}]"
-					findViewById<Button>(R.id.departures_button).setOnClickListener {
-						val intent = Intent(context, DeparturesActivity::class.java).apply {
-							putExtra("code", locatable.code)
-							putExtra("name", locatable.name)
-						}
-						startActivity(intent)
-					}
-					findViewById<Button>(R.id.navigation_button).setOnClickListener {
-						try {
-							startActivity(
-								Intent(
-									Intent.ACTION_VIEW,
-									Uri.parse("geo:${locatable.location().latitude},${locatable.location().longitude}")
-								)
-							)
-						} catch (_: ActivityNotFoundException) {
-							Toast.makeText(context, "No maps app installed", Toast.LENGTH_SHORT).show()
-						}
-					}
-
-					findViewById<TextView>(R.id.change_options).text = locatable.changeOptions()
-
-					findViewById<ImageView>(R.id.speed_icon).visibility = View.GONE
-					findViewById<TextView>(R.id.speed_text).visibility = View.GONE
-					findViewById<ImageView>(R.id.congestion_icon).visibility = View.GONE
-					findViewById<TextView>(R.id.congestion_text).visibility = View.GONE
-					findViewById<ImageView>(R.id.occupancy_icon).visibility = View.GONE
-					findViewById<TextView>(R.id.occupancy_text).visibility = View.GONE
-
-					findViewById<ImageView>(R.id.ac).visibility = View.GONE
-					findViewById<ImageView>(R.id.bike).visibility = View.GONE
-					findViewById<ImageView>(R.id.voice).visibility = View.GONE
-					findViewById<ImageView>(R.id.ticket).visibility = View.GONE
-					findViewById<ImageView>(R.id.usb).visibility = View.GONE
+					showStop(this, locatable)
 				}
 			}
 		}




diff --git a/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/voyage/VoyageViewModel.kt b/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/voyage/VoyageViewModel.kt
index 5348f5356cbc3108ab63a8f2980936d1e7409d5b..0dd58b3469156839e197127f7dd1e7846d408da9 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/voyage/VoyageViewModel.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/dashboard/ui/voyage/VoyageViewModel.kt
@@ -7,7 +7,7 @@
 class VoyageViewModel : ViewModel() {
 
 	private val _text = MutableLiveData<String>().apply {
-		value = "This is dashboard Fragment"
+		value = "This is voyage Fragment"
 	}
 	val text: LiveData<String> = _text
 }
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/departures/Departures.kt b/app/src/main/java/ml/adamsprogs/bimba/departures/Departures.kt
index 339010367fda02c73372000294cbf17b334ac190..2fc1050efba838eb958e88b9a000baf5d517c6bd 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/departures/Departures.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/departures/Departures.kt
@@ -36,7 +36,6 @@ 	val lineName: TextView = itemView.findViewById(R.id.departure_line)
 	val headsign: TextView = itemView.findViewById(R.id.departure_headsign)
 
 	companion object {
-		@SuppressLint("SetTextI18n")
 		fun bind(
 			departure: Departure,
 			holder: BimbaDepartureViewHolder?,
@@ -47,35 +46,13 @@ 			holder?.root?.setOnClickListener {
 				onClickListener(departure)
 			}
 			holder?.lineIcon?.setImageBitmap(departure.line.icon(context!!))
+			holder?.lineIcon?.contentDescription = departure.line.type.name
 			holder?.lineName?.text = departure.line.name
-			holder?.headsign?.text = "» ${departure.headsign}"  // todo >> is not a11y
-			val now = Calendar.getInstance()
-			val departureTime = Calendar.getInstance().apply {
-				set(Calendar.HOUR_OF_DAY, departure.time.Hour.toInt())
-				set(Calendar.MINUTE, departure.time.Minute.toInt())
-				set(Calendar.SECOND, departure.time.Second.toInt())
-				// todo zone
-				roll(Calendar.DAY_OF_MONTH, departure.time.DayOffset.toInt())
-			}
-			var duration = departureTime.timeInMillis - now.timeInMillis
-			val hours = duration / (60 * 60 * 1000)
-			duration %= (60 * 60 * 1000)
-			val minutes = duration / (60 * 1000)
-			duration %= (60 * 1000)
-			holder?.departureTime?.text = when (departure.status.toInt()) { // todo(i18n) plurals
-				0 -> {
-					"in " +
-									if (hours > 0) {
-										"$hours h "
-									} else {
-										""
-									} + "$minutes min"
-				}
-				1 -> "momentarily"
-				2 -> "now"
-				3 -> "departed"
-				else -> ""
-			}
+			holder?.headsign?.text = context?.getString(R.string.departure_headsign, departure.headsign)
+			holder?.headsign?.contentDescription =
+				context?.getString(R.string.departure_headsign_content_description, departure.headsign)
+
+			holder?.departureTime?.text = departure.statusText(context)
 		}
 	}
 }
@@ -91,21 +68,23 @@
 	private var departuresPositions: MutableMap<String, Int> = HashMap()
 
 	init {
-		departures = departures.map { // fixme (!!) does szczanieckiej not populate departure.vehicle.(line,headsign)?
-			Departure(
-				it.ID, it.line, it.headsign, it.time, it.status, it.isRealtime,
-				it.stopOrder, Vehicle(
-					it.vehicle.ID,
-					it.vehicle.Position,
-					it.vehicle.Capabilities,
-					it.vehicle.Speed,
-					it.line,
-					it.headsign,
-					it.vehicle.CongestionLevel,
-					it.vehicle.OccupancyStatus
-				),
-				it.boarding)
-		}
+		departures =
+			departures.map { // fixme (!!) does szczanieckiej not populate departure.vehicle.(line,headsign)?
+				Departure(
+					it.ID, it.line, it.headsign, it.time, it.status, it.isRealtime,
+					it.stopOrder, Vehicle(
+						it.vehicle.ID,
+						it.vehicle.Position,
+						it.vehicle.Capabilities,
+						it.vehicle.Speed,
+						it.line,
+						it.headsign,
+						it.vehicle.CongestionLevel,
+						it.vehicle.OccupancyStatus
+					),
+					it.boarding
+				)
+			}
 		departures.forEachIndexed { i, departure ->
 			departuresPositions[departure.ID] = i
 		}
@@ -131,21 +110,13 @@ 			departures[position]
 		}
 	}
 
+	@SuppressLint("NotifyDataSetChanged") // todo [3.1] DiffUtil
 	fun update(items: List<Departure>) {
 		val newPositions: MutableMap<String, Int> = HashMap()
 		items.forEachIndexed { i, departure ->
 			newPositions[departure.ID] = i
 		}
-//		// fixme jumps
-//		departures.minus(items.toSet()).forEach {
-//			notifyItemRemoved(departuresPositions[it.ID]!!)
-//		}
-//		items.minus(departures.toSet()).forEach {
-//			notifyItemInserted(newPositions[it.ID]!!)
-//		}
-//		items.intersect(departures.toSet()).forEach {
-//			notifyItemChanged(newPositions[it.ID]!!)
-//		}
+
 		departures = items.map { // fixme (!!)
 			Departure(
 				it.ID, it.line, it.headsign, it.time, it.status, it.isRealtime,
@@ -159,16 +130,17 @@ 					it.headsign,
 					it.vehicle.CongestionLevel,
 					it.vehicle.OccupancyStatus
 				),
-				it.boarding)
+				it.boarding
+			)
 		}
 		departuresPositions = newPositions
 		notifyDataSetChanged()
 	}
 }
 
-class ModalBottomSheet(private var departure: Departure) : BottomSheetDialogFragment() {
+class DepartureBottomSheet(private var departure: Departure) : BottomSheetDialogFragment() {
 	companion object {
-		const val TAG = "ModalBottomSheet"
+		const val TAG = "DepartureBottomSheet"
 	}
 
 	private var cancelCallback: (() -> Unit)? = null
@@ -188,18 +160,12 @@ 	}
 
 	fun update(departure: Departure) {
 		this.departure = departure
-		this.view?.let { setContent(it) }
+		this.view?.let { context?.let { ctx -> setContent(it, ctx) } }
 	}
 
-	private fun setContent(view: View) {
-		var timeText = "at ${departure.time.Hour.toString().padStart(2, '0')}:${
-			departure.time.Minute.toString().padStart(2, '0')
-		}"
-		if (departure.isRealtime) {
-			timeText += ":${departure.time.Second.toString().padStart(2, '0')}"
-		}
+	private fun setContent(view: View, ctx: Context) {
 		view.apply {
-			findViewById<TextView>(R.id.time).text = timeText
+			findViewById<TextView>(R.id.time).text = departure.timeString(ctx)
 
 			findViewById<ImageView>(R.id.rt_icon).apply {
 				visibility = if (departure.isRealtime) {
@@ -221,47 +187,20 @@ 				}
 			}
 
 			findViewById<TextView>(R.id.line).apply {
-				contentDescription = "${departure.line.name} towards ${departure.headsign}"
-				text = "${departure.line.name} » ${departure.headsign}"
+				contentDescription = getString(
+					R.string.vehicle_headsign_content_description,
+					departure.line.name,
+					departure.headsign
+				)
+				text = getString(R.string.vehicle_headsign, departure.line.name, departure.headsign)
 			}
 
-			findViewById<TextView>(R.id.boarding_text).apply {
-				text = if (departure.boarding.and(0b1010u) != (0b0).toUByte()) {
-					"on demand"
-				} else if (departure.boarding.and(0b0101u) != (0b0).toUByte()) {
-					"no boarding"
-				} else {
-					"can board"
-				}
-			}
-			findViewById<TextView>(R.id.speed_text).apply {
-				// todo units
-				val speed = departure.vehicle.Speed * 1.703
-				text = "%.3f Vl".format(speed)
-			}
-			findViewById<TextView>(R.id.congestion_text).apply {
-				text = when (departure.vehicle.CongestionLevel.toUInt()) {
-					0u -> "unknown"
-					1u -> "smooth traffic"
-					2u -> "stop and go"
-					3u -> "congestion"
-					4u -> "severe jams"
-					else -> TODO("throw invalid congestion")
-				}
-			}
-			findViewById<TextView>(R.id.occupancy_text).apply {
-				text = when (departure.vehicle.OccupancyStatus.toUInt()) {
-					0u -> "unknown"
-					1u -> "empty"
-					2u -> "many seats"
-					3u -> "few seats"
-					4u -> "standing only"
-					5u -> "crowded"
-					6u -> "full"
-					7u -> "won’t accept passengers"  // todo shorten
-					else -> TODO("throw invalid congestion")
-				}
-			}
+			findViewById<TextView>(R.id.boarding_text).text = departure.boardingText(ctx)
+			// todo units -- [3.1] settings or system-based
+			findViewById<TextView>(R.id.speed_text).text =
+				getString(R.string.speed_in_km_per_h, departure.vehicle.Speed * 3.6)
+			findViewById<TextView>(R.id.congestion_text).text = departure.vehicle.congestion(ctx)
+			findViewById<TextView>(R.id.occupancy_text).text = departure.vehicle.occupancy(ctx)
 
 			findViewById<ImageView>(R.id.ac).visibility =
 				if (departure.vehicle.getCapability(Vehicle.Capability.AC)) {
@@ -300,7 +239,7 @@ 				if (departure.vehicle.Position.isZero()) {
 					map.visibility = View.GONE
 					return@let
 				}
-				map.controller.apply {
+				map.controller.apply { // todo glide to centre, not jump
 					setZoom(19.0f.toDouble())
 					setCenter(
 						GeoPoint(
@@ -317,7 +256,7 @@ 				val marker = Marker(map).apply {
 					position =
 						GeoPoint(departure.vehicle.location().latitude, departure.vehicle.location().longitude)
 					setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
-					icon = context?.let { ctx -> departure.vehicle.icon(ctx, 2f) } // fixme colour?
+					icon = context?.let { ctx -> departure.vehicle.icon(ctx, 2f) }
 					setOnClickListener {}
 				}
 				map.overlays.add(marker)
@@ -334,25 +273,27 @@ 		savedInstanceState: Bundle?
 	): View {
 		val content = inflater.inflate(R.layout.departure_bottom_sheet, container, false)
 
-		content.apply {
-			findViewById<MapView>(R.id.map).let { map ->
-				map.setTileSource(TileSourceFactory.MAPNIK)
-				if (((context?.resources?.configuration?.uiMode ?: UI_MODE_NIGHT_UNDEFINED)
-									and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES
-				) {
-					map.overlayManager.tilesOverlay.setColorFilter(TilesOverlay.INVERT_COLORS)
+		context?.let { ctx ->
+			content.apply {
+				findViewById<MapView>(R.id.map).let { map ->
+					map.setTileSource(TileSourceFactory.MAPNIK)
+					if (((context?.resources?.configuration?.uiMode ?: UI_MODE_NIGHT_UNDEFINED)
+										and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES
+					) {
+						map.overlayManager.tilesOverlay.setColorFilter(TilesOverlay.INVERT_COLORS)
+					}
+					map.zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
+					map.setOnTouchListener { _, _ -> true }
+					map.setMultiTouchControls(true)
+					map.overlays.add(RotationGestureOverlay(map).apply { isEnabled = true })
 				}
-				map.zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
-				map.setOnTouchListener { _, _ ->  true}
-				map.setMultiTouchControls(true)
-				map.overlays.add(RotationGestureOverlay(map).apply { isEnabled = true })
-			}
 
-			setContent(this)
+				setContent(this, ctx)
 
-			(dialog as BottomSheetDialog).behavior.peekHeight = dpToPixelI(180f)
+				(dialog as BottomSheetDialog).behavior.peekHeight = dpToPixelI(180f)
 
-			return content
+			}
 		}
+		return content
 	}
 }
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/departures/DeparturesActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/departures/DeparturesActivity.kt
index e2f12209ee96239ba701d37c4356c5946d81fdb2..fc0fd313ada49db7f943750b2b9e73f83d342142 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/departures/DeparturesActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/departures/DeparturesActivity.kt
@@ -32,7 +32,7 @@
 	private val handler = Handler(Looper.getMainLooper())
 	private var runnable = Runnable {}
 
-	private var openBottomSheet: ModalBottomSheet? = null
+	private var openBottomSheet: DepartureBottomSheet? = null
 
 	override fun onCreate(savedInstanceState: Bundle?) {
 		super.onCreate(savedInstanceState)
@@ -48,8 +48,8 @@ 		}
 
 		binding.departuresRecycler.layoutManager = LinearLayoutManager(this)
 		adapter = BimbaDeparturesAdapter(layoutInflater, this, listOf()) {
-			ModalBottomSheet(it).apply {
-				show(supportFragmentManager, ModalBottomSheet.TAG)
+			DepartureBottomSheet(it).apply {
+				show(supportFragmentManager, DepartureBottomSheet.TAG)
 				openBottomSheet = this
 				setOnCancel { openBottomSheet = null }
 			}
@@ -151,7 +151,7 @@ 			binding.errorImage.visibility = View.GONE
 			binding.errorText.visibility = View.GONE
 			binding.departuresRecycler.visibility = View.VISIBLE
 		}
-		// todo alerts
-		// todo stop info
+		// todo [3.1] alerts
+		// todo [3.1] stop info
 	}
 }
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/feeds/FeedChooserActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/feeds/FeedChooserActivity.kt
index ee75453e0014bd89d67b00c811ab49b736e6e910..7b31fa6a0eaff1d039aa770a45e1cca311315c06 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/feeds/FeedChooserActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/feeds/FeedChooserActivity.kt
@@ -24,6 +24,8 @@ import ml.adamsprogs.bimba.dashboard.MainActivity
 import ml.adamsprogs.bimba.databinding.ActivityFeedChooserBinding
 import java.io.InputStream
 
+// todo split into server+token -> dialog if token not given and (rate limited or access denied), [3.1] check .well-known -> feeds choosing
+
 
 class FeedChooserActivity : AppCompatActivity() {
 	private var _binding: ActivityFeedChooserBinding? = null
@@ -69,7 +71,7 @@ 	private fun setUpRecycler() {
 		binding.resultsRecycler.layoutManager = LinearLayoutManager(this)
 		adapter = BimbaFeedInfoAdapter(layoutInflater, listOf(), this) {
 			Log.v("FeedInfo", "clicked: $it")
-			// todo show bottom sheet
+			// todo show bottom sheet with attribution and info
 		}
 		binding.resultsRecycler.adapter = adapter
 	}
@@ -82,7 +84,7 @@ 			putString("server", host)
 			putString("token", token)
 		}
 
-		binding.circularProgressIndicator.visibility = View.VISIBLE
+		binding.progress.visibility = View.VISIBLE
 		binding.resultsRecycler.visibility = View.GONE
 		binding.feedInfo.visibility = View.GONE
 
@@ -131,14 +133,10 @@ 		}
 	}
 
 	private fun updateItems(response: FeedsSuccess) {
-		Log.v("items", "${response.adminContact} ${response.rateLimited}")
-		response.feeds.forEach {
-			Log.v("items", "$it")
-		}
-		binding.circularProgressIndicator.visibility = View.GONE
+		binding.progress.visibility = View.GONE
 		binding.resultsRecycler.visibility = View.VISIBLE
 		binding.feedInfo.visibility = View.VISIBLE
-		binding.feedInfo.text = // todo(ui) table
+		binding.feedInfo.text = // todo(ui) remove after splitting
 			if (response.rateLimited) {
 				getString(R.string.server_info_rate_limited, response.adminContact)
 			} else {




diff --git a/app/src/main/java/ml/adamsprogs/bimba/feeds/FeedInfos.kt b/app/src/main/java/ml/adamsprogs/bimba/feeds/FeedInfos.kt
index 09707bfb08879bbb89bbc881b3e715bd43f2b275..34a1d1f84653bd4fdcc920d056a371d3f6d75842 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/feeds/FeedInfos.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/feeds/FeedInfos.kt
@@ -1,5 +1,6 @@
 package ml.adamsprogs.bimba.feeds
-// git:fixup feeds
+
+import android.annotation.SuppressLint
 import android.content.Context
 import android.content.Context.MODE_PRIVATE
 import android.view.LayoutInflater
@@ -14,7 +15,7 @@ import ml.adamsprogs.bimba.api.FeedInfo
 
 
 class BimbaFeedInfoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
-	val root: View = itemView.findViewById(R.id.feedinfo)
+	val root: View = itemView.findViewById(R.id.feed)
 	val switch: MaterialSwitch = itemView.findViewById(R.id.feed_switch)
 	val name: TextView = itemView.findViewById(R.id.feed_name)
 
@@ -69,6 +70,7 @@ 	}
 
 	override fun getItemCount(): Int = feeds.size
 
+	@SuppressLint("NotifyDataSetChanged") // todo [3.1] DiffUtil
 	fun update(items: List<FeedInfo>) {
 		feeds = items
 		notifyDataSetChanged()




diff --git a/app/src/main/java/ml/adamsprogs/bimba/search/Results.kt b/app/src/main/java/ml/adamsprogs/bimba/search/Results.kt
index 8758a66de867fce2a67bf3abb40ff6d45a7907eb..4ed01d22ce933369cfabbe52e0cb38c63df37c62 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/search/Results.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/search/Results.kt
@@ -2,7 +2,6 @@ package ml.adamsprogs.bimba.search
 
 import android.annotation.SuppressLint
 import android.content.Context
-import android.util.Log
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
@@ -38,25 +37,37 @@ 				onClickListener(item)
 			}
 		}
 
-		@SuppressLint("SetTextI18n")
-		fun bindStop(stop: Stop, holder: BimbaViewHolder?, context: Context?) {
-			holder?.icon?.setImageDrawable(stop.icon(context!!))
-			holder?.title?.text = "${stop.name} [${stop.code}]"
-			holder?.description?.text = stop.changeOptions()
+		private fun bindStop(stop: Stop, holder: BimbaViewHolder?, context: Context?) {
+			holder?.icon?.apply {
+				setImageDrawable(stop.icon(context!!))
+				contentDescription = context.getString(R.string.stop_content_description)
+			}
+			holder?.title?.text = context?.getString(R.string.stop_title, stop.name, stop.code)
+			context?.let {
+				stop.changeOptions(it).let { changeOptions ->
+					holder?.description?.apply {
+						text = changeOptions.first
+						contentDescription = changeOptions.second
+					}
+				}
+			}
 		}
 
-		@SuppressLint("SetTextI18n")
-		fun bindLine(line: Line, holder: BimbaViewHolder?, context: Context?) {
-			val icon = line.icon(context!!)
+		private fun bindLine(line: Line, holder: BimbaViewHolder?, context: Context?) {
 			holder?.icon?.apply {
-				setImageBitmap(icon)
+				setImageBitmap(line.icon(context!!))
+				contentDescription = line.type.name
 				colorFilter = null
-				Log.v("Colour", "${line.name}: ${line.colour}, ${line.colour.toInt().toString(16)}")
-				Log.v("Colour", "${line.name}: ${line.textColour()}, ${line.textColour().toString(16)}")
 			}
 			holder?.title?.text = line.name
-			holder?.description?.text =
-				"${line.headsignsThere.joinToString { it }} «» ${line.headsignsBack.joinToString { it }}"
+			holder?.description?.text = context?.getString(
+				R.string.line_headsigns,
+				line.headsignsThere.joinToString { it },
+				line.headsignsBack.joinToString { it })
+			holder?.description?.contentDescription = context?.getString(
+				R.string.line_headsigns_content_description,
+				line.headsignsThere.joinToString { it },
+				line.headsignsBack.joinToString { it })
 		}
 	}
 }
@@ -98,6 +109,7 @@ 	}
 
 	override fun getItemCount(): Int = items.size
 
+	@SuppressLint("NotifyDataSetChanged") // todo [3.1] DiffUtil
 	fun update(items: List<Item>) {
 		this.items = items
 		notifyDataSetChanged()




diff --git a/app/src/main/java/ml/adamsprogs/bimba/search/ResultsActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/search/ResultsActivity.kt
index 3f188ad8b534f373ba15da1e7da6c8eeae58506f..3bc018c930bcffbcb66bd0c2ab98a2e6dff3ad34 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/search/ResultsActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/search/ResultsActivity.kt
@@ -1,6 +1,5 @@
 package ml.adamsprogs.bimba.search
 
-import android.annotation.SuppressLint
 import android.content.Context
 import android.content.Intent
 import android.location.Location
@@ -53,7 +52,7 @@ 					}
 					startActivity(intent)
 				}
 				is Line -> {
-					TODO("start line graph activity")
+					TODO("[3.1] start line graph activity")
 				}
 			}
 		}
@@ -62,33 +61,36 @@ 		setSupportActionBar(binding.topAppBar)
 
 		WindowCompat.setDecorFitsSystemWindows(window, false)
 
-		@Suppress("DEPRECATION")  // fix_later getSerializable in API>=33
+		@Suppress("DEPRECATION")  // fixme later getSerializable in API>=33
 		when (intent.extras?.get("mode")) {
 			Mode.MODE_LOCATION -> {
-				supportActionBar?.title = "Stops nearby"
+				supportActionBar?.title = getString(R.string.stops_nearby)
 				locate()
 			}
 			Mode.MODE_SEARCH -> {
 				val query = intent.extras?.getString("query")!!
-				supportActionBar?.title = "Results for ‘$query’"
+				supportActionBar?.title = getString(R.string.results_for, query)
 				getItemsByQuery(Server.get(this), query)
 			}
 		}
 	}
 
-	@SuppressLint("MissingPermission")
 	private fun locate() {
-		val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
-		locationManager.requestLocationUpdates(
-			LocationManager.GPS_PROVIDER, 1000 * 60 * 10, 100f, this
-		)
-		handler.removeCallbacks(runnable)
-		runnable = Runnable {
-			showError(Error(0, R.string.error_gps, R.drawable.error_gps))
+		try {
+			val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
+			locationManager.requestLocationUpdates(
+				LocationManager.GPS_PROVIDER, 1000 * 60 * 10, 100f, this
+			)
+			handler.removeCallbacks(runnable)
+			runnable = Runnable {
+				showError(Error(0, R.string.error_gps, R.drawable.error_gps))
+			}
+			handler.postDelayed(runnable, 60 * 1000)
+			locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
+				?.let { onLocationChanged(it) }
+		} catch (_: SecurityException) {
+			// this won’t happen because we don’t start this activity without location permission
 		}
-		handler.postDelayed(runnable, 60 * 1000)
-		locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
-			?.let { onLocationChanged(it) }
 	}
 
 	override fun onLocationChanged(location: Location) {
@@ -98,7 +100,7 @@ 	}
 
 	override fun onResume() {
 		super.onResume()
-		@Suppress("DEPRECATION")  // fix_later getSerializable in API>=33
+		@Suppress("DEPRECATION")  // fixme later getSerializable in API>=33
 		if (intent.extras?.get("mode") == Mode.MODE_LOCATION) {
 			locate()
 		}




diff --git a/app/src/main/res/drawable/inari.xml b/app/src/main/res/drawable/inari.xml
index d07433c89483e8ed88dac67cd32206f96364f717..da25ef47d0c88263da2cc9acce30d60b253a96a3 100644
--- a/app/src/main/res/drawable/inari.xml
+++ b/app/src/main/res/drawable/inari.xml
@@ -1,4 +1,3 @@
-<!-- git:inari -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
     android:width="66.982dp"
     android:height="55.716dp"




diff --git a/app/src/main/res/layout/activity_departures.xml b/app/src/main/res/layout/activity_departures.xml
index 552d23dd735a7ccaefdba0088b845182b3d6fda6..e5aad3ed0e731d53aedb2311a3c1c30fe7f2b9cf 100644
--- a/app/src/main/res/layout/activity_departures.xml
+++ b/app/src/main/res/layout/activity_departures.xml
@@ -3,14 +3,15 @@  	xmlns:app="http://schemas.android.com/apk/res-auto"
 	xmlns:tools="http://schemas.android.com/tools"
 	android:layout_width="match_parent"
-	android:layout_height="match_parent">
+	android:layout_height="match_parent"
+	android:paddingBottom="16dp">
 
 	<androidx.constraintlayout.widget.ConstraintLayout
 		android:id="@+id/departures_overlay"
 		android:layout_width="match_parent"
 		android:layout_height="match_parent">
 
-		<ProgressBar
+		<com.google.android.material.progressindicator.LinearProgressIndicator
 			android:id="@+id/departures_progress"
 			style="?android:attr/progressBarStyle"
 			android:layout_width="wrap_content"
@@ -21,7 +22,7 @@ 			app:layout_constraintStart_toStartOf="parent"
 			app:layout_constraintTop_toTopOf="parent" />
 
 		<ImageView
-			android:id="@+id/errorImage"
+			android:id="@+id/error_image"
 			android:layout_width="92dp"
 			android:layout_height="92dp"
 			android:visibility="gone"
@@ -32,8 +33,8 @@ 			app:layout_constraintTop_toTopOf="parent"
 			tools:ignore="ContentDescription"
 			tools:src="@drawable/error_net" />
 
-		<TextView
-			android:id="@+id/errorText"
+		<com.google.android.material.textview.MaterialTextView
+			android:id="@+id/error_text"
 			android:layout_width="0dp"
 			android:layout_height="wrap_content"
 			android:layout_marginStart="16dp"
@@ -44,7 +45,7 @@ 			android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
 			android:visibility="gone"
 			app:layout_constraintEnd_toEndOf="parent"
 			app:layout_constraintStart_toStartOf="parent"
-			app:layout_constraintTop_toBottomOf="@+id/errorImage"
+			app:layout_constraintTop_toBottomOf="@+id/error_image"
 			tools:text="No connection" />
 	</androidx.constraintlayout.widget.ConstraintLayout>
 
@@ -72,7 +73,6 @@ 		
 
 	</com.google.android.material.appbar.AppBarLayout>
 
-	<!-- todo padding bottom -->
 	<androidx.recyclerview.widget.RecyclerView
 		android:id="@+id/departures_recycler"
 		android:layout_width="match_parent"




diff --git a/app/src/main/res/layout/activity_feed_chooser.xml b/app/src/main/res/layout/activity_feed_chooser.xml
index 95a30db5071a769fd056dba5d8a314522377c934..8d8a85d5706a3ddc0ecc787289988d9ffc1ead1f 100644
--- a/app/src/main/res/layout/activity_feed_chooser.xml
+++ b/app/src/main/res/layout/activity_feed_chooser.xml
@@ -1,5 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- git:fixup feeds -->
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
 	xmlns:app="http://schemas.android.com/apk/res-auto"
 	xmlns:tools="http://schemas.android.com/tools"
@@ -14,7 +13,7 @@ 		android:layout_height="wrap_content"
 		android:layout_marginStart="16dp"
 		android:layout_marginTop="16dp"
 		android:layout_marginEnd="16dp"
-		android:hint="Bimba server"
+		android:hint="@string/bimba_server_address_hint"
 		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintStart_toStartOf="parent"
 		app:layout_constraintTop_toTopOf="parent">
@@ -32,7 +31,7 @@ 		android:layout_height="wrap_content"
 		android:layout_marginStart="16dp"
 		android:layout_marginTop="16dp"
 		android:layout_marginEnd="16dp"
-		android:hint="Token"
+		android:hint="@string/bimba_server_token_hint"
 		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintStart_toStartOf="parent"
 		app:layout_constraintTop_toBottomOf="@id/server_field">
@@ -48,7 +47,7 @@ 		android:id="@+id/button"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
 		android:layout_marginTop="16dp"
-		android:text="Continue"
+		android:text="@string/bimba_server_continue_button"
 		app:layout_constraintEnd_toEndOf="@+id/token_field"
 		app:layout_constraintStart_toStartOf="@+id/token_field"
 		app:layout_constraintTop_toBottomOf="@+id/token_field" />
@@ -61,7 +60,7 @@ 		android:layout_marginTop="16dp"
 		app:layout_constraintTop_toBottomOf="@+id/button" />
 
 	<com.google.android.material.progressindicator.CircularProgressIndicator
-		android:id="@+id/circularProgressIndicator"
+		android:id="@+id/progress"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
 		android:indeterminate="true"
@@ -71,7 +70,7 @@ 		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintStart_toStartOf="parent"
 		app:layout_constraintTop_toBottomOf="@+id/divider" />
 
-	<TextView
+	<com.google.android.material.textview.MaterialTextView
 		android:id="@+id/feed_info"
 		android:layout_width="match_parent"
 		android:layout_height="wrap_content"




diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 3bb6106b6bf38245c81e0043df466024c1ecd71b..db6ee416fe2a78decde7397d7094bc2493116a1e 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -14,7 +14,7 @@ 			android:layout_width="match_parent"
 			android:layout_height="wrap_content">
 
 			<com.google.android.material.appbar.MaterialToolbar
-				android:id="@+id/topAppBar"
+				android:id="@+id/top_app_bar"
 				android:layout_width="match_parent"
 				android:layout_height="?attr/actionBarSize"
 				app:title="" />
@@ -48,7 +48,7 @@ 				app:menu="@menu/bottom_nav_menu" />
 		</androidx.constraintlayout.widget.ConstraintLayout>
 	</androidx.coordinatorlayout.widget.CoordinatorLayout>
 
-
+	<!-- todo show feeds and go to chooser (settings) -->
 	<com.google.android.material.navigation.NavigationView
 		android:id="@+id/navigation_drawer"
 		android:layout_width="wrap_content"




diff --git a/app/src/main/res/layout/activity_results.xml b/app/src/main/res/layout/activity_results.xml
index 544c223c2396559b92f1042c1f6832ab9cd69e43..e080acfa860e7e43c54143e0a930808dd9efa641 100644
--- a/app/src/main/res/layout/activity_results.xml
+++ b/app/src/main/res/layout/activity_results.xml
@@ -3,14 +3,15 @@  	xmlns:app="http://schemas.android.com/apk/res-auto"
 	xmlns:tools="http://schemas.android.com/tools"
 	android:layout_width="match_parent"
-	android:layout_height="match_parent">
+	android:layout_height="match_parent"
+	android:paddingBottom="16dp">
 
 	<androidx.constraintlayout.widget.ConstraintLayout
 		android:id="@+id/results_overlay"
 		android:layout_width="match_parent"
 		android:layout_height="match_parent">
 
-		<ProgressBar
+		<com.google.android.material.progressindicator.LinearProgressIndicator
 			android:id="@+id/results_progress"
 			style="?android:attr/progressBarStyle"
 			android:layout_width="wrap_content"
@@ -21,7 +22,7 @@ 			app:layout_constraintStart_toStartOf="parent"
 			app:layout_constraintTop_toTopOf="parent" />
 
 		<ImageView
-			android:id="@+id/errorImage"
+			android:id="@+id/error_image"
 			android:layout_width="92dp"
 			android:layout_height="92dp"
 			android:visibility="gone"
@@ -32,8 +33,8 @@ 			app:layout_constraintTop_toTopOf="parent"
 			tools:ignore="ContentDescription"
 			tools:src="@drawable/error_net" />
 
-		<TextView
-			android:id="@+id/errorText"
+		<com.google.android.material.textview.MaterialTextView
+			android:id="@+id/error_text"
 			android:layout_width="0dp"
 			android:layout_height="wrap_content"
 			android:layout_marginStart="16dp"
@@ -44,7 +45,7 @@ 			android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
 			android:visibility="gone"
 			app:layout_constraintEnd_toEndOf="parent"
 			app:layout_constraintStart_toStartOf="parent"
-			app:layout_constraintTop_toBottomOf="@+id/errorImage"
+			app:layout_constraintTop_toBottomOf="@+id/error_image"
 			tools:text="No connection" />
 
 	</androidx.constraintlayout.widget.ConstraintLayout>
@@ -56,14 +57,13 @@ 		android:fitsSystemWindows="true"
 		app:liftOnScroll="true">
 
 		<com.google.android.material.appbar.MaterialToolbar
-			android:id="@+id/topAppBar"
+			android:id="@+id/top_app_bar"
 			android:layout_width="match_parent"
 			android:layout_height="?attr/actionBarSize"
 			app:title="@string/title_activity_results" />
 
 	</com.google.android.material.appbar.AppBarLayout>
 
-	<!-- todo padding bottom -->
 	<androidx.recyclerview.widget.RecyclerView
 		android:id="@+id/results_recycler"
 		android:layout_width="match_parent"




diff --git a/app/src/main/res/layout/departure.xml b/app/src/main/res/layout/departure.xml
index de205dcb54a7edb9a2abaf46c1537bce6d9fcf62..429c5f76ca00a8df572208bce0977da615e9beb9 100644
--- a/app/src/main/res/layout/departure.xml
+++ b/app/src/main/res/layout/departure.xml
@@ -14,9 +14,10 @@ 		android:layout_marginStart="8dp"
 		app:layout_constraintBottom_toTopOf="@+id/departure_headsign"
 		app:layout_constraintStart_toStartOf="parent"
 		app:layout_constraintTop_toTopOf="@+id/departure_time"
-		tools:srcCompat="@drawable/bus_black" />
+		tools:srcCompat="@drawable/bus_black"
+		tools:ignore="ContentDescription" />
 
-	<TextView
+	<com.google.android.material.textview.MaterialTextView
 		android:id="@+id/departure_time"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
@@ -25,9 +26,9 @@ 		android:layout_marginEnd="8dp"
 		android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
 		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintTop_toTopOf="parent"
-		tools:text="1h 56mins" />
+		tools:text="1hr" />
 
-	<TextView
+	<com.google.android.material.textview.MaterialTextView
 		android:id="@+id/departure_line"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
@@ -38,7 +39,7 @@ 		app:layout_constraintStart_toEndOf="@+id/line_icon"
 		app:layout_constraintTop_toTopOf="parent"
 		tools:text="Metropolitan" />
 
-	<TextView
+	<com.google.android.material.textview.MaterialTextView
 		android:id="@+id/departure_headsign"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"




diff --git a/app/src/main/res/layout/departure_bottom_sheet.xml b/app/src/main/res/layout/departure_bottom_sheet.xml
index 755eb3b3f9bfedc2389ccd89d2ef79966bbaa19d..0eb2ee87737e2abcc637c176d0f78a0fef6e1a8f 100644
--- a/app/src/main/res/layout/departure_bottom_sheet.xml
+++ b/app/src/main/res/layout/departure_bottom_sheet.xml
@@ -32,14 +32,14 @@ 		android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
 		android:visibility="gone"
 		app:layout_constraintBaseline_toBaselineOf="@+id/time"
 		app:layout_constraintStart_toEndOf="@+id/time"
-		tool:text="(+2 mins)" />
+		tool:text="(+2 min)" />
 
 	<ImageView
 		android:id="@+id/rt_icon"
 		android:layout_width="24dp"
 		android:layout_height="24dp"
 		android:layout_marginStart="16dp"
-		android:contentDescription="departure is realtime"
+		android:contentDescription="@string/realtime_content_description"
 		app:layout_constraintBottom_toBottomOf="@+id/time"
 		app:layout_constraintStart_toStartOf="parent"
 		app:layout_constraintTop_toTopOf="@+id/time"
@@ -50,7 +50,7 @@ 		android:id="@+id/wheelchair_icon"
 		android:layout_width="24dp"
 		android:layout_height="24dp"
 		android:layout_marginStart="8dp"
-		android:contentDescription="vehicle is wheelchair accessible"
+		android:contentDescription="@string/wheelchair_content_description"
 		app:layout_constraintStart_toEndOf="@id/rt_icon"
 		app:layout_constraintTop_toTopOf="@+id/rt_icon"
 		app:srcCompat="@drawable/wheelchair" />
@@ -66,10 +66,8 @@ 		android:textAlignment="center"
 		android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
 		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintStart_toStartOf="parent"
-		app:layout_constraintTop_toBottomOf="@id/time"
-		tool:text="Metropolitan » Aleje Marcinkowskiego" />
+		app:layout_constraintTop_toBottomOf="@id/time" />
 
-	<!--suppress AndroidUnknownAttribute -->
 	<ImageView
 		android:id="@+id/boarding_icon"
 		android:layout_width="16dp"
@@ -93,7 +91,6 @@ 		app:layout_constraintEnd_toEndOf="@id/middle"
 		app:layout_constraintTop_toTopOf="parent"
 		tool:text="on demand" />
 
-	<!--suppress AndroidUnknownAttribute -->
 	<ImageView
 		android:id="@+id/speed_icon"
 		android:layout_width="16dp"
@@ -116,7 +113,6 @@ 		app:layout_constraintEnd_toStartOf="@+id/middle"
 		app:layout_constraintTop_toBottomOf="@id/boarding_text"
 		tool:text="10 Vl" />
 
-	<!--suppress AndroidUnknownAttribute -->
 	<ImageView
 		android:id="@+id/congestion_icon"
 		android:layout_width="16dp"
@@ -141,7 +137,6 @@ 		app:layout_constraintStart_toEndOf="@id/congestion_icon"
 		app:layout_constraintTop_toTopOf="parent"
 		tool:text="smooth traffic" />
 
-	<!--suppress AndroidUnknownAttribute -->
 	<ImageView
 		android:id="@+id/occupancy_icon"
 		android:layout_width="16dp"
@@ -193,7 +188,7 @@ 	 		android:id="@+id/ac"
 		android:layout_width="24dp"
 		android:layout_height="24dp"
-		android:contentDescription="air condition"
+		android:contentDescription="@string/air_condition_content_description"
 		app:srcCompat="@drawable/ac"
 		tool:ignore="MissingConstraints" />
 
@@ -201,7 +196,7 @@ 	 		android:id="@+id/bike"
 		android:layout_width="24dp"
 		android:layout_height="24dp"
-		android:contentDescription="bicycles allowed"
+		android:contentDescription="@string/bicycles_allowed_content_description"
 		app:srcCompat="@drawable/bike"
 		tool:ignore="MissingConstraints" />
 
@@ -209,7 +204,7 @@ 	 		android:id="@+id/voice"
 		android:layout_width="24dp"
 		android:layout_height="24dp"
-		android:contentDescription="voice announcements"
+		android:contentDescription="@string/voice_announcements_content_description"
 		app:srcCompat="@drawable/voice"
 		tool:ignore="MissingConstraints" />
 
@@ -217,7 +212,7 @@ 	 		android:id="@+id/ticket"
 		android:layout_width="24dp"
 		android:layout_height="24dp"
-		android:contentDescription="tickets sold"
+		android:contentDescription="@string/tickets_sold_content_description"
 		app:srcCompat="@drawable/ticket"
 		tool:ignore="MissingConstraints" />
 
@@ -225,7 +220,7 @@ 	 		android:id="@+id/usb"
 		android:layout_width="24dp"
 		android:layout_height="24dp"
-		android:contentDescription="USB charging"
+		android:contentDescription="@string/usb_charging_content_description"
 		app:srcCompat="@drawable/usb"
 		tool:ignore="MissingConstraints" />
 




diff --git a/app/src/main/res/layout/feedinfo.xml b/app/src/main/res/layout/feedinfo.xml
index 4129ff7c6ffa1c1a9308dfc0872327e96d23ccee..26ed2b036981ea9db8718aa65d08cde15b330ffe 100644
--- a/app/src/main/res/layout/feedinfo.xml
+++ b/app/src/main/res/layout/feedinfo.xml
@@ -1,12 +1,11 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- git:fixup feeds -->
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
 	xmlns:app="http://schemas.android.com/apk/res-auto"
-	android:id="@+id/feedinfo"
+	android:id="@+id/feed"
 	android:layout_width="match_parent"
 	android:layout_height="wrap_content">
 
-	<TextView
+	<com.google.android.material.textview.MaterialTextView
 		android:id="@+id/feed_name"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"




diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
index be113fd08116a6888dee343876f422088b67fc2e..0b6fc8cd6f610ad649bae777ea281aa8f230a660 100644
--- a/app/src/main/res/layout/fragment_home.xml
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -34,7 +34,7 @@ 		app:layout_constraintBottom_toBottomOf="parent"
 		app:layout_constraintEnd_toEndOf="parent" />
 
 	<ImageView
-		android:id="@+id/imageView2"
+		android:id="@+id/inari"
 		android:layout_width="0dp"
 		android:layout_height="0dp"
 		android:layout_marginStart="16dp"
@@ -46,6 +46,7 @@ 		android:src="@drawable/inari"
 		app:layout_constraintBottom_toTopOf="@+id/floating_action_button"
 		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintStart_toStartOf="parent"
-		app:layout_constraintTop_toBottomOf="@+id/search_bar" />
+		app:layout_constraintTop_toBottomOf="@+id/search_bar"
+		tools:ignore="ContentDescription" />
 
 </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file




diff --git a/app/src/main/res/layout/fragment_map.xml b/app/src/main/res/layout/fragment_map.xml
index 539858aa42b4041aea5db8e79b2f7c9f35b6aa96..84500fce485f296b22515dee3ce782aad7ef0efb 100644
--- a/app/src/main/res/layout/fragment_map.xml
+++ b/app/src/main/res/layout/fragment_map.xml
@@ -20,7 +20,7 @@ 		android:id="@+id/floating_action_button"
 		style="?attr/floatingActionButtonSmallStyle"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
-		android:layout_gravity="bottom|right"
+		android:layout_gravity="bottom|end"
 		android:layout_margin="16dp"
 		android:contentDescription="@string/home_fab_description"
 		android:src="@drawable/gps_black" />




diff --git a/app/src/main/res/layout/fragment_voyage.xml b/app/src/main/res/layout/fragment_voyage.xml
index 693d09e63f0b540012e227ccea8a0e423536bc04..977deb77d46bbd2f91cfe3fa362b466366f6a400 100644
--- a/app/src/main/res/layout/fragment_voyage.xml
+++ b/app/src/main/res/layout/fragment_voyage.xml
@@ -6,7 +6,7 @@ 	android:layout_width="match_parent"
 	android:layout_height="match_parent"
 	tools:context=".dashboard.ui.voyage.VoyageFragment">
 
-	<TextView
+	<com.google.android.material.textview.MaterialTextView
 		android:id="@+id/text_dashboard"
 		android:layout_width="match_parent"
 		android:layout_height="wrap_content"




diff --git a/app/src/main/res/layout/map_bottom_sheet.xml b/app/src/main/res/layout/map_bottom_sheet.xml
index d7ab738d5571271bc41a5417c7eab8c6b41e6282..ee43994f8c22280a3fda028fd21149ee94138d3b 100644
--- a/app/src/main/res/layout/map_bottom_sheet.xml
+++ b/app/src/main/res/layout/map_bottom_sheet.xml
@@ -24,8 +24,13 @@ 		android:textAlignment="center"
 		android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
 		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintStart_toStartOf="parent"
-		app:layout_constraintTop_toTopOf="parent"
-		tool:text="Aleje Marcinkowskiego [AMAR01]" />
+		app:layout_constraintTop_toTopOf="parent" />
+
+	<androidx.constraintlayout.widget.Group
+		android:id="@+id/stop_group"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		app:constraint_referenced_ids="change_options,departures_button,navigation_button" />
 
 	<com.google.android.material.textview.MaterialTextView
 		android:id="@+id/change_options"
@@ -35,8 +40,7 @@ 		android:layout_margin="16dp"
 		android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
 		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintStart_toStartOf="parent"
-		app:layout_constraintTop_toBottomOf="@+id/title"
-		tool:text="610 » Górczyn, 215 » Rondo Kaponiera, 392 » Słowiańska" />
+		app:layout_constraintTop_toBottomOf="@+id/title" />
 
 	<Button
 		android:id="@+id/departures_button"
@@ -44,7 +48,7 @@ 		style="@style/Widget.Material3.Button.TextButton.Icon"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
 		android:layout_margin="4dp"
-		android:text="Show departures"
+		android:text="@string/show_departures"
 		app:icon="@drawable/departure"
 		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintStart_toStartOf="parent"
@@ -56,13 +60,18 @@ 		style="@style/Widget.Material3.Button.TextButton.Icon"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
 		android:layout_margin="4dp"
-		android:text="Open in maps app"
+		android:text="@string/open_in_maps_app"
 		app:icon="@drawable/open_outside"
 		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintStart_toStartOf="parent"
 		app:layout_constraintTop_toBottomOf="@+id/departures_button" />
 
-	<!--suppress AndroidUnknownAttribute -->
+	<androidx.constraintlayout.widget.Group
+		android:id="@+id/vehicle_group"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		app:constraint_referenced_ids="speed_icon,speed_text,congestion_icon,congestion_text,occupancy_icon,occupancy_text,ac,bike,voice,ticket,usb" />
+
 	<ImageView
 		android:id="@+id/speed_icon"
 		android:layout_width="16dp"
@@ -85,7 +94,6 @@ 		app:layout_constraintEnd_toStartOf="@+id/middle"
 		app:layout_constraintTop_toBottomOf="@id/title"
 		tool:text="10 Vl" />
 
-	<!--suppress AndroidUnknownAttribute -->
 	<ImageView
 		android:id="@+id/congestion_icon"
 		android:layout_width="16dp"
@@ -110,7 +118,6 @@ 		app:layout_constraintStart_toEndOf="@id/congestion_icon"
 		app:layout_constraintTop_toBottomOf="@id/title"
 		tool:text="smooth traffic" />
 
-	<!--suppress AndroidUnknownAttribute -->
 	<ImageView
 		android:id="@+id/occupancy_icon"
 		android:layout_width="16dp"
@@ -161,7 +168,7 @@ 	 		android:id="@+id/ac"
 		android:layout_width="24dp"
 		android:layout_height="24dp"
-		android:contentDescription="air condition"
+		android:contentDescription="@string/air_condition_content_description"
 		app:srcCompat="@drawable/ac"
 		tool:ignore="MissingConstraints" />
 
@@ -169,7 +176,7 @@ 	 		android:id="@+id/bike"
 		android:layout_width="24dp"
 		android:layout_height="24dp"
-		android:contentDescription="bicycles allowed"
+		android:contentDescription="@string/bicycles_allowed_content_description"
 		app:srcCompat="@drawable/bike"
 		tool:ignore="MissingConstraints" />
 
@@ -177,7 +184,7 @@ 	 		android:id="@+id/voice"
 		android:layout_width="24dp"
 		android:layout_height="24dp"
-		android:contentDescription="voice announcements"
+		android:contentDescription="@string/voice_announcements_content_description"
 		app:srcCompat="@drawable/voice"
 		tool:ignore="MissingConstraints" />
 
@@ -185,7 +192,7 @@ 	 		android:id="@+id/ticket"
 		android:layout_width="24dp"
 		android:layout_height="24dp"
-		android:contentDescription="tickets sold"
+		android:contentDescription="@string/tickets_sold_content_description"
 		app:srcCompat="@drawable/ticket"
 		tool:ignore="MissingConstraints" />
 
@@ -193,7 +200,7 @@ 	 		android:id="@+id/usb"
 		android:layout_width="24dp"
 		android:layout_height="24dp"
-		android:contentDescription="USB charging"
+		android:contentDescription="@string/usb_charging_content_description"
 		app:srcCompat="@drawable/usb"
 		tool:ignore="MissingConstraints" />
 




diff --git a/app/src/main/res/layout/result.xml b/app/src/main/res/layout/result.xml
index b0d9d514924029ec71102dede57e8e96b7e7317c..639d48dc3683b3d9ccc6de9b63e669fce00a6fd5 100644
--- a/app/src/main/res/layout/result.xml
+++ b/app/src/main/res/layout/result.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- git:sugg_result -->
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
 	xmlns:app="http://schemas.android.com/apk/res-auto"
+	xmlns:tools="http://schemas.android.com/tools"
 	android:id="@+id/suggestion"
 	android:layout_width="match_parent"
 	android:layout_height="wrap_content">
@@ -15,9 +15,10 @@ 		android:layout_marginTop="8dp"
 		android:layout_marginBottom="8dp"
 		app:layout_constraintBottom_toBottomOf="parent"
 		app:layout_constraintStart_toStartOf="parent"
-		app:layout_constraintTop_toTopOf="parent" />
+		app:layout_constraintTop_toTopOf="parent"
+		tools:ignore="ContentDescription" />
 
-	<TextView
+	<com.google.android.material.textview.MaterialTextView
 		android:id="@+id/suggestion_title"
 		android:layout_width="0dp"
 		android:layout_height="wrap_content"
@@ -30,7 +31,7 @@ 		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintStart_toEndOf="@+id/suggestion_image"
 		app:layout_constraintTop_toTopOf="parent" />
 
-	<TextView
+	<com.google.android.material.textview.MaterialTextView
 		android:id="@+id/suggestion_description"
 		style="@style/Theme.Bimba.SearchResult.Description"
 		android:layout_width="0dp"




diff --git a/app/src/main/res/layout/suggestion.xml b/app/src/main/res/layout/suggestion.xml
index 8aa8652229bde6d4f63e2b9c40da7c884b4f3bb5..7ad3e5ab507b2259036ec8cd4fc2ba2de2e0d8f4 100644
--- a/app/src/main/res/layout/suggestion.xml
+++ b/app/src/main/res/layout/suggestion.xml
@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
 	xmlns:app="http://schemas.android.com/apk/res-auto"
+	xmlns:tools="http://schemas.android.com/tools"
 	android:id="@+id/suggestion"
 	android:layout_width="match_parent"
 	android:layout_height="72dp">
@@ -14,9 +15,10 @@ 		android:layout_marginTop="8dp"
 		android:layout_marginBottom="8dp"
 		app:layout_constraintBottom_toBottomOf="parent"
 		app:layout_constraintStart_toStartOf="parent"
-		app:layout_constraintTop_toTopOf="parent" />
+		app:layout_constraintTop_toTopOf="parent"
+		tools:ignore="ContentDescription" />
 
-	<TextView
+	<com.google.android.material.textview.MaterialTextView
 		android:id="@+id/suggestion_title"
 		android:layout_width="0dp"
 		android:layout_height="wrap_content"
@@ -29,14 +31,14 @@ 		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintStart_toEndOf="@+id/suggestion_image"
 		app:layout_constraintTop_toTopOf="parent" />
 
-	<TextView
+	<com.google.android.material.textview.MaterialTextView
 		android:id="@+id/suggestion_description"
 		style="@style/Theme.Bimba.SearchResult.Description"
 		android:layout_width="0dp"
 		android:layout_height="0dp"
 		android:layout_marginEnd="8dp"
-		android:maxLines="4"
 		android:ellipsize="end"
+		android:maxLines="4"
 		android:text=""
 		app:layout_constraintBottom_toBottomOf="parent"
 		app:layout_constraintEnd_toEndOf="parent"




diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml
index 51c38c91f6a9ef5d57eab41c7a6b49862b091dbc..3631e523fcafffa65395d79c36b61911ef55aeda 100644
--- a/app/src/main/res/menu/bottom_nav_menu.xml
+++ b/app/src/main/res/menu/bottom_nav_menu.xml
@@ -8,8 +8,9 @@ 	 		android:id="@+id/navigation_home"
 		android:icon="@drawable/home_outline"
 		android:title="@string/title_home" />
+	<!-- todo [voyage planning]
 	<item
 		android:id="@+id/navigation_voyage"
 		android:icon="@drawable/voyage_outline"
-		android:title="@string/title_voyage" />
+		android:title="@string/title_voyage" />-->
 </menu>
\ No newline at end of file




diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 8e2fb5b9fc056fc27c5dc40a72c0eb04a81babd1..d715893aa6f22b49f7237f704316c3c47ed9c19e 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -11,62 +11,67 @@ 	#FF000000
 	<color name="white">#FFFFFFFF</color>
 
 	<color name="seed">#54af39</color>
-	<color name="md_theme_light_primary">#1A6D00</color>
-	<color name="md_theme_light_onPrimary">#FFFFFF</color>
-	<color name="md_theme_light_primaryContainer">#9AFA79</color>
-	<color name="md_theme_light_onPrimaryContainer">#032100</color>
-	<color name="md_theme_light_secondary">#6A5F00</color>
-	<color name="md_theme_light_onSecondary">#FFFFFF</color>
-	<color name="md_theme_light_secondaryContainer">#F9E447</color>
-	<color name="md_theme_light_onSecondaryContainer">#201C00</color>
-	<color name="md_theme_light_tertiary">#8C4F00</color>
-	<color name="md_theme_light_onTertiary">#FFFFFF</color>
-	<color name="md_theme_light_tertiaryContainer">#FFDCC0</color>
-	<color name="md_theme_light_onTertiaryContainer">#2D1600</color>
+	<color name="md_theme_light_primary">#1A6D00</color> <!-- 40 -->
+	<color name="md_theme_light_onPrimary">#FFFFFF</color> <!-- 100 -->
+	<color name="md_theme_light_primaryContainer">#9AFA79</color> <!-- 90 -->
+	<color name="md_theme_light_onPrimaryContainer">#032100</color> <!-- 10 -->
+	<color name="md_theme_light_secondary">#6A5F00</color> <!-- 40 -->
+	<color name="md_theme_light_onSecondary">#FFFFFF</color> <!-- 100 -->
+	<color name="md_theme_light_secondaryContainer">#F9E447</color> <!-- 90 -->
+	<color name="md_theme_light_onSecondaryContainer">#201C00</color> <!-- 10 -->
+	<color name="md_theme_light_tertiary">#8C4F00</color> <!-- 40 -->
+	<color name="md_theme_light_onTertiary">#FFFFFF</color> <!-- 100 -->
+	<color name="md_theme_light_tertiaryContainer">#FFDCC0</color> <!-- 90 -->
+	<color name="md_theme_light_onTertiaryContainer">#2D1600</color> <!-- 10 -->
+
 	<color name="md_theme_light_error">#BA1A1A</color>
 	<color name="md_theme_light_errorContainer">#FFDAD6</color>
 	<color name="md_theme_light_onError">#FFFFFF</color>
 	<color name="md_theme_light_onErrorContainer">#410002</color>
-	<color name="md_theme_light_background">#FCFCFF</color>
-	<color name="md_theme_light_onBackground">#001D33</color>
-	<color name="md_theme_light_surface">#FCFCFF</color>
-	<color name="md_theme_light_onSurface">#001D33</color>
-	<color name="md_theme_light_surfaceVariant">#DFE4D7</color>
-	<color name="md_theme_light_onSurfaceVariant">#43483F</color>
-	<color name="md_theme_light_outline">#73796E</color>
-	<color name="md_theme_light_inverseOnSurface">#E8F2FF</color>
-	<color name="md_theme_light_inverseSurface">#003353</color>
-	<color name="md_theme_light_inversePrimary">#7FDC60</color>
-	<color name="md_theme_light_shadow">#000000</color>
-	<color name="md_theme_light_surfaceTint">#1A6D00</color>
-	<color name="md_theme_light_surfaceTintColor">#1A6D00</color>
-	<color name="md_theme_dark_primary">#7FDC60</color>
-	<color name="md_theme_dark_onPrimary">#093900</color>
-	<color name="md_theme_dark_primaryContainer">#115300</color>
-	<color name="md_theme_dark_onPrimaryContainer">#9AFA79</color>
-	<color name="md_theme_dark_secondary">#DCC82A</color>
-	<color name="md_theme_dark_onSecondary">#373100</color>
-	<color name="md_theme_dark_secondaryContainer">#504700</color>
-	<color name="md_theme_dark_onSecondaryContainer">#F9E447</color>
-	<color name="md_theme_dark_tertiary">#FFB875</color>
-	<color name="md_theme_dark_onTertiary">#4B2800</color>
-	<color name="md_theme_dark_tertiaryContainer">#6B3B00</color>
-	<color name="md_theme_dark_onTertiaryContainer">#FFDCC0</color>
+
+	<color name="md_theme_light_background">#FCFCFF</color> <!-- primary 99 -->
+	<color name="md_theme_light_onBackground">#001D33</color> <!-- primary 10 -->
+	<color name="md_theme_light_surface">#FCFCFF</color> <!-- primary 99 -->
+	<color name="md_theme_light_onSurface">#001D33</color> <!-- primary 10 -->
+	<color name="md_theme_light_surfaceVariant">#DFE4D7</color> <!-- neutral 90 -->
+	<color name="md_theme_light_onSurfaceVariant">#43483F</color> <!-- neutral 30 -->
+	<color name="md_theme_light_outline">#73796E</color> <!-- neutral 50 -->
+	<color name="md_theme_light_inverseOnSurface">#E8F2FF</color> <!-- neutral 95 -->
+	<color name="md_theme_light_inverseSurface">#003353</color> <!-- neutral 20 -->
+	<color name="md_theme_light_inversePrimary">#7FDC60</color> <!-- 80 -->
+	<color name="md_theme_light_shadow">#000000</color> <!-- 0 -->
+	<color name="md_theme_light_surfaceTint">#1A6D00</color> <!-- primary 40 -->
+	<color name="md_theme_light_surfaceTintColor">#1A6D00</color> <!-- primary 40 -->
+
+	<color name="md_theme_dark_primary">#7FDC60</color> <!-- 80 -->
+	<color name="md_theme_dark_onPrimary">#093900</color> <!-- 20 -->
+	<color name="md_theme_dark_primaryContainer">#115300</color> <!-- 30 -->
+	<color name="md_theme_dark_onPrimaryContainer">#9AFA79</color> <!-- 90 -->
+	<color name="md_theme_dark_secondary">#DCC82A</color> <!-- 80 -->
+	<color name="md_theme_dark_onSecondary">#373100</color> <!-- 20 -->
+	<color name="md_theme_dark_secondaryContainer">#504700</color> <!-- 30 -->
+	<color name="md_theme_dark_onSecondaryContainer">#F9E447</color> <!-- 90 -->
+	<color name="md_theme_dark_tertiary">#FFB875</color> <!-- 80 -->
+	<color name="md_theme_dark_onTertiary">#4B2800</color> <!-- 20 -->
+	<color name="md_theme_dark_tertiaryContainer">#6B3B00</color> <!-- 30 -->
+	<color name="md_theme_dark_onTertiaryContainer">#FFDCC0</color> <!-- 90 -->
+
 	<color name="md_theme_dark_error">#FFB4AB</color>
 	<color name="md_theme_dark_errorContainer">#93000A</color>
 	<color name="md_theme_dark_onError">#690005</color>
 	<color name="md_theme_dark_onErrorContainer">#FFDAD6</color>
-	<color name="md_theme_dark_background">#001D33</color>
-	<color name="md_theme_dark_onBackground">#CEE5FF</color>
-	<color name="md_theme_dark_surface">#001D33</color>
-	<color name="md_theme_dark_onSurface">#CEE5FF</color>
-	<color name="md_theme_dark_surfaceVariant">#43483F</color>
-	<color name="md_theme_dark_onSurfaceVariant">#C3C8BC</color>
-	<color name="md_theme_dark_outline">#8D9387</color>
-	<color name="md_theme_dark_inverseOnSurface">#001D33</color>
-	<color name="md_theme_dark_inverseSurface">#CEE5FF</color>
-	<color name="md_theme_dark_inversePrimary">#1A6D00</color>
-	<color name="md_theme_dark_shadow">#000000</color>
-	<color name="md_theme_dark_surfaceTint">#7FDC60</color>
-	<color name="md_theme_dark_surfaceTintColor">#7FDC60</color>
+
+	<color name="md_theme_dark_background">#001D33</color> <!-- primary 10 -->
+	<color name="md_theme_dark_onBackground">#CEE5FF</color> <!-- primary 90 -->
+	<color name="md_theme_dark_surface">#001D33</color> <!-- primary 10 -->
+	<color name="md_theme_dark_onSurface">#CEE5FF</color> <!-- primary 90 -->
+	<color name="md_theme_dark_surfaceVariant">#43483F</color> <!-- neutral 30 -->
+	<color name="md_theme_dark_onSurfaceVariant">#C3C8BC</color> <!-- -->
+	<color name="md_theme_dark_outline">#8D9387</color> <!-- unused -->
+	<color name="md_theme_dark_inverseOnSurface">#001D33</color> <!-- primary 10 -->
+	<color name="md_theme_dark_inverseSurface">#CEE5FF</color> <!-- primary 90 -->
+	<color name="md_theme_dark_inversePrimary">#1A6D00</color> <!-- primary 40 -->
+	<color name="md_theme_dark_shadow">#000000</color> <!-- 0 -->
+	<color name="md_theme_dark_surfaceTint">#7FDC60</color> <!-- primary 80 -->
+	<color name="md_theme_dark_surfaceTintColor">#7FDC60</color> <!-- primary 80 -->
 </resources>
\ No newline at end of file




diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
deleted file mode 100644
index b4b724d5d63243169442e9a7046072e5ba0f0fe7..0000000000000000000000000000000000000000
--- a/app/src/main/res/values/dimens.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<resources>
-	<!-- Default screen margins, per the Android Design guidelines. -->
-	<dimen name="activity_horizontal_margin">16dp</dimen>
-	<dimen name="activity_vertical_margin">16dp</dimen>
-</resources>
\ No newline at end of file




diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 51b4adeb82ec6ebadf9d62aada03881473d907d9..c20bcf31f1e57cb276fd5864b46b8265e8dcfe62 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -3,7 +3,6 @@ 	Bimba
 	<string name="title_home">Home</string>
 	<string name="title_map">Map</string>
 	<string name="title_voyage">Voyage</string>
-	<string name="searchbar_menu_item_gps">search by GPS</string>
 	<string name="home_fab_description">GPS icon</string>
 	<string name="search_placeholder">Search stops and lines</string>
 	<string name="title_activity_results">Results</string>
@@ -18,8 +17,62 @@ 	Not found
 	<string name="error_429">Rate limit exceeded. Try again later</string>
 	<string name="error_50x">There was an error on the sever. Try again later</string>
 	<string name="error_unknown">Unknown error happened</string>
-	<string name="error_connecting">Error connecting to the server. Try again later</string>
-	<string name="error_offline">You are offline. Connect to the Internet</string> <!-- send a bug report to bimba@git.apiote.xyz, details are: url=$URL, response=$response -->
+	<string name="error_connecting">Error connecting to the server. Try again later</string> <!-- send a bug report to bimba@git.apiote.xyz, details are: url=$URL, response=$response -->
+	<string name="error_offline">You are offline. Connect to the Internet</string>
 	<string name="error_gps">Cannot obtain location</string>
 	<string name="no_departures">No departures</string>
+	<string name="waiting_position">waiting for position</string>
+	<string name="vehicle_headsign">%s » %s</string>
+	<string name="vehicle_headsign_content_description">%s towards %s</string>
+	<string name="speed_in_km_per_h">%.3f km/h</string>
+	<string name="congestion_unknown">unknown</string>
+	<string name="congestion_smooth">smooth</string>
+	<string name="congestion_stop_and_go">stop and go</string>
+	<string name="congestion_congestion">congestion</string>
+	<string name="congestion_jams">jams</string>
+	<string name="occupancy_unknown">unknown</string>
+	<string name="occupancy_empty">empty</string>
+	<string name="occupancy_many_seats">many seats</string>
+	<string name="occupancy_few_seats">few seats</string>
+	<string name="occupancy_standing_only">standing only</string>
+	<string name="occupancy_crowded">crowded</string>
+	<string name="occupancy_full">full</string>
+	<string name="occupancy_wont_let">won’t let in</string>
+	<string name="stop_title">%s [%s]</string>
+	<string name="no_map_app">No maps app installed</string>
+	<string name="departure_headsign">» %s</string>
+	<string name="departure_headsign_content_description">towards %s</string>
+	<string name="departure_momentarily">momentarily</string>
+	<string name="departure_departed">departed</string>
+	<string name="departure_now">now</string>
+	<string name="at_time">at %02d:%02d</string>
+	<string name="at_time_realtime">at %02d:%02d:%02d</string>
+	<string name="on_demand">on demand</string>
+	<string name="no_boarding">no boarding</string>
+	<string name="on_boarding">on-boarding</string>
+	<string name="off_boarding">off-boarding</string>
+	<string name="boarding">can board</string>
+	<string name="line_headsigns">%s «» %s</string>
+	<string name="line_headsigns_content_description">between %s and %s</string>
+	<string name="stops_nearby">Stops nearby</string>
+	<string name="results_for">Results for ‘%s’</string>
+	<string name="bimba_server_address_hint">Bimba server</string>
+	<string name="bimba_server_token_hint">Token</string>
+	<string name="bimba_server_continue_button">Continue</string>
+	<string name="realtime_content_description">departure is realtime</string>
+	<string name="wheelchair_content_description">vehicle is wheelchair accessible</string>
+	<string name="air_condition_content_description">air conditioning</string>
+	<string name="bicycles_allowed_content_description">bicycles allowed</string>
+	<string name="voice_announcements_content_description">voice announcements</string>
+	<string name="tickets_sold_content_description">tickets sold on board</string>
+	<string name="usb_charging_content_description">USB charging</string>
+	<string name="show_departures">Show departures</string>
+	<string name="open_in_maps_app">Open in maps app</string>
+	<string name="stop_content_description">stop</string>
+	<string name="seatbelts_everyone">Seatbelts, everyone!</string>
+	<string name="onboarding_question">How would you like to start?</string>
+	<string name="onboarding_simple">Simple</string>
+	<string name="onboarding_simple_action">choose cities</string>
+	<string name="onboarding_advanced">Advanced</string>
+	<string name="onboarding_simple_advanced">choose server</string> <!-- taken from ‘Magic School Bus’. Should be translated like in the series -->
 </resources>
\ No newline at end of file




diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 73c4810b2d9228a96bd4c5f6f1dd592158a6ce91..20db7647bca1ddb8c20b84ce74f662156fcc0910 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -67,4 +67,6 @@ 		@drawable/ic_launcher_foreground
 		<item name="windowSplashScreenIconBackgroundColor">@color/ic_launcher_background</item>
 		<item name="postSplashScreenTheme">@style/Theme.Bimba</item>
 	</style>
+
+	<style name="Theme.Bimba.Style" />
 </resources>
\ No newline at end of file




diff --git a/app/src/main/res/values-v21/themes.xml b/app/src/main/res/values-v21/themes.xml
deleted file mode 100644
index f0f3937ac135cf1b40e66084bb14ecdffcc63c2c..0000000000000000000000000000000000000000
--- a/app/src/main/res/values-v21/themes.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
-
-	<style name="Theme.Bimba.Style" parent="Theme.Bimba" />
-</resources>
\ No newline at end of file




diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
index e99ab9e207141de03020fa52a928f877f388d7ca..9157acd18cdf838751e15affe9aad8ea49f91b49 100644
--- a/app/src/main/res/xml/backup_rules.xml
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -6,8 +6,4 @@    Note: This file is ignored for devices older that API 31
    See https://developer.android.com/about/versions/12/backup-restore
 -->
 <full-backup-content>
-	<!--
- <include domain="sharedpref" path="."/>
- <exclude domain="sharedpref" path="device.xml"/>
--->
 </full-backup-content>
\ No newline at end of file




diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
index 14c561a1ce073d6d7c60d38c89e478b9fa2d1dea..dbd7d6cff871fd2450b0f9c01fc1143b79092776 100644
--- a/app/src/main/res/xml/data_extraction_rules.xml
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -5,7 +5,7 @@    for details.
 -->
 <data-extraction-rules>
 	<cloud-backup>
-		<!-- TODO: Use <include> and <exclude> to control what is backed up.
+		<!-- Use <include> and <exclude> to control what is backed up.
 		<include .../>
 		<exclude .../>
 		-->




diff --git a/app/src/test/java/ml/adamsprogs/bimba/ExampleUnitTest.kt b/app/src/test/java/ml/adamsprogs/bimba/ExampleUnitTest.kt
deleted file mode 100644
index aafcc64544b83749246a78fd5e1db880484c226c..0000000000000000000000000000000000000000
--- a/app/src/test/java/ml/adamsprogs/bimba/ExampleUnitTest.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package ml.adamsprogs.bimba
-
-import org.junit.Test
-
-import org.junit.Assert.*
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-class ExampleUnitTest {
-	@Test
-	fun addition_isCorrect() {
-		assertEquals(4, 2 + 2)
-	}
-}
\ No newline at end of file




diff --git a/inari.svg b/inari.svg
index 226773effee996e1bedf078c220e7cb88d8b9dc0..33ef0449f475b445414c384d7050c0c552173d37 100644
--- a/inari.svg
+++ b/inari.svg
@@ -11,8 +11,7 @@    inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
    sodipodi:docname="drawing.svg"
    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:svg="http://www.w3.org/2000/svg">
+   xmlns="http://www.w3.org/2000/svg">
   <sodipodi:namedview
      id="namedview7"
      pagecolor="#505050"