Bimba.git

commit 7e7af9c9ae26402061d5065839a370197f95d9b9

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

WIP add dynamic client

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


diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 016c28aaf0aecc722b14e7e4aea59b09c736009d..293265bf5157e96543fa4a98ec9fec291f2801f3 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -20,13 +20,13 @@
 android {
 	namespace = "xyz.apiote.bimba.czwek"
 	// NOTE apksigner with `--alignment-preserved` https://gitlab.com/fdroid/fdroiddata/-/issues/3299#note_1989808414
-	compileSdk = 35
-	buildToolsVersion = "35.0.1"
+	compileSdk = 36
+	buildToolsVersion = "36.0.0"
 
 	defaultConfig {
 		applicationId = "xyz.apiote.bimba.czwek"
 		minSdk = 21
-		targetSdk = 35
+		targetSdk = 36
 		versionCode = 37
 		versionName = "3.9.0"
 
@@ -109,6 +109,7 @@ 	implementation("androidx.core:core-splashscreen:1.0.1")
 	implementation("com.google.openlocationcode:openlocationcode:1.0.4")
 	implementation("org.osmdroid:osmdroid-android:6.1.20")
 	implementation("org.yaml:snakeyaml:2.4")
+	implementation("com.charleskorn.kaml:kaml:0.73.0")
 	implementation("androidx.activity:activity-ktx:1.10.1")
 	implementation("com.otaliastudios:zoomlayout:1.9.0")
 	implementation("dev.bandb.graphview:graphview:0.8.1")




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/Bimba.kt b/app/src/main/java/xyz/apiote/bimba/czwek/Bimba.kt
index d695be178ef80a558d1d7d8c6f56ad77db757a28..1b051f3ee33eb91fa6ee8f471ba11360b8ab4a70 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/Bimba.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/Bimba.kt
@@ -51,11 +51,11 @@ 				channelName = getString(R.string.acra_notification_channel)
 				channelDescription = getString(R.string.acra_notification_channel_description)
 				channelImportance = NotificationManagerCompat.IMPORTANCE_DEFAULT
 				sendButtonText = getString(R.string.send)
-				resSendButtonIcon = R.drawable.send
+				resSendButtonIcon = R.drawable.send  // TODO black?
 				discardButtonText = getString(R.string.discard)
-				resDiscardButtonIcon = R.drawable.discard
+				resDiscardButtonIcon = R.drawable.discard // TODO colorOnSurface?
 				sendWithCommentButtonText = getString(R.string.send_with_comment)
-				resSendWithCommentButtonIcon = R.drawable.comment
+				resSendWithCommentButtonIcon = R.drawable.comment // TODO colorOnSurface?
 				commentPrompt = getString(R.string.acra_notification_comment)
 			}
 		}




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/Api.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/Api.kt
index 4ff3d6d839d708b8e84d21b774678bd6a4e2dc97..e13244cf159cd8744c2f4eded77b43f13b68e7d0 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/api/Api.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/Api.kt
@@ -9,9 +9,15 @@ import android.content.Context.MODE_PRIVATE
 import android.net.ConnectivityManager
 import android.net.NetworkCapabilities
 import android.os.Build
+import android.util.Base64
+import android.util.Log
+import androidx.core.content.edit
+import com.charleskorn.kaml.Yaml
+import com.charleskorn.kaml.decodeFromStream
+import com.github.jershell.kbson.KBson
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
-import org.openapitools.client.infrastructure.ServerError
+import kotlinx.serialization.Serializable
 import org.openapitools.client.infrastructure.ServerException
 import xyz.apiote.bimba.czwek.R
 import xyz.apiote.bimba.czwek.settings.feeds.FeedsSettings
@@ -27,53 +33,103 @@
 // todo [3.2] constants
 // todo [3.2] split api files to classes files
 
+@Serializable
+data class TrafficServer(
+	val url: String,
+	val description: String,
+	val seatsRequired: List<String>,
+	var feeds: FeedsSettings = FeedsSettings(mutableMapOf())
+)
+
+@Serializable
+data class Traffic(
+	val authEndpoint: String,
+	val servers: List<TrafficServer>,
+	var selectedServer: Int = 0
+)
+
+@Serializable
 data class Server(
-	val host: String,
-	val token: String,
-	val feeds: FeedsSettings,
-	val apiPath: String
+	var host: String,
+	var traffic: Traffic?
 ) {
 	companion object {
-		const val DEFAULT = "bimba.apiote.xyz"
-		const val HOST_KEY = "host"
-		const val TOKEN_KEY = "token"
+		const val DEFAULT = "bimba.app"
+		@Deprecated("should use traffic's url")
 		const val API_PATH_KEY = "apiPath"
+		const val SERVER_KEY = "server"
 
 		fun get(context: Context): Server {
 			val preferences = context.getSharedPreferences("shp", MODE_PRIVATE)
-			val apiPath = preferences.getString(API_PATH_KEY, "")!!
-			val feeds = FeedsSettings.load(context, apiPath)
-			val host = preferences.getString(HOST_KEY, DEFAULT)!!
-			return Server(
-				host, preferences.getString(TOKEN_KEY, "")!!,
-				feeds, apiPath
+			val savedServer = preferences.getString(SERVER_KEY, null)
+			return savedServer?.let {
+				KBson().load(serializer(), Base64.decode(it, Base64.DEFAULT))
+			} ?: Server(DEFAULT, null)
+		}
+	}
+
+	fun save(context: Context) {
+		val savedServer = Base64.encodeToString(KBson().dump(serializer(), this), Base64.DEFAULT)
+		val preferences = context.getSharedPreferences("shp", MODE_PRIVATE)
+		preferences.edit {
+			putString(SERVER_KEY, savedServer)
+		}
+	}
+
+	fun getSelectedServer(): TrafficServer {
+		return traffic!!.servers[traffic!!.selectedServer]
+	}
+
+	suspend fun getTraffic(context: Context, force: Boolean = false) {
+		if (traffic != null && !force) {
+			Log.i("Server", "traffic already not null")
+			return
+		}
+		val result = try {
+			rawRequest(
+				URL("${hostWithScheme(host)}/.well-known/traffic.yml"), context, emptyArray()
 			)
+		} catch (_: MalformedURLException) {
+			Result(null, Error(0, R.string.error_url, R.drawable.error_url))
+		}
+		if (result.error != null) {
+			Log.e("Server", "while getting traffic: ${result.error}")
+		} else {
+			val traffic = Yaml.default.decodeFromStream<Traffic>(result.stream!!)
+			if (this.traffic == null) {
+				this.traffic = traffic
+			} else {
+				val servers = traffic.servers.mapIndexed { i, server ->
+					if (server.url == this.traffic!!.servers[i].url) {
+						server.feeds = this.traffic!!.servers[i].feeds
+					}
+					if (this.getSelectedServer().url == server.url) {
+						traffic.selectedServer = i
+					}
+					server
+				}
+				this.traffic = Traffic(traffic.authEndpoint, servers, traffic.selectedServer)
+			}
+
+			save(context)
 		}
 	}
+
 }
 
 data class Result(val stream: InputStream?, val error: Error?)
 
 data class Error(val statusCode: Int, val stringResource: Int, val imageResource: Int) {
 	companion object {
-		fun fromTransitous(e: ServerException): Error = Error(e.statusCode, R.string.error, R.drawable.error_other)
-	}
-}
-
-suspend fun getBimba(context: Context, server: Server): Result {
-	return try {
-		rawRequest(
-			URL("${hostWithScheme(server.host)}/.well-known/traffic-api"), server, context, emptyArray()
-		)
-	} catch (e: MalformedURLException) {
-		Result(null, Error(0, R.string.error_url, R.drawable.error_url))
+		fun fromTransitous(e: ServerException): Error =
+			Error(e.statusCode, R.string.error, R.drawable.error_other)
 	}
 }
 
 suspend fun getFeeds(context: Context, server: Server): Result {
 	return try {
 		rawRequest(
-			URL("${server.apiPath}/"), server, context, arrayOf(1u, 2u)
+			URL(server.getSelectedServer().url), context, arrayOf(1u, 2u)
 		)
 	} catch (_: MalformedURLException) {
 		Result(null, Error(0, R.string.error_url, R.drawable.error_url))
@@ -187,7 +243,7 @@ 	}
 }
 
 suspend fun rawRequest(
-	url: URL, server: Server, context: Context, responseVersion: Array<UInt>
+	url: URL, context: Context, responseVersion: Array<UInt>
 ): Result {
 	if (!isNetworkAvailable(context)) {
 		return Result(null, Error(0, R.string.error_offline, R.drawable.error_net))
@@ -198,7 +254,7 @@ 			setRequestProperty(
 				"User-Agent",
 				"${context.getString(R.string.applicationId)}/${context.getString(R.string.versionName)} (${Build.VERSION.SDK_INT})"
 			)
-			setRequestProperty("X-Bimba-Token", server.token)
+			// TODO put token in authorization
 			responseVersion.forEach { addRequestProperty("Accept", "application/$it+bare") }
 		}
 		try {
@@ -208,7 +264,7 @@ 			} else {
 				val (string, image) = mapHttpError(c.responseCode)
 				Result(c.errorStream, Error(c.responseCode, string, image))
 			}
-		} catch (e: IOException) {
+		} catch (_: IOException) {
 			Result(null, Error(0, R.string.error_connecting, R.drawable.error_server))
 		}
 	}
@@ -243,7 +299,7 @@ 	feeds: String?
 ): Result {
 	return withContext(Dispatchers.IO) {
 		val url = URL( // todo [3.2] scheme, host, path, constructed query
-			"${server.apiPath}/${feeds?.ifEmpty { server.feeds.getIDs() } ?: server.feeds.getIDs()}/$resource${
+			"${server.getSelectedServer().url}/${feeds?.ifEmpty { server.getSelectedServer().feeds.getIDs() } ?: server.getSelectedServer().feeds.getIDs()}/$resource${
 				if (item == null) {
 					""
 				} else {
@@ -259,7 +315,7 @@ 					}"
 				}.joinToString("&", "?")
 			}"
 		)
-		rawRequest(url, server, context, responseVersion)
+		rawRequest(url, context, responseVersion)
 	}
 }
 




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/onboarding/FirstRunActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/onboarding/FirstRunActivity.kt
index e7828395a0b090e4ce33797eb61fa5fc2c789f1d..b0f61f116875bbc7cfcb79d669dadd0bd54c4ab2 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/onboarding/FirstRunActivity.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/onboarding/FirstRunActivity.kt
@@ -15,7 +15,10 @@ import androidx.core.content.edit
 import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
 import androidx.work.OneTimeWorkRequest
 import androidx.work.WorkManager
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
 import xyz.apiote.bimba.czwek.R
+import xyz.apiote.bimba.czwek.api.Server
 import xyz.apiote.bimba.czwek.dashboard.MainActivity
 import xyz.apiote.bimba.czwek.repo.migrateDB
 import xyz.apiote.bimba.czwek.settings.DownloadCitiesWorker
@@ -26,7 +29,10 @@ 	override fun onCreate(savedInstanceState: Bundle?) {
 		installSplashScreen()
 		super.onCreate(savedInstanceState)
 
-		migrateFeedsSettings(this)
+		MainScope().launch {
+			Server.get(this@FirstRunActivity).getTraffic(this@FirstRunActivity)
+			migrateFeedsSettings(this@FirstRunActivity)
+		}
 		migrateDB(this)
 		createNotificationChannels()
 




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/onboarding/OnboardingActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/onboarding/OnboardingActivity.kt
index f934bd2eb9c7e7f084b89b67dd6e0cf0d18e97c1..d932fead370affcb8d82e39ed1f374c0e5a135dd 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/onboarding/OnboardingActivity.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/onboarding/OnboardingActivity.kt
@@ -6,17 +6,22 @@ package xyz.apiote.bimba.czwek.onboarding
 
 import android.content.Intent
 import android.content.SharedPreferences
-import android.net.Uri
 import android.os.Bundle
 import android.util.Log
+import android.view.KeyEvent
+import android.view.View
+import android.widget.TextView
 import androidx.activity.enableEdgeToEdge
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.appcompat.app.AppCompatActivity
 import androidx.core.content.edit
+import androidx.core.net.toUri
 import androidx.core.view.ViewCompat
 import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.updatePadding
 import androidx.core.widget.addTextChangedListener
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
 import net.openid.appauth.AuthState
 import net.openid.appauth.AuthorizationException
 import net.openid.appauth.AuthorizationRequest
@@ -24,10 +29,17 @@ import net.openid.appauth.AuthorizationResponse
 import net.openid.appauth.AuthorizationService
 import net.openid.appauth.AuthorizationServiceConfiguration
 import net.openid.appauth.AuthorizationServiceConfiguration.RetrieveConfigurationCallback
+import net.openid.appauth.ClientSecretPost
+import net.openid.appauth.RegistrationRequest
+import net.openid.appauth.RegistrationResponse
 import net.openid.appauth.ResponseTypeValues
+import net.openid.appauth.TokenResponse
+import org.acra.ktx.sendWithAcra
 import xyz.apiote.bimba.czwek.api.Server
+import xyz.apiote.bimba.czwek.dashboard.MainActivity
 import xyz.apiote.bimba.czwek.databinding.ActivityOnboardingBinding
 import xyz.apiote.bimba.czwek.settings.feeds.FeedChooserActivity
+import java.security.MessageDigest
 
 
 class OnboardingActivity : AppCompatActivity() {
@@ -35,15 +47,19 @@ 	private var _binding: ActivityOnboardingBinding? = null
 	private val binding get() = _binding!!
 	private lateinit var preferences: SharedPreferences
 	private lateinit var authState: AuthState
+	private lateinit var clientSecret: String
+	private lateinit var server: Server
+	private var authError: Exception? = null
 
-	private val activityLauncher =
+	private val authLauncher =
 		registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
-			if (!FirstRunActivity.getFirstRun(this)) {
-				finish()
-			}
+			val resp = AuthorizationResponse.fromIntent(it.data!!)
+			val ex = AuthorizationException.fromIntent(it.data)
+			onAuthorization(resp, ex)
 		}
 
 	override fun onCreate(savedInstanceState: Bundle?) {
+		// TODO insets
 		enableEdgeToEdge()
 		super.onCreate(savedInstanceState)
 		_binding = ActivityOnboardingBinding.inflate(layoutInflater)
@@ -63,17 +79,44 @@
 		binding.server.editText!!.addTextChangedListener { editable ->
 			binding.accountButton.isEnabled = !editable.isNullOrBlank()
 			binding.anonymousButton.isEnabled = !editable.isNullOrBlank()
+			binding.continueButton.isEnabled = !editable.isNullOrBlank()
 		}
 
-		if (!FirstRunActivity.getFirstRun(this)) {
-			Server.get(this).let { server ->
-				binding.server.editText!!.setText(server.host)
+		binding.defaultServerSwitch.setOnCheckedChangeListener { _, checked ->
+			binding.server.visibility = if (checked) {
+				binding.accountButton.isEnabled = true
+				binding.anonymousButton.isEnabled = true
+				binding.continueButton.isEnabled = true
+				View.GONE
+			} else {
+				View.VISIBLE
 			}
 		}
 
+		binding.reportButton.setOnClickListener {
+			authError?.sendWithAcra() // TODO or silently
+		}
+
+		server = Server.get(this)
+		binding.server.editText!!.setText(server.host)
+		binding.defaultServerSwitch.isChecked = server.host != Server.DEFAULT
+
 		binding.accountButton.setOnClickListener {
-			// TODO OIDC flow https://github.com/openid/AppAuth-Android
-			oidcFlow()
+			// NOTE OIDC flow https://github.com/openid/AppAuth-Android
+			startAuth()
+		}
+
+		binding.server.editText!!.setOnKeyListener { _, keyCode, event ->
+			if (keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_UP && binding.continueButton.visibility == View.VISIBLE) {
+				doContinue()
+				true
+			} else {
+				false
+			}
+		}
+
+		binding.continueButton.setOnClickListener {
+			doContinue()
 		}
 
 		binding.anonymousButton.setOnClickListener {
@@ -81,81 +124,153 @@ 			moveOn()
 		}
 	}
 
+	@OptIn(ExperimentalStdlibApi::class)
+	private fun doContinue() {
+		val md = MessageDigest.getInstance("SHA-256")
+		md.update(binding.server.editText!!.text.toString().encodeToByteArray())
+		val hex = md.digest().toHexString()
+		if (hex == "0621b618c336648cd99dcf85bec68b61741c8679a27ef13eef7f91cc24e2eb80") {
+			binding.continueButton.visibility = View.GONE
+			binding.anonymousButton.visibility = View.VISIBLE
+			binding.accountButton.visibility = View.VISIBLE
+			binding.server.editText!!.setText(server.host)
+			// TODO save shibboleet
+		} else {
+			moveOn()
+		}
+	}
+
 	private fun setServer(hostname: String) {
-		preferences.edit(true) {
-			putString(Server.HOST_KEY, hostname)
+		server.host = hostname
+		server.save(this)
+	}
+
+	private fun startAuth() {
+		setServer(
+			if (binding.defaultServerSwitch.isChecked) {
+				Server.DEFAULT
+			} else {
+				binding.server.editText!!.text.toString()
+			}
+		)
+
+		binding.errorText.visibility = View.GONE
+		binding.reportButton.visibility = View.GONE
+
+		MainScope().launch {
+			server.getTraffic(this@OnboardingActivity, true)
+
+			AuthorizationServiceConfiguration.fetchFromIssuer(
+				server.traffic!!.authEndpoint.toUri(),
+				RetrieveConfigurationCallback { serviceConfiguration, ex ->
+					onRetrieveConfiguration(serviceConfiguration, ex)
+				}
+			)
 		}
 	}
 
 	private fun moveOn() {
-		// TODO test 401/403
-		// TODO show rate-limit info, terms and privacy
+		/*
+		if (server.traffic!!.servers.all { it.seatsRequired.indexOf(user.seat) == -1 }) {
+			// TODO show forbidden info
+		}
+		*/
+
 		binding.server.editText?.text?.toString()?.let { serverAddress -> setServer(serverAddress) }
-		activityLauncher.launch(Intent(this, FeedChooserActivity::class.java))
+		if (!FirstRunActivity.getFirstRun(this)) {
+			startActivity(Intent(this, FeedChooserActivity::class.java))
+		}
+		finish()
+	}
+
+	private fun onAuthError(exception: Exception) {
+		binding.errorText.visibility = View.VISIBLE
+		binding.reportButton.visibility = View.VISIBLE
+		authError = exception
+	}
+
+	private fun onRetrieveConfiguration(
+		config: AuthorizationServiceConfiguration?,
+		exception: AuthorizationException?
+	) {
+		if (exception != null) {
+			Log.e("OIDC", "failed to fetch configuration")
+			onAuthError(exception)
+			return
+		}
+		authState = AuthState(config!!)
+
+		val registrationRequest = RegistrationRequest.Builder(
+			config,
+			listOf(CALLBACK)
+		).build()
+
+		val service = AuthorizationService(this)
+		service.performRegistrationRequest(
+			registrationRequest
+		) { response, ex ->
+			onPerformRegistration(response, ex)
+		}
 	}
 
-	private fun oidcFlow() {
-		AuthorizationServiceConfiguration.fetchFromIssuer(
-			Uri.parse("https://oauth-bimba.apiote.xyz"),
-			RetrieveConfigurationCallback { serviceConfiguration, ex ->
-				if (ex != null) {
-					Log.e("OIDC", "failed to fetch configuration")
-					return@RetrieveConfigurationCallback
-				}
-				authState = AuthState(serviceConfiguration!!)
+	fun onPerformRegistration(response: RegistrationResponse?, ex: AuthorizationException?) {
+		if (ex != null) {
+			Log.e("OIDC", "failed to register client")
+			onAuthError(ex)
+			return
+		}
+		authState.update(response)
+		clientSecret = response!!.clientSecret!!
 
-				/*
-				{"client_id":"46e1be27-a747-4507-a1d4-c383f137745f","client_name":"Bimba","client_secret_expires_at":0,"client_uri":"https://bimba.app","contacts":["questions@bimba.app"],"created_at":"2024-09-09T12:41:30Z","grant_types":["authorization_code","refresh_token"],"jwks":{},"logo_uri":"https://bimba.app/bimba.svg","metadata":{},"owner":"","policy_uri":"","redirect_uris":["bimba://callback"],"registration_access_token":"ory_at_xQWn3kMRuev5ZmCM_BgwnuOo57GRKWz-7LAA8EH2OTA.GXZCCf7XCg8fIwjpGZjZGdgJPMSsMSM9E-6B7bE8nB0","registration_client_uri":"https://oauth-bimba.apiote.xyz/oauth2/register/","request_object_signing_alg":"RS256","response_types":["code","id_token"],"scope":"openid offline","skip_consent":false,"skip_logout_consent":false,"subject_type":"public","token_endpoint_auth_method":"none","tos_uri":"","updated_at":"2024-09-09T14:41:29.982386+02:00","userinfo_signed_response_alg":"none"}
-				 */
-				val authRequest = AuthorizationRequest.Builder(
-					serviceConfiguration,
-					"46e1be27-a747-4507-a1d4-c383f137745f",
-					ResponseTypeValues.CODE,
-					Uri.parse("bimba://callback")
-				)
-					.setScope("openid offline")
-					.build();
+		val authRequest = AuthorizationRequest.Builder(
+			authState.authorizationServiceConfiguration!!,
+			response.clientId,
+			ResponseTypeValues.CODE,
+			CALLBACK
+		)
+			.setScope(SCOPES)
+			.build()
 
-				val authIntent = AuthorizationService(this).getAuthorizationRequestIntent(authRequest)
-				startActivityForResult(authIntent, RC_AUTH)
-			})
+		val authIntent = AuthorizationService(this).getAuthorizationRequestIntent(authRequest)
+		authLauncher.launch(authIntent)
 	}
 
-	override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
-		super.onActivityResult(requestCode, resultCode, data)
-		if (requestCode == RC_AUTH) {
-			val resp = AuthorizationResponse.fromIntent(data!!)
-			val ex = AuthorizationException.fromIntent(data)
-			if (resp != null) {
-				authState.update(resp, ex)
-				AuthorizationService(this).performTokenRequest(
-					resp.createTokenExchangeRequest()
-				) { tokenResponse, tokenException ->
-					if (tokenResponse != null) {
-						authState.update(resp, ex)
-						getSharedPreferences("auth", MODE_PRIVATE).edit {
-							putString("state", authState.jsonSerializeString())
-						}
-						if (tokenResponse.idToken == null) {
-							// TODO openid is needed; show some dialogue
-						}
-						Log.i("OIDC", "got token: id: ${tokenResponse.idToken}\n access: ${tokenResponse.accessToken}\n params: ${tokenResponse.additionalParameters}")
-						// TODO moveOn()
-					} else {
-						// FIXME token exchange failed: Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method).
-						Log.e("OIDC", "token exchange failed: ${tokenException!!.message}")
-						tokenException.printStackTrace()
-					}
-				}
-			} else {
-				Log.e("OIDC", "auth failed: ${ex!!.message}")
+	fun onAuthorization(resp: AuthorizationResponse?, ex: AuthorizationException?) {
+		authState.update(resp, ex)
+		if (resp != null) {
+			AuthorizationService(this).performTokenRequest(
+				resp.createTokenExchangeRequest(),
+				ClientSecretPost(clientSecret)
+			) { tokenResponse, tokenException ->
+				onTokenRequest(tokenResponse, tokenException)
 			}
+		} else {
+			Log.e("OIDC", "auth failed: ${ex!!.message}")
+			onAuthError(ex)
+		}
+	}
+
+	fun onTokenRequest(tokenResponse: TokenResponse?, tokenException: AuthorizationException?) {
+		authState.update(tokenResponse, tokenException)
+		if (tokenResponse != null) {
+			getSharedPreferences("auth", MODE_PRIVATE).edit {
+				putString("state", authState.jsonSerializeString())
+			}
+			Log.i(
+				"OIDC",
+				"got token: id: ${tokenResponse.idToken}\n access: ${tokenResponse.accessToken}\n params: ${tokenResponse.additionalParameters}"
+			)
+			moveOn()
+		} else {
+			Log.e("OIDC", "token exchange failed: ${tokenException!!.message}")
+			onAuthError(tokenException)
 		}
 	}
 
 	companion object {
 		const val PREFERENCES_NAME = "shp"
 		const val IN_FEEDS_TRANSACTION = "inFeedsTransaction"
-		const val RC_AUTH = 42
+		val CALLBACK = "bimba://callback".toUri()
+		const val SCOPES = "openid offline_access email profile"
 	}
 }
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt
index 2906b7096f0283669d3c38ab9c82ae4e94e96838..ef9c999cb5141653559b50d3b9f650e8551ed502 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt
@@ -10,13 +10,13 @@ import androidx.core.content.edit
 import androidx.core.database.sqlite.transaction
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
+import xyz.apiote.bimba.czwek.R
 import xyz.apiote.bimba.czwek.api.Server
 import xyz.apiote.fruchtfleisch.Reader
 import xyz.apiote.fruchtfleisch.Writer
 import java.io.File
 import java.net.URLEncoder
 import java.time.LocalDate
-import xyz.apiote.bimba.czwek.R
 
 class OfflineRepository(context: Context) : Repository {
 	private val db =
@@ -24,7 +24,7 @@ 		SQLiteDatabase.openOrCreateDatabase(context.getDatabasePath("favourites").path, null)
 
 	fun saveFeedCache(context: Context, feedInfos: Map<String, FeedInfo>) {
 		val file = File(
-			context.filesDir, URLEncoder.encode(Server.get(context).apiPath, "utf-8")
+			context.filesDir, URLEncoder.encode(Server.get(context).getSelectedServer().url, "utf-8")
 		)
 		context.getSharedPreferences("offlineFeeds", Context.MODE_PRIVATE).edit {
 			putInt("version", FeedInfo.VERSION.toInt())
@@ -153,7 +153,7 @@ 		server: Server
 	): Map<String, FeedInfo>? {
 		val file = File(
 			context.filesDir, withContext(Dispatchers.IO) {
-				URLEncoder.encode(server.apiPath, "utf-8")
+				URLEncoder.encode(server.getSelectedServer().url, "utf-8")
 			}
 		)
 		if (!file.exists()) {




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt
index fa1dcca8a770916ab39206fa0dbf10fff6ab0995..315865ff8a8204d9b3ae0c4173d6041605ba8460 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt
@@ -173,7 +173,7 @@ 		context: Context,
 		bl: Position,
 		tr: Position,
 	): List<Locatable>? {
-		val transitousQueryables = if (Server.get(context).feeds.transitousEnabled()) {
+		val transitousQueryables = if (Server.get(context).getSelectedServer().feeds.transitousEnabled()) {
 			locateTransitousQueryables(
 				Position(bl.latitude, tr.longitude),
 				Position(tr.latitude, bl.longitude),
@@ -187,7 +187,7 @@ 			}.filterNotNull()
 		} else {
 			null
 		}
-		val bimbaQueryables = if (Server.get(context).feeds.bimbaEnabled()) {
+		val bimbaQueryables = if (Server.get(context).getSelectedServer().feeds.bimbaEnabled()) {
 			val result = xyz.apiote.bimba.czwek.api.getLocatablesIn(
 				context,
 				Server.get(context),
@@ -276,12 +276,12 @@
 	override suspend fun queryQueryables(
 		query: String, context: Context, feedID: String?
 	): List<Queryable>? {
-		val transitousQueryables = if (Server.get(context).feeds.transitousEnabled() || feedID == "transitous") {
+		val transitousQueryables = if (Server.get(context).getSelectedServer().feeds.transitousEnabled() || feedID == "transitous") {
 			getTransitousQueryables(query, context)
 		} else {
 			null
 		}
-		val bimbaQueryables = if (Server.get(context).feeds.bimbaEnabled() && feedID == null) {  // TODO select bimba feed
+		val bimbaQueryables = if (Server.get(context).getSelectedServer().feeds.bimbaEnabled() && feedID == null) {  // TODO select bimba feed
 			getQueryables(query, null, context, "query")
 		} else {
 			null
@@ -296,12 +296,12 @@
 	override suspend fun locateQueryables(
 		position: Position, context: Context
 	): List<Queryable>? {
-		val transitousQueryables = if (Server.get(context).feeds.transitousEnabled()) {
+		val transitousQueryables = if (Server.get(context).getSelectedServer().feeds.transitousEnabled()) {
 			locateTransitousQueryables(position, context)
 		} else {
 			null
 		}
-		val bimbaQueryables = if (Server.get(context).feeds.bimbaEnabled()) {
+		val bimbaQueryables = if (Server.get(context).getSelectedServer().feeds.bimbaEnabled()) {
 			getQueryables(null, position, context, "locate")
 		} else {
 			null




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedChooserActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedChooserActivity.kt
index b712d05ae2e23872fe8dea94d7303af43f68b91d..9c780e3faebf372fba4a489632c5a7da4fa38fcc 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedChooserActivity.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedChooserActivity.kt
@@ -119,7 +119,8 @@ 		}
 	}
 
 	private fun moveOn() {
-		viewModel.settings.value?.save(this, Server.get(this))
+		val server = Server.get(this)
+		server.getSelectedServer().feeds = viewModel.settings.value?: FeedsSettings(mutableMapOf())
 		val preferences = getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE)
 		preferences.edit(true) {
 			putBoolean(OnboardingActivity.IN_FEEDS_TRANSACTION, false)




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedSettings.kt b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedSettings.kt
index 0407d15f16954e42e900ebad7f43b6e268603eec..dcc0f07126ac59d2fa825e5516f78478e0cbb118 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedSettings.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedSettings.kt
@@ -5,11 +5,16 @@
 package xyz.apiote.bimba.czwek.settings.feeds
 
 import android.content.Context
+import android.content.Context.MODE_PRIVATE
+import android.util.Log
 import androidx.appcompat.app.AppCompatActivity
 import androidx.core.content.edit
 import com.github.jershell.kbson.KBson
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
 import kotlinx.serialization.Serializable
 import xyz.apiote.bimba.czwek.api.Server
+import xyz.apiote.bimba.czwek.api.Server.Companion.DEFAULT
 import java.net.URLEncoder
 
 @Serializable
@@ -21,22 +26,26 @@ 	fun getIDs() = activeFeeds().filter { it != "transitous" }.joinToString(",")
 	fun transitousEnabled() = activeFeeds().contains("transitous")
 	fun bimbaEnabled() = activeFeeds().filter { it != "transitous" }.isNotEmpty()
 
+	@Deprecated("use save on Server")
 	fun save(context: Context, server: Server) {
 		val doc = KBson().dump(serializer(), this).toHexString()
 		val feedsPreferences =
 			context.getSharedPreferences(PREFERENCES_NAME, AppCompatActivity.MODE_PRIVATE)
 		feedsPreferences.edit {
-			val key = URLEncoder.encode(server.apiPath, "utf-8")
+			val key = URLEncoder.encode(server.getSelectedServer().url, "utf-8")
 			putString(key, doc)
 		}
 	}
 
 	companion object {
 		const val PREFERENCES_NAME = "feeds_settings"
-		fun load(context: Context, apiPath: String = Server.get(context).apiPath): FeedsSettings {
+
+		@Deprecated("load with Server")
+		fun load(
+			context: Context, apiPath: String = Server.get(context).getSelectedServer().url
+		): FeedsSettings {
 			val doc = context.getSharedPreferences(
-				PREFERENCES_NAME,
-				Context.MODE_PRIVATE
+				PREFERENCES_NAME, Context.MODE_PRIVATE
 			).getString(URLEncoder.encode(apiPath, "utf-8"), null)
 			return doc?.let { KBson().load(serializer(), doc.hexToByteArray()) } ?: FeedsSettings(
 				mutableMapOf()
@@ -47,28 +56,38 @@ }
 
 @Serializable
 data class FeedSettings(
-	val enabled: Boolean,
-	val useOnline: Boolean
+	val enabled: Boolean, val useOnline: Boolean
 )
 
 fun migrateFeedsSettings(context: Context, server: Server = Server.get(context)) {
-	val shp =
-		context.getSharedPreferences(
-			URLEncoder.encode(server.apiPath, "utf-8"),
-			AppCompatActivity.MODE_PRIVATE
-		)
-	if (shp.all.isEmpty()) {
+	if (server.traffic == null) {
+		Log.w("migrateFeedsSetting", "server.traffic is null, not migrating")
 		return
 	}
-
-	val feedsSettings = FeedsSettings(mutableMapOf())
-	shp.all.forEach { (feedID, enabled) ->
-		if (enabled as Boolean) {
-			feedsSettings.settings[feedID] = FeedSettings(enabled = true, useOnline = true)
+	val preferences = context.getSharedPreferences("shp", MODE_PRIVATE)
+	var apiPath = preferences.getString(Server.API_PATH_KEY, DEFAULT)!!
+	val shp = context.getSharedPreferences(
+		URLEncoder.encode(apiPath, "utf-8"), MODE_PRIVATE
+	)
+	if (!shp.all.isEmpty()) {
+		val feedsSettings = FeedsSettings(mutableMapOf())
+		shp.all.forEach { (feedID, enabled) ->
+			if (enabled as Boolean) {
+				feedsSettings.settings[feedID] = FeedSettings(enabled = true, useOnline = true)
+			}
+		}
+		shp.edit {
+			clear()
 		}
+		val ix = server.traffic!!.servers.indexOfFirst { it.url == apiPath }
+		server.traffic!!.servers[ix].feeds = feedsSettings
+		server.save(context)
+		return
 	}
-	shp.edit {
-		clear()
+	val feedsSettings = FeedsSettings.load(context, apiPath)
+	if (feedsSettings.settings.isNotEmpty()) {
+		val ix = server.traffic!!.servers.indexOfFirst { it.url == apiPath }
+		server.traffic!!.servers[ix].feeds = feedsSettings
+		server.save(context)
 	}
-	feedsSettings.save(context, server)
 }




diff --git a/app/src/main/res/layout/account.xml b/app/src/main/res/layout/account.xml
index f0edee1eea3f31121dbf710167a97dcb94d810e0..2235486a61ebe9937195aaca26ce4f5252a621cd 100644
--- a/app/src/main/res/layout/account.xml
+++ b/app/src/main/res/layout/account.xml
@@ -1,4 +1,9 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!--
+SPDX-FileCopyrightText: Adam Evyčędo
+
+SPDX-License-Identifier: GPL-3.0-or-later
+-->
 <androidx.coordinatorlayout.widget.CoordinatorLayout 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"




diff --git a/app/src/main/res/layout/account_edit.xml b/app/src/main/res/layout/account_edit.xml
index a912c25b2175296d2b4ce9b1baf3d2de9879facb..f7f268ca9eadceaa66962eb81863197f4b58db00 100644
--- a/app/src/main/res/layout/account_edit.xml
+++ b/app/src/main/res/layout/account_edit.xml
@@ -1,4 +1,9 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!--
+SPDX-FileCopyrightText: Adam Evyčędo
+
+SPDX-License-Identifier: GPL-3.0-or-later
+-->
 <androidx.coordinatorlayout.widget.CoordinatorLayout 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"




diff --git a/app/src/main/res/layout/activity_onboarding.xml b/app/src/main/res/layout/activity_onboarding.xml
index dc3a1d72dbf4dd6190d42ec6984161a265996cbe..673d9280fc2ca82b685acde1b72e0f263d1d13d9 100644
--- a/app/src/main/res/layout/activity_onboarding.xml
+++ b/app/src/main/res/layout/activity_onboarding.xml
@@ -1,4 +1,8 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8"?><!--
+SPDX-FileCopyrightText: Adam Evyčędo
+
+SPDX-License-Identifier: GPL-3.0-or-later
+-->
 <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"
@@ -29,57 +33,126 @@ 		app:layout_constraintStart_toStartOf="@+id/logo"
 		app:layout_constraintTop_toBottomOf="@+id/logo" />
 
 	<com.google.android.material.textview.MaterialTextView
-		android:layout_width="0dp"
+		android:id="@+id/defaultServerSwitchLabel"
+		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
 		android:layout_marginStart="8dp"
+		android:text="@string/use_default_server"
+		android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
+		app:layout_constraintBottom_toTopOf="@+id/guideline40"
+		app:layout_constraintStart_toStartOf="parent" />
+
+	<com.google.android.material.materialswitch.MaterialSwitch
+		android:id="@+id/defaultServerSwitch"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
 		android:layout_marginEnd="8dp"
-		android:layout_marginBottom="16dp"
-		android:text="@string/seatbelts_everyone"
-		android:textAlignment="center"
-		android:textAppearance="@style/TextAppearance.Material3.BodySmall"
-		android:textStyle="italic"
-		app:layout_constraintBottom_toBottomOf="parent"
+		android:checked="true"
+		app:layout_constraintBottom_toBottomOf="@+id/defaultServerSwitchLabel"
 		app:layout_constraintEnd_toEndOf="parent"
-		app:layout_constraintStart_toStartOf="parent" />
+		app:layout_constraintTop_toTopOf="@+id/defaultServerSwitchLabel" />
+
+	<androidx.constraintlayout.widget.Guideline
+		android:id="@+id/guideline40"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:orientation="horizontal"
+		app:layout_constraintGuide_percent=".4" />
 
 	<com.google.android.material.textfield.TextInputLayout
 		android:id="@+id/server"
-		android:layout_width="400dp"
+		android:layout_width="0dp"
 		android:layout_height="wrap_content"
 		android:layout_marginStart="16dp"
-		android:layout_marginTop="64dp"
+		android:layout_marginTop="16dp"
 		android:layout_marginEnd="16dp"
 		android:hint="@string/bimba_server_address_hint"
+		android:visibility="gone"
 		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintStart_toStartOf="parent"
-		app:layout_constraintTop_toBottomOf="@+id/app_name">
+		app:layout_constraintTop_toBottomOf="@+id/defaultServerSwitchLabel">
 
 		<com.google.android.material.textfield.TextInputEditText
 			android:layout_width="match_parent"
 			android:layout_height="wrap_content"
-			android:text="bimba.apiote.xyz"
-			tools:ignore="HardcodedText" />
+			tools:text="bimba.app" />
 
 	</com.google.android.material.textfield.TextInputLayout>
 
+	<androidx.constraintlayout.widget.Guideline
+		android:id="@+id/guideline75"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:orientation="horizontal"
+		app:layout_constraintGuide_percent=".75" />
+
 	<Button
 		android:id="@+id/account_button"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
-		android:layout_marginTop="64dp"
 		android:text="@string/log_in_or_sign_up"
+		android:visibility="gone"
 		app:layout_constraintEnd_toEndOf="@+id/server"
 		app:layout_constraintStart_toStartOf="@+id/server"
-		app:layout_constraintTop_toBottomOf="@+id/server" />
+		app:layout_constraintTop_toTopOf="@+id/guideline75" />
 
 	<Button
 		android:id="@+id/anonymous_button"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
-		android:layout_marginTop="16dp"
 		android:text="@string/continue_without_account"
+		android:visibility="gone"
 		app:layout_constraintEnd_toEndOf="@+id/account_button"
 		app:layout_constraintStart_toStartOf="@+id/account_button"
 		app:layout_constraintTop_toBottomOf="@+id/account_button" />
+
+	<Button
+		android:id="@+id/continue_button"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:text="@string/continue_"
+		app:layout_constraintEnd_toEndOf="@+id/account_button"
+		app:layout_constraintStart_toStartOf="@+id/account_button"
+		app:layout_constraintTop_toTopOf="@+id/account_button" />
+
+	<com.google.android.material.textview.MaterialTextView
+		android:id="@+id/errorText"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_marginStart="16dp"
+		android:text="@string/error_while_logging_in"
+		android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
+		android:textColor="?colorError"
+		android:visibility="gone"
+		app:layout_constraintBottom_toTopOf="@id/seatbelts"
+		app:layout_constraintStart_toStartOf="parent"
+		app:layout_constraintTop_toBottomOf="@id/anonymous_button" />
+
+	<Button
+		android:id="@+id/reportButton"
+		style="@style/Widget.Material3.Button.TextButton"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_marginEnd="16dp"
+		android:text="@string/send_a_report"
+		android:visibility="gone"
+		app:layout_constraintBottom_toBottomOf="@+id/errorText"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintTop_toTopOf="@+id/errorText" />
+
+	<com.google.android.material.textview.MaterialTextView
+		android:id="@+id/seatbelts"
+		android:layout_width="0dp"
+		android:layout_height="wrap_content"
+		android:layout_marginStart="8dp"
+		android:layout_marginEnd="8dp"
+		android:layout_marginBottom="16dp"
+		android:text="@string/seatbelts_everyone"
+		android:textAlignment="center"
+		android:textAppearance="@style/TextAppearance.Material3.BodySmall"
+		android:textStyle="italic"
+		app:layout_constraintBottom_toBottomOf="parent"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintStart_toStartOf="parent" />
 
 </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file




diff --git a/app/src/main/res/menu/account.xml b/app/src/main/res/menu/account.xml
index fc3b526989d14d93d2619016ddafe280fbb33d60..75bbdd346d97ad31bbea22657f24529c6790bb48 100644
--- a/app/src/main/res/menu/account.xml
+++ b/app/src/main/res/menu/account.xml
@@ -1,4 +1,9 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!--
+SPDX-FileCopyrightText: Adam Evyčędo
+
+SPDX-License-Identifier: GPL-3.0-or-later
+-->
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
 	xmlns:app="http://schemas.android.com/apk/res-auto">
 	<item




diff --git a/app/src/main/res/menu/account_edit.xml b/app/src/main/res/menu/account_edit.xml
index 960bf340e27c216bd3f962889429766ecce83464..e8687e73ae1f7bc54ba57f18336688381907d80b 100644
--- a/app/src/main/res/menu/account_edit.xml
+++ b/app/src/main/res/menu/account_edit.xml
@@ -1,4 +1,9 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!--
+SPDX-FileCopyrightText: Adam Evyčędo
+
+SPDX-License-Identifier: GPL-3.0-or-later
+-->
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
 	xmlns:app="http://schemas.android.com/apk/res-auto">
 	<item




diff --git a/app/src/main/res/menu/feeds_menu.xml b/app/src/main/res/menu/feeds_menu.xml
index bc87c01e295ede32e965dc043da89413cc2cefc3..a8f7f52b851ccd769f462f6b93e893f9161dd9ac 100644
--- a/app/src/main/res/menu/feeds_menu.xml
+++ b/app/src/main/res/menu/feeds_menu.xml
@@ -1,4 +1,9 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!--
+SPDX-FileCopyrightText: Adam Evyčędo
+
+SPDX-License-Identifier: GPL-3.0-or-later
+-->
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
 	xmlns:app="http://schemas.android.com/apk/res-auto">
 	<item




diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1ed54e2a0934810e8fbf093cc95e1de3b396c571..dc091d1e59727db5a7f93bb256745feee53bc2a0 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -331,4 +331,7 @@ 	Using
 	<string name="name">Name</string>
 	<string name="resend_verification_code">Resend verification code</string>
 	<string name="title_account">Account</string>
+	<string name="use_default_server">Use default server</string>
+	<string name="error_while_logging_in">Error while logging in</string>
+	<string name="send_a_report">Send a report</string>
 </resources>




diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml
index 0439f88d8155b6d4e6a196da0a663ec31eb810cd..b4e5c22ee8094ca3cc8e09e926dccab7bcfbf712 100644
--- a/app/src/main/res/values-en-rGB/strings.xml
+++ b/app/src/main/res/values-en-rGB/strings.xml
@@ -313,4 +313,7 @@ 	end of journey’s leg
 	<string name="use_as_origin">use as origin</string>
 	<string name="use_as_destination">use as destination</string>
 	<string name="here">here</string>
+	<string name="use_default_server">Use default server</string>
+	<string name="error_while_logging_in">Error while logging in</string>
+	<string name="send_a_report">Send a report</string>
 </resources>




diff --git a/app/src/main/res/values-en-rUS/strings.xml b/app/src/main/res/values-en-rUS/strings.xml
index 3b80001be91904e0c223070fd8bb031f75a20534..ec0c8ce5e17a09cf5e2e3b347e827c4bf90fdad9 100644
--- a/app/src/main/res/values-en-rUS/strings.xml
+++ b/app/src/main/res/values-en-rUS/strings.xml
@@ -311,4 +311,7 @@ 	end of journey’s leg
 	<string name="use_as_origin">use as origin</string>
 	<string name="use_as_destination">use as destination</string>
 	<string name="here">here</string>
+	<string name="use_default_server">Use default server</string>
+	<string name="error_while_logging_in">Error while logging in</string>
+	<string name="send_a_report">Send a report</string>
 </resources>
\ No newline at end of file