From f1f1cbb1e0232cacf37977574c4750ef23928851 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 27 Jun 2026 08:12:54 +0300 Subject: [PATCH] Handle Android API authorization setup --- android/TradeBotMonitor/app/build.gradle.kts | 4 +- .../kusoft/tradebotmonitor/MainActivity.kt | 61 ++++++++++++++++--- .../xyz/kusoft/tradebotmonitor/TradeBotApi.kt | 3 + 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/android/TradeBotMonitor/app/build.gradle.kts b/android/TradeBotMonitor/app/build.gradle.kts index 43b928d..c5deb35 100644 --- a/android/TradeBotMonitor/app/build.gradle.kts +++ b/android/TradeBotMonitor/app/build.gradle.kts @@ -10,7 +10,7 @@ android { applicationId = "xyz.kusoft.tradebotmonitor" minSdk = 26 targetSdk = 36 - versionCode = 3 - versionName = "0.2.0" + versionCode = 4 + versionName = "0.2.1" } } diff --git a/android/TradeBotMonitor/app/src/main/java/xyz/kusoft/tradebotmonitor/MainActivity.kt b/android/TradeBotMonitor/app/src/main/java/xyz/kusoft/tradebotmonitor/MainActivity.kt index a370130..c1124ec 100644 --- a/android/TradeBotMonitor/app/src/main/java/xyz/kusoft/tradebotmonitor/MainActivity.kt +++ b/android/TradeBotMonitor/app/src/main/java/xyz/kusoft/tradebotmonitor/MainActivity.kt @@ -33,7 +33,6 @@ class MainActivity : Activity() { private val executor = Executors.newSingleThreadExecutor() private val mainHandler = Handler(Looper.getMainLooper()) private val scrollPositions = mutableMapOf() - private val clockFormat = SimpleDateFormat("HH:mm", Locale("ru", "RU")) private var snapshot: BotSnapshot? = null private var lastError: String = "" @@ -78,10 +77,10 @@ class MainActivity : Activity() { bottomNav = LinearLayout(this).apply { orientation = LinearLayout.HORIZONTAL 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) } - root.addView(bottomNav, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(64))) + root.addView(bottomNav, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(64) + navigationBarInset())) renderBottomNav() render(saveScroll = false) @@ -187,7 +186,7 @@ class MainActivity : Activity() { box.addView(statusLine("AI Торговля", modeBadgeText())) val data = snapshot if (data == null) { - box.addView(emptyState("Нет данных от API. ${lastError.ifBlank { "Проверь подключение." }}").top(dp(18))) + box.addView(connectionStatePanel().top(dp(18))) return wrapScroll(box) } @@ -211,7 +210,7 @@ class MainActivity : Activity() { box.addView(statusLine("Рынки", "12 spot-пар")) val data = snapshot if (data == null) { - box.addView(emptyState("Нет данных от API. ${lastError.ifBlank { "Проверь подключение." }}").top(dp(18))) + box.addView(connectionStatePanel().top(dp(18))) return wrapScroll(box) } @@ -232,7 +231,7 @@ class MainActivity : Activity() { box.addView(statusLine("Сделка и риск", prefs.selectedSymbol)) val data = snapshot if (data == null) { - box.addView(emptyState("Нет данных от API. ${lastError.ifBlank { "Проверь подключение." }}").top(dp(18))) + box.addView(connectionStatePanel().top(dp(18))) return wrapScroll(box) } @@ -255,7 +254,7 @@ class MainActivity : Activity() { box.addView(statusLine("Активы", modeBadgeText())) val data = snapshot if (data == null) { - box.addView(emptyState("Нет данных от API. ${lastError.ifBlank { "Проверь подключение." }}").top(dp(18))) + box.addView(connectionStatePanel().top(dp(18))) return wrapScroll(box) } @@ -662,15 +661,46 @@ class MainActivity : Activity() { addView(LinearLayout(this@MainActivity).apply { orientation = LinearLayout.HORIZONTAL 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(text(title, 21f, Typeface.BOLD).top(dp(18))) if (lastError.isNotBlank() && snapshot == null) { 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 = TextView(this).apply { text = value @@ -828,7 +858,7 @@ class MainActivity : Activity() { private fun screenContent(): LinearLayout = LinearLayout(this).apply { 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 = @@ -1042,6 +1072,17 @@ class MainActivity : Activity() { private fun dp(value: Int): Int = (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) { Toast.makeText(this, message, Toast.LENGTH_LONG).show() } diff --git a/android/TradeBotMonitor/app/src/main/java/xyz/kusoft/tradebotmonitor/TradeBotApi.kt b/android/TradeBotMonitor/app/src/main/java/xyz/kusoft/tradebotmonitor/TradeBotApi.kt index f3657ae..9e9ea76 100644 --- a/android/TradeBotMonitor/app/src/main/java/xyz/kusoft/tradebotmonitor/TradeBotApi.kt +++ b/android/TradeBotMonitor/app/src/main/java/xyz/kusoft/tradebotmonitor/TradeBotApi.kt @@ -76,6 +76,9 @@ class TradeBotApi( val text = stream?.bufferedReader(StandardCharsets.UTF_8)?.use(BufferedReader::readText).orEmpty() connection.disconnect() if (code !in 200..299) { + if (code == HttpURLConnection.HTTP_UNAUTHORIZED) { + throw IllegalStateException("HTTP 401: сервер требует логин и пароль") + } throw IllegalStateException("HTTP $code: ${text.take(240)}") } return if (text.isBlank()) JSONObject() else JSONObject(text)