Bimba.git

commit baeecf427f982195dd29ad310cc4416c457a436e

Author: Adam Evyčędo <git@apiote.xyz>

add units

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


diff --git a/app/build.gradle b/app/build.gradle
index 54d50b5a32b00c98bde44d662ce7e94bfbe0df97..ac26e06a71c52aa0be420a23dec120bb67e4d13d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -78,6 +78,7 @@     implementation 'org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3'
     implementation 'com.github.jershell:kbson:0.5.0'
 
     implementation project(path: ':fruchtfleisch')
+    implementation 'androidx.preference:preference:1.2.0'
 
     coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
 




diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f7c0314f0b78f5e5a72bccc33f8d7b940088a0d9..3cd49cd487e5d6f55d771451b68bbea53a362c70 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,11 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
-
-<!--
-SPDX-FileCopyrightText: Adam Evyčędo
-
-SPDX-License-Identifier: GPL-3.0-or-later
--->
-
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
 	xmlns:tool="http://schemas.android.com/tools">
 
@@ -27,6 +20,10 @@ 		android:roundIcon="@mipmap/ic_launcher_round"
 		android:supportsRtl="true"
 		android:theme="@style/Theme.Bimba.Style"
 		tool:targetApi="33">
+		<activity
+			android:name=".settings.SettingsActivity"
+			android:exported="false"
+			android:label="@string/title_activity_settings" />
 		<activity
 			android:name=".AboutActivity"
 			android:exported="false" />




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/MainActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/MainActivity.kt
index b2a20c6ca37251d8b153b0789dbfa920fa015eb3..b12c34a0e996cefb965993b6374e59bed1b73f99 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/MainActivity.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/MainActivity.kt
@@ -39,6 +39,7 @@ import xyz.apiote.bimba.czwek.dashboard.ui.voyage.VoyageFragment
 import xyz.apiote.bimba.czwek.databinding.ActivityMainBinding
 import xyz.apiote.bimba.czwek.search.ResultsActivity
 import xyz.apiote.bimba.czwek.settings.ServerChooserActivity
+import xyz.apiote.bimba.czwek.settings.SettingsActivity
 import xyz.apiote.bimba.czwek.settings.feeds.FeedChooserActivity
 
 
@@ -107,6 +108,10 @@ 				}
 
 				R.id.drawer_cities -> {
 					startActivity(Intent(this, FeedChooserActivity::class.java))
+				}
+
+				R.id.drawer_settings -> {
+					startActivity(Intent(this, SettingsActivity::class.java))
 				}
 
 				R.id.drawer_about -> {




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt
index d3d3c1afc89244763f309441eb5c24275862d30d..e22468daa531581c6de54bf84b14ccc24d153874 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt
@@ -29,7 +29,6 @@ import xyz.apiote.bimba.czwek.databinding.FragmentHomeBinding
 import xyz.apiote.bimba.czwek.dpToPixelI
 import xyz.apiote.bimba.czwek.repo.Favourite
 import xyz.apiote.bimba.czwek.search.BimbaResultsAdapter
-import xyz.apiote.bimba.czwek.units.Millisecond
 import xyz.apiote.bimba.czwek.units.Second
 
 class HomeFragment : Fragment() {
@@ -41,7 +40,7 @@ 	private lateinit var favouritesAdapter: BimbaFavouritesAdapter
 	private lateinit var viewModel: HomeViewModel
 
 	private val countdown =
-		object : CountDownTimer(Millisecond(Second(30)).millis, Millisecond(Second(10)).millis) {
+		object : CountDownTimer(Second(30).milliseconds(), Second(10).milliseconds()) {
 			override fun onTick(millisUntilFinished: Long) {
 			}
 




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapViewModel.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapViewModel.kt
index f53541d8f75292860a260fd389f57fe047e1bf32..f046fad8cedf37d2d2164eff4e63ba9e40af7781 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapViewModel.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapViewModel.kt
@@ -35,6 +35,8 @@ import xyz.apiote.bimba.czwek.repo.Position
 import xyz.apiote.bimba.czwek.repo.Stop
 import xyz.apiote.bimba.czwek.repo.TrafficResponseException
 import xyz.apiote.bimba.czwek.repo.Vehicle
+import xyz.apiote.bimba.czwek.units.UnitSystem
+
 
 class MapViewModel : ViewModel() {
 
@@ -79,9 +81,11 @@ 			content.findViewById(R.id.rt_icon).visibility = View.GONE
 			// TODO vehicle accessible
 			content.findViewById<ImageView>(R.id.wheelchair_icon).visibility = View.GONE
 
-			// todo units -- [3.2] 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)
+			Log.i("unit", "${vehicle.Speed.mps}")
+			UnitSystem.getSelected(requireContext()).let { us ->
+				content.findViewById<TextView>(R.id.speed_text).text =
+					us.toString(requireContext(), us.speedUnit(vehicle.Speed))
+			}
 
 			content.findViewById<LinearLayout>(R.id.congestion).visibility =
 				if (vehicle.congestionLevel == CongestionLevel.UNKNOWN) View.GONE else View.VISIBLE




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt b/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt
index b9b2871dd953aab86b88c501729cc02801e8f705..4c9945746d503aeed8749870d75dcca08a0d2d1a 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt
@@ -41,6 +41,7 @@ import xyz.apiote.bimba.czwek.repo.Departure
 import xyz.apiote.bimba.czwek.repo.DepartureItem
 import xyz.apiote.bimba.czwek.repo.OccupancyStatus
 import xyz.apiote.bimba.czwek.repo.Vehicle
+import xyz.apiote.bimba.czwek.units.UnitSystem
 import java.time.ZoneId
 import java.time.ZonedDateTime
 
@@ -91,7 +92,7 @@ 			context: Context?
 		) {
 			val alertDescriptions = alerts.map { it.description }.filter { it != "" }
 				.joinToString(separator = "\n")
-			holder?.moreButton?.setOnClickListener{
+			holder?.moreButton?.setOnClickListener {
 				MaterialAlertDialogBuilder(context!!)
 					.setTitle("Alerts")
 					.setPositiveButton(R.string.ok) { _, _ -> }
@@ -303,9 +304,10 @@ 				)
 			}
 
 			findViewById<TextView>(R.id.boarding_text).text = departure.boardingText(ctx)
-			// todo units -- [3.2] settings or system-based
-			findViewById<TextView>(R.id.speed_text).text =
-				getString(R.string.speed_in_km_per_h, departure.vehicle.Speed * 3.6)
+			UnitSystem.getSelected(requireContext()).let { us ->
+				findViewById<TextView>(R.id.speed_text).text =
+					us.toString(context, us.speedUnit(departure.vehicle.Speed))
+			}
 
 			findViewById<LinearLayout>(R.id.congestion).visibility =
 				if (departure.vehicle.congestionLevel == CongestionLevel.UNKNOWN) View.GONE else View.VISIBLE




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt
index 783606925186535e8a5ba30761e3b1ae932b6b2d..c31627a8ae83a48e330ab7b3a3408901e40135b4 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt
@@ -44,7 +44,6 @@ import xyz.apiote.bimba.czwek.repo.DepartureItem
 import xyz.apiote.bimba.czwek.repo.Favourite
 import xyz.apiote.bimba.czwek.repo.OfflineRepository
 import xyz.apiote.bimba.czwek.repo.Stop
-import xyz.apiote.bimba.czwek.units.Millisecond
 import xyz.apiote.bimba.czwek.units.Second
 import xyz.apiote.bimba.czwek.units.Tim
 import java.time.Instant
@@ -73,10 +72,10 @@ 	private val linesFilterTemporary = mutableMapOf()
 
 	// TODO [elizabeth] millisInFuture from header Cache-Control max-age
 	private val countdown =
-		object : CountDownTimer(Millisecond(Second(30)).millis, Millisecond(Tim(1)).millis) {
+		object : CountDownTimer(Second(30).milliseconds(), Tim(1).milliseconds()) {
 			override fun onTick(millisUntilFinished: Long) {
-				val timsUntillFinished = Tim(Millisecond(millisUntilFinished))
-				binding.departuresUpdatesProgress.progress = timsUntillFinished.tims
+				val timsUntillFinished = Tim(Second(millisUntilFinished.toDouble()/1000))
+				binding.departuresUpdatesProgress.progress = timsUntillFinished.tims.toInt()
 			}
 
 			override fun onFinish() {
@@ -454,8 +453,8 @@ 		// TODO [elizabeth] max, progress from header Cache-Control max-age
 		binding.departuresUpdatesProgress.apply {
 			visibility = View.VISIBLE
 			isIndeterminate = false
-			max = Tim(Second(30)).tims
-			progress = Tim(Second(30)).tims
+			max = Tim(Second(30)).tims.toInt()
+			progress = Tim(Second(30)).tims.toInt()
 		}
 		countdown.cancel()
 		countdown.start()




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt
index e90ce512e3b7eba9e635c9514268ee246ddc9db8..930c33fd4f177d77db3a0129bcafb98dfe86305a 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt
@@ -16,6 +16,8 @@ import xyz.apiote.bimba.czwek.api.DepartureV3
 import xyz.apiote.bimba.czwek.api.DepartureV4
 import xyz.apiote.bimba.czwek.api.Time
 import xyz.apiote.bimba.czwek.api.UnknownResourceVersionException
+import xyz.apiote.bimba.czwek.units.Second
+import xyz.apiote.bimba.czwek.units.UnitSystem
 import java.time.Instant
 import java.time.ZoneId
 import java.time.ZonedDateTime
@@ -169,12 +171,20 @@ 		if (departureTime.isBefore(now) && r < 3u) {
 			r = 0u
 		}
 		return when (r) {
-			0u -> DateUtils.getRelativeTimeSpanString(
-				departureTime.toEpochSecond() * 1000,
-				now.toEpochSecond() * 1000,
-				DateUtils.MINUTE_IN_MILLIS,
-				DateUtils.FORMAT_ABBREV_RELATIVE
-			).toString()
+			0u -> if (context != null && UnitSystem.getSelected(context).base == 12) {
+				val us = UnitSystem.getSelected(context)
+				us.toString(
+					context,
+					us.timeUnit(Second((departureTime.toEpochSecond() - now.toEpochSecond()).toInt()))
+				)
+			} else {
+				DateUtils.getRelativeTimeSpanString(
+					departureTime.toEpochSecond() * 1000,
+					now.toEpochSecond() * 1000,
+					DateUtils.MINUTE_IN_MILLIS,
+					DateUtils.FORMAT_ABBREV_RELATIVE
+				).toString()
+			}
 
 			1u -> context?.getString(R.string.departure_momentarily) ?: "momentarily"
 			2u -> context?.getString(R.string.departure_now) ?: "now"




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Vehicle.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Vehicle.kt
index b4ae2e175d1ed72cf2b38980d9da95693764bd80..047b8c576a927a9c8359d9214a08d6df1ac10d34 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Vehicle.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Vehicle.kt
@@ -5,12 +5,14 @@
 package xyz.apiote.bimba.czwek.repo
 
 import android.content.Context
+import android.util.Log
 import xyz.apiote.bimba.czwek.R
 import xyz.apiote.bimba.czwek.api.CongestionLevelV1
 import xyz.apiote.bimba.czwek.api.OccupancyStatusV1
 import xyz.apiote.bimba.czwek.api.VehicleV1
 import xyz.apiote.bimba.czwek.api.VehicleV2
 import xyz.apiote.bimba.czwek.api.VehicleV3
+import xyz.apiote.bimba.czwek.units.Mps
 
 enum class CongestionLevel {
 	UNKNOWN, SMOOTH, STOP_AND_GO, SIGNIFICANT, SEVERE;
@@ -73,7 +75,7 @@ data class Vehicle(
 	val ID: String,
 	val Position: Position,
 	val Capabilities: UShort,
-	val Speed: Float,
+	val Speed: Mps,
 	val Line: LineStub,
 	val Headsign: String,
 	val congestionLevel: CongestionLevel,
@@ -83,34 +85,40 @@ 	constructor(v: VehicleV1) : this(
 		v.ID,
 		Position(v.Position),
 		v.Capabilities,
-		v.Speed,
+		Mps(v.Speed.toDouble()),
 		LineStub(v.Line),
 		v.Headsign,
 		CongestionLevel.of(v.CongestionLevel),
 		OccupancyStatus.of(v.OccupancyStatus)
-	)
+	) {
+		Log.i("unit", "veh: ${v.Speed}")
+	}
 
 	constructor(v: VehicleV2) : this(
 		v.ID,
 		Position(v.Position),
 		v.Capabilities,
-		v.Speed,
+		Mps(v.Speed.toDouble()),
 		LineStub(v.Line),
 		v.Headsign,
 		CongestionLevel.of(v.CongestionLevel),
 		OccupancyStatus.of(v.OccupancyStatus)
-	)
+	) {
+		Log.i("unit", "veh: ${v.Speed.toDouble()}")
+	}
 
 	constructor(v: VehicleV3) : this(
 		v.ID,
 		Position(v.Position),
 		v.Capabilities,
-		v.Speed,
+		Mps(v.Speed.toDouble()),
 		LineStub(v.Line),
 		v.Headsign,
 		CongestionLevel.of(v.CongestionLevel),
 		OccupancyStatus.of(v.OccupancyStatus)
-	)
+	) {
+		Log.i("unit", "veh: ${v.Speed.toDouble()}")
+	}
 
 	enum class Capability(val bit: UShort) {
 		RAMP(0b0001u), LOW_FLOOR(0b0010u), LOW_ENTRY(0b0001_0000_0000u), AC(0b0100u), BIKE(0b1000u), VOICE(




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/settings/SettingsActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/settings/SettingsActivity.kt
new file mode 100644
index 0000000000000000000000000000000000000000..be458c29a7f4d36e99f1e0d0189c4eb7879aa16e
--- /dev/null
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/settings/SettingsActivity.kt
@@ -0,0 +1,45 @@
+package xyz.apiote.bimba.czwek.settings
+
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updateLayoutParams
+import androidx.preference.PreferenceFragmentCompat
+import xyz.apiote.bimba.czwek.R
+
+class SettingsActivity : AppCompatActivity() {
+
+	override fun onCreate(savedInstanceState: Bundle?) {
+		enableEdgeToEdge()
+		super.onCreate(savedInstanceState)
+		setContentView(R.layout.settings_activity)
+		if (savedInstanceState == null) {
+			supportFragmentManager
+				.beginTransaction()
+				.replace(R.id.settings, SettingsFragment())
+				.commit()
+		}
+		supportActionBar?.setDisplayHomeAsUpEnabled(true)
+		// TODO insets
+
+		val root = findViewById<View>(R.id.settings)
+
+		ViewCompat.setOnApplyWindowInsetsListener(root) { v, windowInsets ->
+			val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+			v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
+				topMargin = insets.top
+			}
+			windowInsets
+		}
+	}
+
+	class SettingsFragment : PreferenceFragmentCompat() {
+		override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+			setPreferencesFromResource(R.xml.root_preferences, rootKey)
+		}
+	}
+}
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/units/TGM.kt b/app/src/main/java/xyz/apiote/bimba/czwek/units/TGM.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ca2c62ff76293c56f939852f078c0e1514eae3cf
--- /dev/null
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/units/TGM.kt
@@ -0,0 +1,82 @@
+package xyz.apiote.bimba.czwek.units
+
+import android.content.Context
+import xyz.apiote.bimba.czwek.R
+import java.text.NumberFormat
+import kotlin.math.abs
+import kotlin.math.pow
+
+
+class TGM(base: Int) : UnitSystem(base) {
+	override fun timeUnit(count: Long): TimeUnit = Tim(count)
+	override fun timeUnit(other: TimeUnit): TimeUnit = Tim(other)
+
+	override fun speedUnit(count: Double): SpeedUnit = Vlos(count)
+	override fun speedUnit(other: SpeedUnit): SpeedUnit = Vlos(other)
+	override fun toString(context: Context, s: SpeedUnit): String = s.toString(context, base)
+	override fun toString(context: Context, t: TimeUnit): String = t.toString(context, base)
+}
+
+class Tim(val tims: Long) : TimeUnit {
+	constructor(other: TimeUnit) : this((other.milliseconds() * 144 / 25 / 1000))
+
+	override fun milliseconds(): Long = tims * 25 * 1000 / 144
+	override fun toString(context: Context, base: Int): String {
+		val res = if (tims < 0) {
+			R.string.time_in_tm_past
+		} else {
+			R.string.time_in_tm
+		}
+		val (t,m) = if (tims > base.toDouble().pow(4)) {
+			Pair(tims/base.toDouble().pow(4).toInt(), "⁴")
+		} else {
+			Pair(tims/base.toDouble().pow(2).toInt(), "²")
+		}
+		return if (base == 10) {
+			context.getString(res, NumberFormat.getInstance().format(abs(t)), m)
+		} else {
+			context.getString(
+				res,
+				abs(t).toString(12).lowercase().replace("a", "↊").replace("b", "↋"),
+				m
+			)
+		}
+	}
+
+	override fun contentDescription(context: Context, base: Int): String =
+		context.resources.getQuantityString(R.plurals.time_in_tm_cd, tims.toInt(), tims)
+}
+
+class Vlos(val vlos: Double) : SpeedUnit {
+	constructor(other: SpeedUnit) : this(other.mps() / 1.703133986928105)
+
+	override fun mps(): Double = vlos * 1.703133986928105
+	override fun contentDescription(context: Context, base: Int): String {
+		// TODO
+		return ""
+	}
+
+	override fun toString(context: Context, base: Int): String {
+		return context.getString(R.string.speed_in_vl, vlos.toString(base, 3).lowercase())
+	}
+}
+
+fun Double.toString(radix: Int, precision: Int): String {
+	var x = this
+	var result = ""
+	if (x < 0) {
+		result = "-"
+		x = -x
+	}
+	result += x.toInt().toString(radix)
+	var frac = (x - x.toInt())
+	var digits = 0
+	while (frac > 0 && digits < precision) {
+		if (digits == 0) result += if (radix == 12) "·" else "."  // TODO separator based on locale
+		frac *= radix
+		result += frac.toInt().toString(radix)
+		frac -= frac.toInt()
+		digits++
+	}
+	return result.lowercase().replace("a", "↊").replace("b", "↋")
+}
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/units/Time.kt b/app/src/main/java/xyz/apiote/bimba/czwek/units/Time.kt
deleted file mode 100644
index 9456eae31f27301d561789f4e07ca44dd9519bef..0000000000000000000000000000000000000000
--- a/app/src/main/java/xyz/apiote/bimba/czwek/units/Time.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package xyz.apiote.bimba.czwek.units
-
-interface TimeUnit
-
-data class Second(val secs: Long) {
-	constructor(t: Tim) : this(t.tims.toLong() * 25 / 144)
-}
-
-data class Millisecond(val millis: Long) {
-	constructor(t: Tim) : this(t.tims.toLong() * 25 * 1000 / 144)
-	constructor(s: Second) : this(s.secs * 1000)
-}
-
-data class Tim(val tims: Int) {
-	constructor(s: Second) : this((s.secs * 144 / 25).toInt())
-	constructor(m: Millisecond) : this((m.millis * 144 / 25 / 1000).toInt())
-}
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/units/UnitSystem.kt b/app/src/main/java/xyz/apiote/bimba/czwek/units/UnitSystem.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2af9ab4bc7f3e06e89f8f5710b05770dcbe1b287
--- /dev/null
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/units/UnitSystem.kt
@@ -0,0 +1,74 @@
+package xyz.apiote.bimba.czwek.units
+
+import android.content.Context
+import android.icu.util.LocaleData
+import android.icu.util.LocaleData.MeasurementSystem
+import android.icu.util.ULocale
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.preference.PreferenceManager
+import java.util.Locale
+
+abstract class UnitSystem(val base: Int) {
+	companion object {
+		@RequiresApi(Build.VERSION_CODES.P)
+		private fun forMeasureSystem(ms: MeasurementSystem) =
+			when (ms) {
+				MeasurementSystem.SI -> {
+					Metric
+				}
+
+				MeasurementSystem.UK -> {
+					Imperial
+				}
+
+				MeasurementSystem.US -> {
+					USCustomary
+				}
+
+				else -> {
+					Metric
+				}
+			}
+
+		private fun forLocale(country: String): UnitSystem = Metric  // TODO
+
+		private fun getDefault() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+			forMeasureSystem(LocaleData.getMeasurementSystem(ULocale.forLocale(Locale.getDefault())))
+		} else {
+			forLocale(Locale.getDefault().country)
+		}
+
+		fun getSelected(context: Context): UnitSystem =
+			when (PreferenceManager.getDefaultSharedPreferences(context)
+				.getString("unit_system", "default")) {
+				"default" -> getDefault()
+				"metric" -> Metric
+				"imperial" -> Imperial
+				"customary" -> USCustomary
+				"tgm10" -> TGM(10)
+				"tgm12" -> TGM(12)
+				else -> getDefault()
+			}
+	}
+
+	abstract fun timeUnit(count: Long): TimeUnit
+	abstract fun timeUnit(other: TimeUnit): TimeUnit
+	abstract fun speedUnit(count: Double): SpeedUnit
+	abstract fun speedUnit(other: SpeedUnit): SpeedUnit
+
+	abstract fun toString(context: Context, s: SpeedUnit): String
+	abstract fun toString(context: Context, t: TimeUnit): String
+}
+
+interface TimeUnit {
+	fun milliseconds(): Long
+	fun toString(context: Context, base: Int): String
+	fun contentDescription(context: Context, base: Int): String
+}
+
+interface SpeedUnit {
+	fun mps(): Double
+	fun toString(context: Context, base: Int): String
+	fun contentDescription(context: Context, base: Int): String
+}




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/units/imperial.kt b/app/src/main/java/xyz/apiote/bimba/czwek/units/imperial.kt
new file mode 100644
index 0000000000000000000000000000000000000000..964a97973865f2c5ba98e432f3fdd3427cf1ca8d
--- /dev/null
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/units/imperial.kt
@@ -0,0 +1,26 @@
+package xyz.apiote.bimba.czwek.units
+
+import android.content.Context
+import xyz.apiote.bimba.czwek.R
+import java.text.NumberFormat
+
+object Imperial : UnitSystem(10) {
+	override fun timeUnit(count: Long): TimeUnit = Second(count.toDouble())
+	override fun timeUnit(other: TimeUnit): TimeUnit = Second(other)
+
+	override fun speedUnit(count: Double): SpeedUnit = Miph(count)
+	override fun speedUnit(other: SpeedUnit): SpeedUnit = Miph(other)
+	override fun toString(context: Context, s: SpeedUnit): String = s.toString(context, base)
+	override fun toString(context: Context, t: TimeUnit): String = t.toString(context, base)
+}
+
+class Miph(val miph: Double) : SpeedUnit {
+	constructor(s: Int) : this(s.toDouble())
+	constructor(other: SpeedUnit) : this(other.mps() / 0.44704)
+
+	override fun mps(): Double = miph * 0.44704
+	override fun toString(context: Context, base: Int): String = context.getString(R.string.speed_in_mi_per_h, NumberFormat.getInstance().format(miph))
+	override fun contentDescription(context: Context, base: Int): String = context.resources.getQuantityString(
+		R.plurals.speed_in_mi_per_h_cd, miph.toInt(), miph)
+
+}
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/units/metric.kt b/app/src/main/java/xyz/apiote/bimba/czwek/units/metric.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2492c4d0ff00a56037de882cdb0888bdc1ad8144
--- /dev/null
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/units/metric.kt
@@ -0,0 +1,44 @@
+package xyz.apiote.bimba.czwek.units
+
+import android.content.Context
+import xyz.apiote.bimba.czwek.R
+import java.text.NumberFormat
+
+object Metric : UnitSystem(10) {
+
+	override fun timeUnit(count: Long): TimeUnit = Second(count.toDouble())
+	override fun timeUnit(other: TimeUnit): TimeUnit = Second(other)
+
+	override fun speedUnit(count: Double): SpeedUnit = Kmph(count)
+	override fun speedUnit(other: SpeedUnit): SpeedUnit = Kmph(other)
+	override fun toString(context: Context, s: SpeedUnit): String = s.toString(context, base)
+	override fun toString(context: Context, t: TimeUnit): String = t.toString(context, base)
+}
+
+class Second(val seconds: Double) : TimeUnit {
+	constructor(s: Int) : this(s.toDouble())
+	constructor(other: TimeUnit) : this((other.milliseconds().toDouble() / 1000))
+
+	override fun milliseconds(): Long = (seconds * 1000).toLong()
+	override fun toString(context: Context, base: Int): String = context.getString(R.string.time_in_s, NumberFormat.getInstance().format(seconds))
+	override fun contentDescription(context: Context, base: Int): String = context.resources.getQuantityString(R.plurals.time_in_s_cd, seconds.toInt(), seconds)
+}
+
+class Mps(val mps: Double) : SpeedUnit {
+	constructor(s: Int) : this(s.toDouble())
+	constructor(other: SpeedUnit) : this(other.mps())
+
+	override fun mps(): Double = mps
+	override fun toString(context: Context, base: Int): String = context.getString(R.string.speed_in_m_per_s, NumberFormat.getInstance().format(mps))
+	override fun contentDescription(context: Context, base: Int): String = context.resources.getQuantityString(R.plurals.speed_in_m_per_s_cd, mps.toInt(), mps)
+}
+
+class Kmph(val kmph: Double) : SpeedUnit {
+	constructor(s: Int) : this(s.toDouble())
+	constructor(other: SpeedUnit) : this(other.mps() * 3.6)
+
+	override fun mps(): Double = kmph / 3.6
+	override fun toString(context: Context, base: Int): String = context.getString(R.string.speed_in_km_per_h, NumberFormat.getInstance().format(kmph))
+	override fun contentDescription(context: Context, base: Int): String = context.resources.getQuantityString(R.plurals.speed_in_km_per_h_cd, kmph.toInt(), kmph)
+
+}
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/units/usCustomary.kt b/app/src/main/java/xyz/apiote/bimba/czwek/units/usCustomary.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8b2317362a850d7bde70b60e0f2c41216bf4d3f4
--- /dev/null
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/units/usCustomary.kt
@@ -0,0 +1,13 @@
+package xyz.apiote.bimba.czwek.units
+
+import android.content.Context
+
+object USCustomary: UnitSystem(10) {
+	override fun timeUnit(count: Long): TimeUnit = Second(count.toDouble())
+	override fun timeUnit(other: TimeUnit): TimeUnit = Second(other)
+
+	override fun speedUnit(count: Double): SpeedUnit = Miph(count)
+	override fun speedUnit(other: SpeedUnit): SpeedUnit = Miph(other)
+	override fun toString(context: Context, s: SpeedUnit): String = s.toString(context, base)
+	override fun toString(context: Context, t: TimeUnit): String = t.toString(context, base)
+}




diff --git a/app/src/main/res/drawable/units.xml b/app/src/main/res/drawable/units.xml
new file mode 100644
index 0000000000000000000000000000000000000000..65250dd0441defb5c597d591f4d9a9416857b4c6
--- /dev/null
+++ b/app/src/main/res/drawable/units.xml
@@ -0,0 +1,10 @@
+<!--
+SPDX-FileCopyrightText: Google
+
+SPDX-License-Identifier: Apache-2.0
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorOnSurface" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M7,15H5.5v-4.5H4V9h3V15zM13.5,13.5h-3v-1h2c0.55,0 1,-0.45 1,-1V10c0,-0.55 -0.45,-1 -1,-1H9v1.5h3v1h-2c-0.55,0 -1,0.45 -1,1V15h4.5V13.5zM19.5,14v-4c0,-0.55 -0.45,-1 -1,-1H15v1.5h3v1h-2v1h2v1h-3V15h3.5C19.05,15 19.5,14.55 19.5,14z"/>
+    
+</vector>




diff --git a/app/src/main/res/font/yellowcircle8.otf b/app/src/main/res/font/yellowcircle8.otf
index 31e8706da286d9575e653938bfd72b7fdba1676d..bfd91f587ddefb3c1f81d7750fd41162c882ae6f 100644
Binary files a/app/src/main/res/font/yellowcircle8.otf and b/app/src/main/res/font/yellowcircle8.otf differ




diff --git a/app/src/main/res/layout/settings_activity.xml b/app/src/main/res/layout/settings_activity.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b5b5089cad3cc4480dae8e346bc72ab4d18096c9
--- /dev/null
+++ b/app/src/main/res/layout/settings_activity.xml
@@ -0,0 +1,9 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+	android:layout_width="match_parent"
+	android:layout_height="match_parent">
+
+	<FrameLayout
+		android:id="@+id/settings"
+		android:layout_width="match_parent"
+		android:layout_height="match_parent" />
+</LinearLayout>
\ No newline at end of file




diff --git a/app/src/main/res/menu/drawer.xml b/app/src/main/res/menu/drawer.xml
index 6aa4cd40617cbc62655fc57af593549f6d4016a1..1d876c0cf374716191449dcfaf664152687c4623 100644
--- a/app/src/main/res/menu/drawer.xml
+++ b/app/src/main/res/menu/drawer.xml
@@ -25,6 +25,9 @@ 		
 	</item>
 	<!-- other settings -->
 	<item
+	android:id="@+id/drawer_settings"
+	android:title="@string/title_settings"/>
+	<item
 		android:id="@+id/drawer_about"
 		android:title="@string/title_about"/>
 </menu>
\ No newline at end of file




diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5769aed0a30f247b1dd5735e17b142d669ee65ad
--- /dev/null
+++ b/app/src/main/res/values/arrays.xml
@@ -0,0 +1,19 @@
+<resources>
+	<string-array name="unit_entries">
+		<item>@string/units_locale_based</item>
+		<item>@string/units_metric</item>
+		<item>@string/units_imperial</item>
+		<item>@string/units_customary</item>
+		<item>@string/units_tgm10</item>
+		<item>@string/units_tgm12</item>
+	</string-array>
+
+	<string-array name="unit_values">
+		<item>locale</item>
+		<item>metric</item>
+		<item>imperial</item>
+		<item>customary</item>
+		<item>tgm10</item>
+		<item>tgm12</item>
+	</string-array>
+</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 19afd0e9e16147ad1463025d7342a239b206452a..a32435ac389fb8240427984e8b571754f5b7f833 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -27,7 +27,37 @@ 	Cannot obtain current location
 	<string name="no_departures">No departures</string>
 	<string name="waiting_position">waiting for position</string>
 	<string name="vehicle_headsign">%1$s » %2$s</string>
-	<string name="speed_in_km_per_h">%1$.3f km/h</string>
+	<string name="time_in_s">%1$s s</string>
+	<plurals name="time_in_s_cd">
+		<item quantity="one">%1$d second</item>
+		<item quantity="other">%1$d seconds</item>
+	</plurals>
+	<string name="time_in_tm">%1$s %2$sTm</string>
+	<string name="time_in_tm_past">%1$s %2$sTm ago</string>
+	<plurals name="time_in_tm_cd">
+		<item quantity="one">%1$d tim</item>
+		<item quantity="other">%1$d tims</item>
+	</plurals>
+	<string name="speed_in_km_per_h">%1$s km/h</string>
+	<string name="speed_in_m_per_s">%1$s m/s</string>
+	<string name="speed_in_mi_per_h">%1$s mph</string>
+	<string name="speed_in_vl">%1$s Vl</string>
+	<plurals name="speed_in_m_per_s_cd">
+		<item quantity="one">%1$d meter per second</item>
+		<item quantity="other">%1$d meters per second</item>
+	</plurals>
+	<plurals name="speed_in_km_per_h_cd">
+		<item quantity="one">%1$d kilometer per hour</item>
+		<item quantity="other">%1$d kilometers per hour</item>
+	</plurals>
+	<plurals name="speed_in_mi_per_h_cd">
+		<item quantity="one">%1$d mile per hour</item>
+		<item quantity="other">%1$d mile per hour</item>
+	</plurals>
+	<plurals name="speed_in_vl_cd">
+		<item quantity="one">%1$s vlos</item>
+		<item quantity="other">%1$s vlos</item>
+	</plurals>
 	<string name="congestion_unknown">unknown</string>
 	<string name="congestion_smooth">smooth</string>
 	<string name="congestion_stop_and_go">stop and go</string>
@@ -136,5 +166,14 @@ 	No more departures
 	<string name="loading">loading…</string>
 	<string name="favourite_deleted">Favourite deleted</string>
 	<string name="undo">Undo</string>
+	<string name="title_activity_settings">SettingsActivity</string>
+	<string name="units_title">Unit system</string>
+	<string name="units_locale_based">Locale based</string>
+	<string name="units_metric">Metric (SI)</string>
+	<string name="units_imperial">Imperial (UK)</string>
+	<string name="units_customary">US Customary</string>
+	<string name="units_tgm10">TGM (base 10)</string>
+	<string name="units_tgm12">TGM (base 12)</string>
+	<string name="title_settings">Settings</string>
 
 </resources>




diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 3b2cafc7373811872ce3e4dc2a3e795908a0e2a9..c05c82928aa6e68cb4fd603ae8558e5f65972f90 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -10,7 +10,7 @@     GPS-Symbol
     <string name="title_activity_results">Ergebnisse</string>
     <string name="cont">Fortsetzen</string>
     <string name="vehicle_headsign">%1$s » %2$s</string>
-    <string name="speed_in_km_per_h">%1$.3f km/h</string>
+    <string name="speed_in_km_per_h">%1$s km/h</string>
     <string name="congestion_unknown">unbekannt</string>
     <string name="congestion_congestion">Stau</string>
     <string name="occupancy_unknown">unbekannt</string>




diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 4b8162ad24cae62e539542f5747503263a3424a7..a7ad7ccc620202f1698b8fa2859f47bb1c5276ce 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -27,7 +27,7 @@     Nessune partenze
     <string name="waiting_position">In attesa della posizione</string>
     <string name="vehicle_headsign">%1$s » %2$s</string>
     <string name="vehicle_headsign_content_description">%1$s verso %2$s</string>
-    <string name="speed_in_km_per_h">%1$.3f km/h</string>
+    <string name="speed_in_km_per_h">%1$s km/h</string>
     <string name="congestion_unknown">sconosciuta</string>
     <string name="congestion_smooth">fluido</string>
     <string name="congestion_stop_and_go">fermarsi e andare</string>




diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 541705d81a56a03233dbe01c4f898b4090b4da74..1f933108d6c66a0dabfa1dd07ac8ddf53f05fc9f 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -27,7 +27,7 @@     Brak odjazdów
     <string name="waiting_position">oczekiwanie na pozycję</string>
     <string name="vehicle_headsign">%1$s » %2$s</string>
     <string name="vehicle_headsign_content_description">%1$s w kierunku przystanku %2$s</string>
-    <string name="speed_in_km_per_h">%1$.3f km/h</string>
+    <string name="speed_in_km_per_h">%1$s km/h</string>
     <string name="congestion_unknown">nieznane</string>
     <string name="congestion_smooth">płynne</string>
     <string name="congestion_stop_and_go">przestoje</string>




diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml
new file mode 100644
index 0000000000000000000000000000000000000000..25c5c6c8cb3b58a4c1a14b613141d5004941b2ab
--- /dev/null
+++ b/app/src/main/res/xml/root_preferences.xml
@@ -0,0 +1,10 @@
+<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
+		<ListPreference
+			app:icon="@drawable/units"
+			app:defaultValue="default"
+			app:entries="@array/unit_entries"
+			app:entryValues="@array/unit_values"
+			app:key="unit_system"
+			app:title="@string/units_title"
+			app:useSimpleSummaryProvider="true" />
+</PreferenceScreen>
\ No newline at end of file