From 0762481005bc147e5b059cc0eb64749d940c63d5 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Fri, 10 Apr 2026 21:49:47 +0500 Subject: [PATCH 1/2] feature: implement proxy connectivity testing and improve Android system proxy resolution - Add `ProxyTester` interface and `ProxyTesterImpl` to verify network reachability through a configured proxy using the GitHub API. - Implement a "Test" button in the network settings UI to trigger proxy validation and display latency or specific error messages (DNS, timeout, auth, etc.). - Add localized strings for proxy test outcomes in English, Arabic, Bengali, Spanish, French, Hindi, Italian, Japanese, Korean, Polish, Russian, Turkish, and Simplified Chinese. - Improve Android system proxy support by explicitly resolving `https.proxyHost` and `http.proxyHost` properties, ensuring traffic (including downloads) correctly follows device-level proxy settings. - Update `TweaksViewModel` and `TweaksState` to manage proxy testing logic and loading states. --- .claude/settings.local.json | 4 +- .../baselineProfiles/0/composeApp-release.dm | Bin 11283 -> 11430 bytes .../baselineProfiles/1/composeApp-release.dm | Bin 11276 -> 11411 bytes .../data/network/HttpClientFactory.android.kt | 25 ++++- .../core/data/services/AndroidDownloader.kt | 8 +- .../zed/rainxch/core/data/di/SharedModule.kt | 6 ++ .../core/data/network/ProxyTesterImpl.kt | 70 +++++++++++++ .../core/domain/network/ProxyTester.kt | 44 ++++++++ .../composeResources/values-ar/strings-ar.xml | 9 ++ .../composeResources/values-bn/strings-bn.xml | 9 ++ .../composeResources/values-es/strings-es.xml | 9 ++ .../composeResources/values-fr/strings-fr.xml | 9 ++ .../composeResources/values-hi/strings-hi.xml | 9 ++ .../composeResources/values-it/strings-it.xml | 9 ++ .../composeResources/values-ja/strings-ja.xml | 9 ++ .../composeResources/values-ko/strings-ko.xml | 9 ++ .../composeResources/values-pl/strings-pl.xml | 9 ++ .../composeResources/values-ru/strings-ru.xml | 9 ++ .../composeResources/values-tr/strings-tr.xml | 9 ++ .../values-zh-rCN/strings-zh-rCN.xml | 9 ++ .../composeResources/values/strings.xml | 9 ++ .../tweaks/presentation/TweaksAction.kt | 2 + .../tweaks/presentation/TweaksEvent.kt | 8 ++ .../rainxch/tweaks/presentation/TweaksRoot.kt | 15 +++ .../tweaks/presentation/TweaksState.kt | 1 + .../tweaks/presentation/TweaksViewModel.kt | 99 ++++++++++++++++++ .../components/sections/Network.kt | 98 +++++++++++++---- 27 files changed, 471 insertions(+), 26 deletions(-) create mode 100644 core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyTesterImpl.kt create mode 100644 core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/network/ProxyTester.kt diff --git a/.claude/settings.local.json b/.claude/settings.local.json index aab7f875..469ebe82 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +1,9 @@ { "permissions": { "allow": [ - "Bash(grep -r \"\\\\.FLATPAK\\\\|Flatpak\\\\|flatpak\" D:/development/Github-Store --include=*.kt)" + "Bash(grep -r \"\\\\.FLATPAK\\\\|Flatpak\\\\|flatpak\" D:/development/Github-Store --include=*.kt)", + "Bash(./gradlew :composeApp:assembleDebug)", + "Bash(./gradlew :composeApp:jvmJar)" ] } } diff --git a/composeApp/release/baselineProfiles/0/composeApp-release.dm b/composeApp/release/baselineProfiles/0/composeApp-release.dm index 838e163fbe7717dbd1cad78a359d5b4a201541ca..ce6abdffefe441a143a44cc5fade70ff04230998 100644 GIT binary patch literal 11430 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 a@4sg{d{x54`cL}Vzv1yOlCl2h>Hh&mhW`Bk literal 11283 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 diff --git a/composeApp/release/baselineProfiles/1/composeApp-release.dm b/composeApp/release/baselineProfiles/1/composeApp-release.dm index 6ab7c2f24901afaa34a5288b84c645e3d4768d0d..dd27f85cb8e880015bebbab1bee98a8d016d91ee 100644 GIT binary patch literal 11411 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 Bme~LR diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.android.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.android.kt index 9afc5b53..ab386f6a 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.android.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.android.kt @@ -8,7 +8,6 @@ import java.net.Authenticator import java.net.InetSocketAddress import java.net.PasswordAuthentication import java.net.Proxy -import java.net.ProxySelector actual fun createPlatformHttpClient(proxyConfig: ProxyConfig): HttpClient { Authenticator.setDefault(null) @@ -21,9 +20,11 @@ actual fun createPlatformHttpClient(proxyConfig: ProxyConfig): HttpClient { } is ProxyConfig.System -> { - config { - proxySelector(ProxySelector.getDefault()) - } + // java.net.ProxySelector.getDefault() does not read Android's + // per-network HTTP proxy. Android publishes the active proxy + // through standard system properties instead, which we resolve + // explicitly here so traffic actually flows through it. + proxy = resolveAndroidSystemProxy() } is ProxyConfig.Http -> { @@ -78,3 +79,19 @@ actual fun createPlatformHttpClient(proxyConfig: ProxyConfig): HttpClient { } } } + +internal fun resolveAndroidSystemProxy(): Proxy { + val httpsHost = System.getProperty("https.proxyHost")?.takeIf { it.isNotBlank() } + val httpsPort = System.getProperty("https.proxyPort")?.toIntOrNull() + if (httpsHost != null && httpsPort != null) { + return Proxy(Proxy.Type.HTTP, InetSocketAddress(httpsHost, httpsPort)) + } + + val httpHost = System.getProperty("http.proxyHost")?.takeIf { it.isNotBlank() } + val httpPort = System.getProperty("http.proxyPort")?.toIntOrNull() + if (httpHost != null && httpPort != null) { + return Proxy(Proxy.Type.HTTP, InetSocketAddress(httpHost, httpPort)) + } + + return Proxy.NO_PROXY +} diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidDownloader.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidDownloader.kt index 56455f98..43d90c7d 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidDownloader.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidDownloader.kt @@ -11,6 +11,7 @@ import okhttp3.Credentials import okhttp3.OkHttpClient import okhttp3.Request import zed.rainxch.core.data.network.ProxyManager +import zed.rainxch.core.data.network.resolveAndroidSystemProxy import zed.rainxch.core.domain.model.DownloadProgress import zed.rainxch.core.domain.model.ProxyConfig import zed.rainxch.core.domain.network.Downloader @@ -44,7 +45,12 @@ class AndroidDownloader( proxy(Proxy.NO_PROXY) } - is ProxyConfig.System -> {} + is ProxyConfig.System -> { + // ProxySelector.getDefault() does not honor Android's + // per-network HTTP proxy; resolve it explicitly so + // downloads also flow through the device proxy. + proxy(resolveAndroidSystemProxy()) + } is ProxyConfig.Http -> { proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(config.host, config.port))) diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt index f42f1f10..6283fa61 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt @@ -22,6 +22,7 @@ import zed.rainxch.core.data.local.db.dao.UpdateHistoryDao import zed.rainxch.core.data.logging.KermitLogger import zed.rainxch.core.data.network.GitHubClientProvider import zed.rainxch.core.data.network.ProxyManager +import zed.rainxch.core.data.network.ProxyTesterImpl import zed.rainxch.core.data.network.createGitHubHttpClient import zed.rainxch.core.data.repository.AuthenticationStateImpl import zed.rainxch.core.data.repository.FavouritesRepositoryImpl @@ -36,6 +37,7 @@ import zed.rainxch.core.domain.getPlatform import zed.rainxch.core.domain.logging.GitHubStoreLogger import zed.rainxch.core.domain.model.Platform import zed.rainxch.core.domain.model.ProxyConfig +import zed.rainxch.core.domain.network.ProxyTester import zed.rainxch.core.domain.repository.AuthenticationState import zed.rainxch.core.domain.repository.FavouritesRepository import zed.rainxch.core.domain.repository.InstalledAppsRepository @@ -117,6 +119,10 @@ val coreModule = ) } + single { + ProxyTesterImpl() + } + single { SyncInstalledAppsUseCase( packageMonitor = get(), diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyTesterImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyTesterImpl.kt new file mode 100644 index 00000000..f6bd126b --- /dev/null +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyTesterImpl.kt @@ -0,0 +1,70 @@ +package zed.rainxch.core.data.network + +import io.ktor.client.plugins.HttpRequestTimeoutException +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.request.get +import io.ktor.client.statement.HttpResponse +import io.ktor.util.network.UnresolvedAddressException +import zed.rainxch.core.domain.model.ProxyConfig +import zed.rainxch.core.domain.network.ProxyTestOutcome +import zed.rainxch.core.domain.network.ProxyTester +import java.io.IOException +import java.net.ConnectException +import java.net.SocketTimeoutException +import java.net.UnknownHostException +import kotlin.coroutines.cancellation.CancellationException +import kotlin.time.TimeSource + +class ProxyTesterImpl : ProxyTester { + override suspend fun test(config: ProxyConfig): ProxyTestOutcome { + val client = + createPlatformHttpClient(config).config { + install(HttpTimeout) { + requestTimeoutMillis = TEST_TIMEOUT_MS + connectTimeoutMillis = TEST_TIMEOUT_MS + socketTimeoutMillis = TEST_TIMEOUT_MS + } + expectSuccess = false + } + + return try { + val started = TimeSource.Monotonic.markNow() + val response: HttpResponse = client.get(TEST_URL) + val elapsed = started.elapsedNow().inWholeMilliseconds + + when { + response.status.value == 407 -> + ProxyTestOutcome.Failure.ProxyAuthRequired + + response.status.value in 200..299 -> + ProxyTestOutcome.Success(latencyMs = elapsed) + + else -> + ProxyTestOutcome.Failure.UnexpectedResponse(response.status.value) + } + } catch (e: CancellationException) { + throw e + } catch (e: HttpRequestTimeoutException) { + ProxyTestOutcome.Failure.Timeout + } catch (e: SocketTimeoutException) { + ProxyTestOutcome.Failure.Timeout + } catch (e: UnresolvedAddressException) { + ProxyTestOutcome.Failure.DnsFailure + } catch (e: UnknownHostException) { + ProxyTestOutcome.Failure.DnsFailure + } catch (e: ConnectException) { + ProxyTestOutcome.Failure.ProxyUnreachable + } catch (e: IOException) { + ProxyTestOutcome.Failure.Unknown(e.message) + } catch (e: Throwable) { + ProxyTestOutcome.Failure.Unknown(e.message) + } finally { + client.close() + } + } + + private companion object { + const val TEST_URL = "https://api.github.com/zen" + const val TEST_TIMEOUT_MS = 8_000L + } +} diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/network/ProxyTester.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/network/ProxyTester.kt new file mode 100644 index 00000000..076789b0 --- /dev/null +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/network/ProxyTester.kt @@ -0,0 +1,44 @@ +package zed.rainxch.core.domain.network + +import zed.rainxch.core.domain.model.ProxyConfig + +/** + * Verifies that a [ProxyConfig] can actually reach the GitHub API. Implementations + * should issue a single lightweight request through a throwaway HTTP client built + * with the supplied config so the test exercises the same engine code path the + * real client uses. + */ +interface ProxyTester { + suspend fun test(config: ProxyConfig): ProxyTestOutcome +} + +sealed interface ProxyTestOutcome { + /** Connection succeeded. [latencyMs] is the round-trip time of the test request. */ + data class Success( + val latencyMs: Long, + ) : ProxyTestOutcome + + sealed interface Failure : ProxyTestOutcome { + /** Could not resolve a hostname (DNS failure or unresolved proxy host). */ + data object DnsFailure : Failure + + /** Reached the network but could not connect to the proxy itself. */ + data object ProxyUnreachable : Failure + + /** Connection or socket timed out. */ + data object Timeout : Failure + + /** Proxy returned 407 / requested authentication. */ + data object ProxyAuthRequired : Failure + + /** Proxy or upstream returned a non-2xx HTTP status. */ + data class UnexpectedResponse( + val statusCode: Int, + ) : Failure + + /** Anything else (TLS errors, malformed config, etc.). */ + data class Unknown( + val message: String?, + ) : Failure + } +} diff --git a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml index 5f46a32c..843a8446 100644 --- a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml +++ b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml @@ -149,6 +149,15 @@ منفذ الوكيل غير صالح إظهار كلمة المرور إخفاء كلمة المرور + اختبار + جارٍ الاختبار… + الاتصال ناجح (%1$d مللي ثانية) + تعذر تحليل المضيف. تحقق من عنوان الوكيل. + تعذر الوصول إلى خادم الوكيل. + انتهت مهلة الاتصال. + يلزم التحقق من الوكيل. + استجابة غير متوقعة: HTTP %1$d + فشل اختبار الاتصال تم تسجيل الخروج بنجاح، جارٍ إعادة التوجيه... diff --git a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml index 83f26e80..9ecb5171 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -410,6 +410,15 @@ অবৈধ প্রক্সি পোর্ট পাসওয়ার্ড দেখান পাসওয়ার্ড লুকান + পরীক্ষা + পরীক্ষা চলছে… + সংযোগ ঠিক আছে (%1$d ms) + হোস্ট সমাধান করা যায়নি। প্রক্সি ঠিকানা যাচাই করুন। + প্রক্সি সার্ভারে পৌঁছানো যায়নি। + সংযোগের সময় শেষ হয়ে গেছে। + প্রক্সি প্রমাণীকরণ প্রয়োজন। + অপ্রত্যাশিত প্রতিক্রিয়া: HTTP %1$d + সংযোগ পরীক্ষা ব্যর্থ diff --git a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml index f7ec7bb3..34cf5e9b 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -375,6 +375,15 @@ Puerto de proxy no válido Mostrar contraseña Ocultar contraseña + Probar + Probando… + Conexión correcta (%1$d ms) + No se pudo resolver el host. Verifica la dirección del proxy. + No se pudo conectar con el servidor proxy. + Se agotó el tiempo de conexión. + Se requiere autenticación del proxy. + Respuesta inesperada: HTTP %1$d + La prueba de conexión falló diff --git a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml index 1ba038f7..3af132c1 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -375,6 +375,15 @@ Port proxy invalide Afficher le mot de passe Masquer le mot de passe + Tester + Test en cours… + Connexion OK (%1$d ms) + Impossible de résoudre l\'hôte. Vérifiez l\'adresse du proxy. + Impossible de joindre le serveur proxy. + Délai de connexion dépassé. + Authentification proxy requise. + Réponse inattendue : HTTP %1$d + Échec du test de connexion diff --git a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml index ffdd1973..23ce87ce 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -409,6 +409,15 @@ अमान्य प्रॉक्सी पोर्ट पासवर्ड दिखाएँ पासवर्ड छुपाएँ + परीक्षण + परीक्षण हो रहा है… + कनेक्शन ठीक है (%1$d ms) + होस्ट हल नहीं हो सका। प्रॉक्सी पता जाँचें। + प्रॉक्सी सर्वर तक नहीं पहुँचा जा सका। + कनेक्शन समय समाप्त हो गया। + प्रॉक्सी प्रमाणीकरण आवश्यक है। + अप्रत्याशित प्रतिक्रिया: HTTP %1$d + कनेक्शन परीक्षण विफल diff --git a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml index 7a99a8ab..fc49c3a1 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -411,6 +411,15 @@ Porta proxy non valida Mostra password Nascondi password + Verifica + Verifica in corso… + Connessione OK (%1$d ms) + Impossibile risolvere l\'host. Controlla l\'indirizzo del proxy. + Impossibile raggiungere il server proxy. + Timeout della connessione. + Autenticazione proxy richiesta. + Risposta inattesa: HTTP %1$d + Verifica connessione non riuscita diff --git a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml index 9f96e4ba..d7615153 100644 --- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -375,6 +375,15 @@ 無効なプロキシポート パスワードを表示 パスワードを非表示 + テスト + テスト中… + 接続OK (%1$d ms) + ホストを解決できません。プロキシアドレスを確認してください。 + プロキシサーバーに接続できません。 + 接続がタイムアウトしました。 + プロキシ認証が必要です。 + 予期しない応答:HTTP %1$d + 接続テストに失敗しました diff --git a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml index 039cab47..2d99333c 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -408,6 +408,15 @@ 잘못된 프록시 포트 비밀번호 표시 비밀번호 숨기기 + 테스트 + 테스트 중… + 연결 성공 (%1$d ms) + 호스트를 확인할 수 없습니다. 프록시 주소를 확인하세요. + 프록시 서버에 연결할 수 없습니다. + 연결 시간이 초과되었습니다. + 프록시 인증이 필요합니다. + 예기치 않은 응답: HTTP %1$d + 연결 테스트 실패 diff --git a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml index 5c76bd83..756f3572 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -373,6 +373,15 @@ Nieprawidłowy port proxy Pokaż hasło Ukryj hasło + Testuj + Testowanie… + Połączenie OK (%1$d ms) + Nie można rozwiązać hosta. Sprawdź adres proxy. + Nie można połączyć się z serwerem proxy. + Upłynął limit czasu połączenia. + Wymagane uwierzytelnienie proxy. + Nieoczekiwana odpowiedź: HTTP %1$d + Test połączenia nie powiódł się diff --git a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml index 6eeb2e75..33683cd1 100644 --- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -375,6 +375,15 @@ Недопустимый порт прокси Показать пароль Скрыть пароль + Проверить + Проверка… + Соединение в порядке (%1$d мс) + Не удалось разрешить хост. Проверьте адрес прокси. + Не удалось подключиться к прокси-серверу. + Время ожидания истекло. + Требуется аутентификация прокси. + Неожиданный ответ: HTTP %1$d + Не удалось проверить соединение diff --git a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml index a8301596..f43b397d 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -407,6 +407,15 @@ Geçersiz proxy portu Şifreyi göster Şifreyi gizle + Test + Test ediliyor… + Bağlantı tamam (%1$d ms) + Sunucu adresi çözümlenemedi. Proxy adresini kontrol edin. + Proxy sunucusuna ulaşılamadı. + Bağlantı zaman aşımına uğradı. + Proxy kimlik doğrulaması gerekiyor. + Beklenmeyen yanıt: HTTP %1$d + Bağlantı testi başarısız diff --git a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index 9c9c085e..2c008681 100644 --- a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -376,6 +376,15 @@ 无效的代理端口 显示密码 隐藏密码 + 测试 + 测试中… + 连接正常 (%1$d ms) + 无法解析主机。请检查代理地址。 + 无法连接到代理服务器。 + 连接超时。 + 需要代理身份验证。 + 意外响应:HTTP %1$d + 连接测试失败 diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index fed21943..9ad81f84 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -152,6 +152,15 @@ Invalid proxy port Show password Hide password + Test + Testing… + Connection OK (%1$d ms) + Could not resolve host. Check the proxy address. + Could not reach the proxy server. + Connection timed out. + Proxy authentication required. + Unexpected response: HTTP %1$d + Connection test failed Logged out successfully, redirecting... diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt index 4d5e7c76..41511304 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt @@ -56,6 +56,8 @@ sealed interface TweaksAction { data object OnProxySave : TweaksAction + data object OnProxyTest : TweaksAction + data class OnInstallerTypeSelected( val type: InstallerType, ) : TweaksAction diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksEvent.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksEvent.kt index f711b5d4..73ae34ca 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksEvent.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksEvent.kt @@ -7,6 +7,14 @@ sealed interface TweaksEvent { val message: String, ) : TweaksEvent + data class OnProxyTestSuccess( + val latencyMs: Long, + ) : TweaksEvent + + data class OnProxyTestError( + val message: String, + ) : TweaksEvent + data object OnCacheCleared : TweaksEvent data class OnCacheClearError( diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt index 4ec18e88..d89d6594 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt @@ -36,6 +36,7 @@ import zed.rainxch.core.presentation.utils.ObserveAsEvents import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.downloads_cleared import zed.rainxch.githubstore.core.presentation.res.proxy_saved +import zed.rainxch.githubstore.core.presentation.res.proxy_test_success import zed.rainxch.githubstore.core.presentation.res.seen_history_cleared import zed.rainxch.githubstore.core.presentation.res.tweaks_title import zed.rainxch.tweaks.presentation.components.ClearDownloadsDialog @@ -77,6 +78,20 @@ fun TweaksRoot(viewModel: TweaksViewModel = koinViewModel()) { } } + is TweaksEvent.OnProxyTestSuccess -> { + coroutineScope.launch { + snackbarState.showSnackbar( + getString(Res.string.proxy_test_success, event.latencyMs), + ) + } + } + + is TweaksEvent.OnProxyTestError -> { + coroutineScope.launch { + snackbarState.showSnackbar(event.message) + } + } + TweaksEvent.OnCacheCleared -> { coroutineScope.launch { snackbarState.showSnackbar(getString(Res.string.downloads_cleared)) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt index 2eb1ab00..a2d78052 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt @@ -18,6 +18,7 @@ data class TweaksState( val proxyUsername: String = "", val proxyPassword: String = "", val isProxyPasswordVisible: Boolean = false, + val isProxyTestInProgress: Boolean = false, val autoDetectClipboardLinks: Boolean = true, val cacheSize: String = "", val isClearDownloadsDialogVisible: Boolean = false, diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt index de26ebf7..d2ec83d1 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt @@ -13,6 +13,8 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString import zed.rainxch.core.domain.model.ProxyConfig +import zed.rainxch.core.domain.network.ProxyTestOutcome +import zed.rainxch.core.domain.network.ProxyTester import zed.rainxch.core.domain.repository.ProxyRepository import zed.rainxch.core.domain.repository.SeenReposRepository import zed.rainxch.core.domain.repository.TweaksRepository @@ -23,6 +25,12 @@ import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.failed_to_save_proxy_settings import zed.rainxch.githubstore.core.presentation.res.invalid_proxy_port import zed.rainxch.githubstore.core.presentation.res.proxy_host_required +import zed.rainxch.githubstore.core.presentation.res.proxy_test_error_auth_required +import zed.rainxch.githubstore.core.presentation.res.proxy_test_error_dns +import zed.rainxch.githubstore.core.presentation.res.proxy_test_error_status +import zed.rainxch.githubstore.core.presentation.res.proxy_test_error_timeout +import zed.rainxch.githubstore.core.presentation.res.proxy_test_error_unknown +import zed.rainxch.githubstore.core.presentation.res.proxy_test_error_unreachable import zed.rainxch.profile.domain.repository.ProfileRepository import zed.rainxch.tweaks.presentation.model.ProxyType @@ -32,6 +40,7 @@ class TweaksViewModel( private val profileRepository: ProfileRepository, private val installerStatusProvider: InstallerStatusProvider, private val proxyRepository: ProxyRepository, + private val proxyTester: ProxyTester, private val updateScheduleManager: UpdateScheduleManager, private val seenReposRepository: SeenReposRepository, ) : ViewModel() { @@ -396,6 +405,19 @@ class TweaksViewModel( } } + TweaksAction.OnProxyTest -> { + if (_state.value.isProxyTestInProgress) return + val config = buildProxyConfigForTest() ?: return + _state.update { it.copy(isProxyTestInProgress = true) } + viewModelScope.launch { + val outcome = + runCatching { proxyTester.test(config) } + .getOrElse { ProxyTestOutcome.Failure.Unknown(it.message) } + _state.update { it.copy(isProxyTestInProgress = false) } + _events.send(outcome.toEvent()) + } + } + is TweaksAction.OnInstallerTypeSelected -> { viewModelScope.launch { tweaksRepository.setInstallerType(action.type) @@ -483,4 +505,81 @@ class TweaksViewModel( } } } + + /** + * Builds the [ProxyConfig] to test from the current form state. For + * [ProxyType.HTTP] / [ProxyType.SOCKS] this requires a valid host and port — + * if either is missing the user is told via an error event and `null` is + * returned, mirroring the validation in [TweaksAction.OnProxySave]. + */ + private fun buildProxyConfigForTest(): ProxyConfig? { + val current = _state.value + return when (current.proxyType) { + ProxyType.NONE -> ProxyConfig.None + ProxyType.SYSTEM -> ProxyConfig.System + ProxyType.HTTP, ProxyType.SOCKS -> { + val port = + current.proxyPort + .toIntOrNull() + ?.takeIf { it in 1..65535 } + ?: run { + viewModelScope.launch { + _events.send( + TweaksEvent.OnProxyTestError( + getString(Res.string.invalid_proxy_port), + ), + ) + } + return null + } + val host = + current.proxyHost.trim().takeIf { it.isNotBlank() } + ?: run { + viewModelScope.launch { + _events.send( + TweaksEvent.OnProxyTestError( + getString(Res.string.proxy_host_required), + ), + ) + } + return null + } + val username = current.proxyUsername.takeIf { it.isNotBlank() } + val password = current.proxyPassword.takeIf { it.isNotBlank() } + if (current.proxyType == ProxyType.HTTP) { + ProxyConfig.Http(host, port, username, password) + } else { + ProxyConfig.Socks(host, port, username, password) + } + } + } + } + + private suspend fun ProxyTestOutcome.toEvent(): TweaksEvent = + when (this) { + is ProxyTestOutcome.Success -> + TweaksEvent.OnProxyTestSuccess(latencyMs = latencyMs) + + ProxyTestOutcome.Failure.DnsFailure -> + TweaksEvent.OnProxyTestError(getString(Res.string.proxy_test_error_dns)) + + ProxyTestOutcome.Failure.ProxyUnreachable -> + TweaksEvent.OnProxyTestError(getString(Res.string.proxy_test_error_unreachable)) + + ProxyTestOutcome.Failure.Timeout -> + TweaksEvent.OnProxyTestError(getString(Res.string.proxy_test_error_timeout)) + + ProxyTestOutcome.Failure.ProxyAuthRequired -> + TweaksEvent.OnProxyTestError(getString(Res.string.proxy_test_error_auth_required)) + + is ProxyTestOutcome.Failure.UnexpectedResponse -> + TweaksEvent.OnProxyTestError( + getString(Res.string.proxy_test_error_status, statusCode), + ) + + is ProxyTestOutcome.Failure.Unknown -> + TweaksEvent.OnProxyTestError( + message ?: getString(Res.string.proxy_test_error_unknown), + ) + } } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Network.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Network.kt index 56959619..c77b3004 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Network.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Network.kt @@ -19,10 +19,12 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.NetworkCheck import androidx.compose.material.icons.filled.Save import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FilledTonalButton @@ -30,6 +32,7 @@ import androidx.compose.material3.FilterChip import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -72,16 +75,27 @@ fun LazyListScope.networkSection( enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut(), ) { - Text( - text = - when (state.proxyType) { - ProxyType.SYSTEM -> stringResource(Res.string.proxy_system_description) - else -> stringResource(Res.string.proxy_none_description) - }, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(start = 8.dp, top = 12.dp), - ) + Column { + Text( + text = + when (state.proxyType) { + ProxyType.SYSTEM -> stringResource(Res.string.proxy_system_description) + else -> stringResource(Res.string.proxy_none_description) + }, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 8.dp, top = 12.dp), + ) + + Spacer(Modifier.height(12.dp)) + + ProxyTestButton( + isInProgress = state.isProxyTestInProgress, + enabled = !state.isProxyTestInProgress, + onClick = { onAction(TweaksAction.OnProxyTest) }, + modifier = Modifier.padding(start = 8.dp), + ) + } } AnimatedVisibility( @@ -266,20 +280,64 @@ private fun ProxyDetailsCard( shape = RoundedCornerShape(12.dp), ) - // Save button - FilledTonalButton( - onClick = { onAction(TweaksAction.OnProxySave) }, - enabled = isFormValid, + // Test + Save buttons + Row( modifier = Modifier.align(Alignment.End), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, ) { - Icon( - imageVector = Icons.Default.Save, - contentDescription = null, - modifier = Modifier.size(18.dp), + ProxyTestButton( + isInProgress = state.isProxyTestInProgress, + enabled = isFormValid && !state.isProxyTestInProgress, + onClick = { onAction(TweaksAction.OnProxyTest) }, ) - Spacer(Modifier.size(8.dp)) - Text(stringResource(Res.string.proxy_save)) + + FilledTonalButton( + onClick = { onAction(TweaksAction.OnProxySave) }, + enabled = isFormValid && !state.isProxyTestInProgress, + ) { + Icon( + imageVector = Icons.Default.Save, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.size(8.dp)) + Text(stringResource(Res.string.proxy_save)) + } } } } } + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +private fun ProxyTestButton( + isInProgress: Boolean, + enabled: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + OutlinedButton( + onClick = onClick, + enabled = enabled, + modifier = modifier, + ) { + if (isInProgress) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.primary, + ) + Spacer(Modifier.size(8.dp)) + Text(stringResource(Res.string.proxy_test_in_progress)) + } else { + Icon( + imageVector = Icons.Default.NetworkCheck, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.size(8.dp)) + Text(stringResource(Res.string.proxy_test)) + } + } +} From fc8bd1368adeeba5b01e2a79249ed66abc8d3537 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Fri, 10 Apr 2026 22:37:00 +0500 Subject: [PATCH 2/2] tweaks: improve error handling and safety in proxy testing and configuration - Refactor `TweaksViewModel` to explicitly handle `CancellationException` during proxy tests to preserve structured concurrency. - Update `TweaksViewModel` to use localized fallback strings for unknown proxy test errors instead of leaking raw exception messages. - Strengthen proxy port validation in `resolveAndroidSystemProxy` to ensure values are within the valid 1-65535 range before creating `InetSocketAddress`. - Narrow exception catching in `ProxyTesterImpl` from `Throwable` to `Exception`. - Update `.claude/settings.local.json` to use relative paths in grep commands and add a combined Gradle build task. --- .claude/settings.local.json | 5 ++-- .../data/network/HttpClientFactory.android.kt | 7 ++++-- .../core/data/network/ProxyTesterImpl.kt | 2 +- .../tweaks/presentation/TweaksViewModel.kt | 24 +++++++++++++------ 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 469ebe82..81c0379b 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,9 +1,10 @@ { "permissions": { "allow": [ - "Bash(grep -r \"\\\\.FLATPAK\\\\|Flatpak\\\\|flatpak\" D:/development/Github-Store --include=*.kt)", + "Bash(grep -r \"\\\\.FLATPAK\\\\|Flatpak\\\\|flatpak\" . --include=*.kt)", "Bash(./gradlew :composeApp:assembleDebug)", - "Bash(./gradlew :composeApp:jvmJar)" + "Bash(./gradlew :composeApp:jvmJar)", + "Bash(./gradlew :composeApp:assembleDebug :composeApp:jvmJar)" ] } } diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.android.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.android.kt index ab386f6a..89c9e577 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.android.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.android.kt @@ -81,14 +81,17 @@ actual fun createPlatformHttpClient(proxyConfig: ProxyConfig): HttpClient { } internal fun resolveAndroidSystemProxy(): Proxy { + // System properties are user/OS-supplied, so guard against malformed + // values: InetSocketAddress(String, Int) throws IllegalArgumentException + // for ports outside 0..65535. val httpsHost = System.getProperty("https.proxyHost")?.takeIf { it.isNotBlank() } - val httpsPort = System.getProperty("https.proxyPort")?.toIntOrNull() + val httpsPort = System.getProperty("https.proxyPort")?.toIntOrNull()?.takeIf { it in 1..65535 } if (httpsHost != null && httpsPort != null) { return Proxy(Proxy.Type.HTTP, InetSocketAddress(httpsHost, httpsPort)) } val httpHost = System.getProperty("http.proxyHost")?.takeIf { it.isNotBlank() } - val httpPort = System.getProperty("http.proxyPort")?.toIntOrNull() + val httpPort = System.getProperty("http.proxyPort")?.toIntOrNull()?.takeIf { it in 1..65535 } if (httpHost != null && httpPort != null) { return Proxy(Proxy.Type.HTTP, InetSocketAddress(httpHost, httpPort)) } diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyTesterImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyTesterImpl.kt index f6bd126b..a3b8a115 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyTesterImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyTesterImpl.kt @@ -56,7 +56,7 @@ class ProxyTesterImpl : ProxyTester { ProxyTestOutcome.Failure.ProxyUnreachable } catch (e: IOException) { ProxyTestOutcome.Failure.Unknown(e.message) - } catch (e: Throwable) { + } catch (e: Exception) { ProxyTestOutcome.Failure.Unknown(e.message) } finally { client.close() diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt index d2ec83d1..7b7153d0 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt @@ -2,6 +2,7 @@ package zed.rainxch.tweaks.presentation import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -410,10 +411,17 @@ class TweaksViewModel( val config = buildProxyConfigForTest() ?: return _state.update { it.copy(isProxyTestInProgress = true) } viewModelScope.launch { - val outcome = - runCatching { proxyTester.test(config) } - .getOrElse { ProxyTestOutcome.Failure.Unknown(it.message) } - _state.update { it.copy(isProxyTestInProgress = false) } + val outcome: ProxyTestOutcome = + try { + proxyTester.test(config) + } catch (e: CancellationException) { + // Preserve structured concurrency — never swallow. + throw e + } catch (e: Exception) { + ProxyTestOutcome.Failure.Unknown(e.message) + } finally { + _state.update { it.copy(isProxyTestInProgress = false) } + } _events.send(outcome.toEvent()) } } @@ -578,8 +586,10 @@ class TweaksViewModel( ) is ProxyTestOutcome.Failure.Unknown -> - TweaksEvent.OnProxyTestError( - message ?: getString(Res.string.proxy_test_error_unknown), - ) + // Raw exception messages are platform-specific, untranslated, + // and may leak internal detail — always show the localized + // fallback to the user. The original `message` is intentionally + // dropped here; surface it via logging if diagnostics are needed. + TweaksEvent.OnProxyTestError(getString(Res.string.proxy_test_error_unknown)) } }