Handle Android API authorization setup

This commit is contained in:
Codex
2026-06-27 08:12:54 +03:00
parent ab2557ea26
commit f1f1cbb1e0
3 changed files with 56 additions and 12 deletions
+2 -2
View File
@@ -10,7 +10,7 @@ android {
applicationId = "xyz.kusoft.tradebotmonitor" applicationId = "xyz.kusoft.tradebotmonitor"
minSdk = 26 minSdk = 26
targetSdk = 36 targetSdk = 36
versionCode = 3 versionCode = 4
versionName = "0.2.0" versionName = "0.2.1"
} }
} }
@@ -33,7 +33,6 @@ class MainActivity : Activity() {
private val executor = Executors.newSingleThreadExecutor() private val executor = Executors.newSingleThreadExecutor()
private val mainHandler = Handler(Looper.getMainLooper()) private val mainHandler = Handler(Looper.getMainLooper())
private val scrollPositions = mutableMapOf<String, Int>() private val scrollPositions = mutableMapOf<String, Int>()
private val clockFormat = SimpleDateFormat("HH:mm", Locale("ru", "RU"))
private var snapshot: BotSnapshot? = null private var snapshot: BotSnapshot? = null
private var lastError: String = "" private var lastError: String = ""
@@ -78,10 +77,10 @@ class MainActivity : Activity() {
bottomNav = LinearLayout(this).apply { bottomNav = LinearLayout(this).apply {
orientation = LinearLayout.HORIZONTAL orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER gravity = Gravity.CENTER
setPadding(dp(10), dp(7), dp(10), dp(9)) setPadding(dp(10), dp(7), dp(10), dp(9) + navigationBarInset())
background = boxDrawable(palette.panel, palette.line, 0) background = boxDrawable(palette.panel, palette.line, 0)
} }
root.addView(bottomNav, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(64))) root.addView(bottomNav, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(64) + navigationBarInset()))
renderBottomNav() renderBottomNav()
render(saveScroll = false) render(saveScroll = false)
@@ -187,7 +186,7 @@ class MainActivity : Activity() {
box.addView(statusLine("AI Торговля", modeBadgeText())) box.addView(statusLine("AI Торговля", modeBadgeText()))
val data = snapshot val data = snapshot
if (data == null) { if (data == null) {
box.addView(emptyState("Нет данных от API. ${lastError.ifBlank { "Проверь подключение." }}").top(dp(18))) box.addView(connectionStatePanel().top(dp(18)))
return wrapScroll(box) return wrapScroll(box)
} }
@@ -211,7 +210,7 @@ class MainActivity : Activity() {
box.addView(statusLine("Рынки", "12 spot-пар")) box.addView(statusLine("Рынки", "12 spot-пар"))
val data = snapshot val data = snapshot
if (data == null) { if (data == null) {
box.addView(emptyState("Нет данных от API. ${lastError.ifBlank { "Проверь подключение." }}").top(dp(18))) box.addView(connectionStatePanel().top(dp(18)))
return wrapScroll(box) return wrapScroll(box)
} }
@@ -232,7 +231,7 @@ class MainActivity : Activity() {
box.addView(statusLine("Сделка и риск", prefs.selectedSymbol)) box.addView(statusLine("Сделка и риск", prefs.selectedSymbol))
val data = snapshot val data = snapshot
if (data == null) { if (data == null) {
box.addView(emptyState("Нет данных от API. ${lastError.ifBlank { "Проверь подключение." }}").top(dp(18))) box.addView(connectionStatePanel().top(dp(18)))
return wrapScroll(box) return wrapScroll(box)
} }
@@ -255,7 +254,7 @@ class MainActivity : Activity() {
box.addView(statusLine("Активы", modeBadgeText())) box.addView(statusLine("Активы", modeBadgeText()))
val data = snapshot val data = snapshot
if (data == null) { if (data == null) {
box.addView(emptyState("Нет данных от API. ${lastError.ifBlank { "Проверь подключение." }}").top(dp(18))) box.addView(connectionStatePanel().top(dp(18)))
return wrapScroll(box) return wrapScroll(box)
} }
@@ -662,15 +661,46 @@ class MainActivity : Activity() {
addView(LinearLayout(this@MainActivity).apply { addView(LinearLayout(this@MainActivity).apply {
orientation = LinearLayout.HORIZONTAL orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL gravity = Gravity.CENTER_VERTICAL
addView(text(clockFormat.format(Date()), 13f, Typeface.BOLD), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)) addView(text(title, 21f, Typeface.BOLD), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
addView(statusBadge(badge, if (snapshot?.running == true) "ok" else "warn")) addView(statusBadge(badge, if (snapshot?.running == true) "ok" else "warn"))
}) })
addView(text(title, 21f, Typeface.BOLD).top(dp(18)))
if (lastError.isNotBlank() && snapshot == null) { if (lastError.isNotBlank() && snapshot == null) {
addView(text(lastError.take(120), 12f, Typeface.NORMAL, palette.red).top(dp(6))) addView(text(lastError.take(120), 12f, Typeface.NORMAL, palette.red).top(dp(6)))
} }
} }
private fun connectionStatePanel(): View {
if (!isAuthError()) {
return emptyState("Нет данных от API. ${lastError.ifBlank { "Проверь подключение." }}")
}
val apiInput = input("Адрес API бота", prefs.apiBaseUrl)
val authInput = input("Логин и пароль: login:password", prefs.commandToken).apply {
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
}
return section("Нужен вход в API", LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
addView(text("Сервер tb.kusoft.xyz отвечает 401, значит он доступен, но требует авторизацию. Введите доступ в формате login:password, приложение само отправит Basic Auth.", 13f, Typeface.NORMAL, palette.muted))
addView(apiInput.top(dp(12)))
addView(authInput.top(dp(8)))
addView(actionRow(
"Подключиться" to {
prefs.apiBaseUrl = apiInput.text.toString()
prefs.commandToken = authInput.text.toString()
toast("Доступ сохранен, проверяю API")
refreshData(silent = false)
},
"Настройки" to {
activeTab = "settings"
renderBottomNav()
render(saveScroll = false)
},
).top(dp(10)))
})
}
private fun isAuthError(): Boolean =
lastError.contains("401")
private fun searchPlaceholder(value: String): View = private fun searchPlaceholder(value: String): View =
TextView(this).apply { TextView(this).apply {
text = value text = value
@@ -828,7 +858,7 @@ class MainActivity : Activity() {
private fun screenContent(): LinearLayout = private fun screenContent(): LinearLayout =
LinearLayout(this).apply { LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL orientation = LinearLayout.VERTICAL
setPadding(dp(18), dp(16), dp(18), dp(22)) setPadding(dp(18), dp(16) + statusBarInset(), dp(18), dp(22))
} }
private fun wrapScroll(content: View): View = private fun wrapScroll(content: View): View =
@@ -1042,6 +1072,17 @@ class MainActivity : Activity() {
private fun dp(value: Int): Int = private fun dp(value: Int): Int =
(value * resources.displayMetrics.density).toInt() (value * resources.displayMetrics.density).toInt()
private fun statusBarInset(): Int =
systemBarInset("status_bar_height")
private fun navigationBarInset(): Int =
systemBarInset("navigation_bar_height")
private fun systemBarInset(name: String): Int {
val id = resources.getIdentifier(name, "dimen", "android")
return if (id > 0) resources.getDimensionPixelSize(id) else 0
}
private fun toast(message: String) { private fun toast(message: String) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show() Toast.makeText(this, message, Toast.LENGTH_LONG).show()
} }
@@ -76,6 +76,9 @@ class TradeBotApi(
val text = stream?.bufferedReader(StandardCharsets.UTF_8)?.use(BufferedReader::readText).orEmpty() val text = stream?.bufferedReader(StandardCharsets.UTF_8)?.use(BufferedReader::readText).orEmpty()
connection.disconnect() connection.disconnect()
if (code !in 200..299) { if (code !in 200..299) {
if (code == HttpURLConnection.HTTP_UNAUTHORIZED) {
throw IllegalStateException("HTTP 401: сервер требует логин и пароль")
}
throw IllegalStateException("HTTP $code: ${text.take(240)}") throw IllegalStateException("HTTP $code: ${text.take(240)}")
} }
return if (text.isBlank()) JSONObject() else JSONObject(text) return if (text.isBlank()) JSONObject() else JSONObject(text)