diff --git a/android/app/src/main/java/org/flowseal/tgwsproxy/MainActivity.kt b/android/app/src/main/java/org/flowseal/tgwsproxy/MainActivity.kt
index 727be4f..e5429b7 100644
--- a/android/app/src/main/java/org/flowseal/tgwsproxy/MainActivity.kt
+++ b/android/app/src/main/java/org/flowseal/tgwsproxy/MainActivity.kt
@@ -3,6 +3,7 @@ package org.flowseal.tgwsproxy
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
+import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.widget.Toast
@@ -14,13 +15,16 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.snackbar.Snackbar
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import org.flowseal.tgwsproxy.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var settingsStore: ProxySettingsStore
+ private var currentUpdateStatus: ProxyUpdateStatus? = null
private val notificationPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission(),
@@ -46,6 +50,10 @@ class MainActivity : AppCompatActivity() {
binding.saveButton.setOnClickListener { onSaveClicked(showMessage = true) }
binding.openLogsButton.setOnClickListener { onOpenLogsClicked() }
binding.openTelegramButton.setOnClickListener { onOpenTelegramClicked() }
+ binding.openReleasePageButton.setOnClickListener { onOpenReleasePageClicked() }
+ binding.checkUpdatesSwitch.setOnCheckedChangeListener { _, _ ->
+ renderUpdateStatus(currentUpdateStatus, binding.checkUpdatesSwitch.isChecked)
+ }
binding.disableBatteryOptimizationButton.setOnClickListener {
AndroidSystemStatus.openBatteryOptimizationSettings(this)
}
@@ -53,7 +61,9 @@ class MainActivity : AppCompatActivity() {
AndroidSystemStatus.openAppSettings(this)
}
- renderConfig(settingsStore.load())
+ val config = settingsStore.load()
+ renderConfig(config)
+ refreshUpdateStatus(checkNow = config.checkUpdates)
requestNotificationPermissionIfNeeded()
observeServiceState()
renderSystemStatus()
@@ -78,6 +88,7 @@ class MainActivity : AppCompatActivity() {
if (showMessage) {
Snackbar.make(binding.root, R.string.settings_saved, Snackbar.LENGTH_SHORT).show()
}
+ refreshUpdateStatus(checkNow = config.checkUpdates)
return config
}
@@ -111,7 +122,9 @@ class MainActivity : AppCompatActivity() {
binding.logMaxMbInput.setText(config.logMaxMbText)
binding.bufferKbInput.setText(config.bufferKbText)
binding.poolSizeInput.setText(config.poolSizeText)
+ binding.checkUpdatesSwitch.isChecked = config.checkUpdates
binding.verboseSwitch.isChecked = config.verbose
+ renderUpdateStatus(currentUpdateStatus, config.checkUpdates)
}
private fun collectConfigFromForm(): ProxyConfig {
@@ -122,10 +135,63 @@ class MainActivity : AppCompatActivity() {
logMaxMbText = binding.logMaxMbInput.text?.toString().orEmpty(),
bufferKbText = binding.bufferKbInput.text?.toString().orEmpty(),
poolSizeText = binding.poolSizeInput.text?.toString().orEmpty(),
+ checkUpdates = binding.checkUpdatesSwitch.isChecked,
verbose = binding.verboseSwitch.isChecked,
)
}
+ private fun onOpenReleasePageClicked() {
+ val url = currentUpdateStatus?.htmlUrl ?: "https://github.com/Dark-Avery/tg-ws-proxy/releases/latest"
+ val opened = runCatching {
+ startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
+ }.isSuccess
+ if (!opened) {
+ Snackbar.make(binding.root, R.string.release_page_open_failed, Snackbar.LENGTH_LONG).show()
+ }
+ }
+
+ private fun refreshUpdateStatus(checkNow: Boolean) {
+ lifecycleScope.launch {
+ val status = withContext(Dispatchers.IO) {
+ PythonProxyBridge.getUpdateStatus(this@MainActivity, checkNow)
+ }
+ currentUpdateStatus = status
+ renderUpdateStatus(status, binding.checkUpdatesSwitch.isChecked)
+ }
+ }
+
+ private fun renderUpdateStatus(status: ProxyUpdateStatus?, checkUpdatesEnabled: Boolean) {
+ val currentVersion = status?.currentVersion ?: "unknown"
+ binding.currentVersionValue.text = getString(
+ R.string.updates_current_version_format,
+ currentVersion,
+ )
+ binding.updateStatusValue.text = when {
+ !checkUpdatesEnabled -> {
+ getString(R.string.updates_status_disabled)
+ }
+ status == null -> {
+ getString(R.string.updates_status_initial)
+ }
+ !status.error.isNullOrBlank() -> {
+ getString(R.string.updates_status_error, status.error)
+ }
+ status.hasUpdate && !status.latestVersion.isNullOrBlank() -> {
+ getString(
+ R.string.updates_status_available,
+ status.latestVersion,
+ status.currentVersion,
+ )
+ }
+ status.aheadOfRelease -> {
+ getString(R.string.updates_status_newer, status.currentVersion)
+ }
+ else -> {
+ getString(R.string.updates_status_latest, status.currentVersion)
+ }
+ }
+ }
+
private fun observeServiceState() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
diff --git a/android/app/src/main/java/org/flowseal/tgwsproxy/ProxyConfig.kt b/android/app/src/main/java/org/flowseal/tgwsproxy/ProxyConfig.kt
index 39cf065..c2bb797 100644
--- a/android/app/src/main/java/org/flowseal/tgwsproxy/ProxyConfig.kt
+++ b/android/app/src/main/java/org/flowseal/tgwsproxy/ProxyConfig.kt
@@ -7,6 +7,7 @@ data class ProxyConfig(
val logMaxMbText: String = formatDecimal(DEFAULT_LOG_MAX_MB),
val bufferKbText: String = DEFAULT_BUFFER_KB.toString(),
val poolSizeText: String = DEFAULT_POOL_SIZE.toString(),
+ val checkUpdates: Boolean = true,
val verbose: Boolean = false,
) {
fun validate(): ValidationResult {
@@ -78,6 +79,7 @@ data class ProxyConfig(
logMaxMb = logMaxMbValue,
bufferKb = bufferKbValue,
poolSize = poolSizeValue,
+ checkUpdates = checkUpdates,
verbose = verbose,
)
)
@@ -130,5 +132,6 @@ data class NormalizedProxyConfig(
val logMaxMb: Double,
val bufferKb: Int,
val poolSize: Int,
+ val checkUpdates: Boolean,
val verbose: Boolean,
)
diff --git a/android/app/src/main/java/org/flowseal/tgwsproxy/ProxySettingsStore.kt b/android/app/src/main/java/org/flowseal/tgwsproxy/ProxySettingsStore.kt
index 9a08cc9..3414e66 100644
--- a/android/app/src/main/java/org/flowseal/tgwsproxy/ProxySettingsStore.kt
+++ b/android/app/src/main/java/org/flowseal/tgwsproxy/ProxySettingsStore.kt
@@ -27,6 +27,7 @@ class ProxySettingsStore(context: Context) {
KEY_POOL_SIZE,
ProxyConfig.DEFAULT_POOL_SIZE,
).toString(),
+ checkUpdates = preferences.getBoolean(KEY_CHECK_UPDATES, true),
verbose = preferences.getBoolean(KEY_VERBOSE, false),
)
}
@@ -39,6 +40,7 @@ class ProxySettingsStore(context: Context) {
.putFloat(KEY_LOG_MAX_MB, config.logMaxMb.toFloat())
.putInt(KEY_BUFFER_KB, config.bufferKb)
.putInt(KEY_POOL_SIZE, config.poolSize)
+ .putBoolean(KEY_CHECK_UPDATES, config.checkUpdates)
.putBoolean(KEY_VERBOSE, config.verbose)
.apply()
}
@@ -51,6 +53,7 @@ class ProxySettingsStore(context: Context) {
private const val KEY_LOG_MAX_MB = "log_max_mb"
private const val KEY_BUFFER_KB = "buf_kb"
private const val KEY_POOL_SIZE = "pool_size"
+ private const val KEY_CHECK_UPDATES = "check_updates"
private const val KEY_VERBOSE = "verbose"
}
}
diff --git a/android/app/src/main/java/org/flowseal/tgwsproxy/PythonProxyBridge.kt b/android/app/src/main/java/org/flowseal/tgwsproxy/PythonProxyBridge.kt
index e6e4ce9..1d38614 100644
--- a/android/app/src/main/java/org/flowseal/tgwsproxy/PythonProxyBridge.kt
+++ b/android/app/src/main/java/org/flowseal/tgwsproxy/PythonProxyBridge.kt
@@ -46,6 +46,19 @@ object PythonProxyBridge {
)
}
+ fun getUpdateStatus(context: Context, checkNow: Boolean = false): ProxyUpdateStatus {
+ val payload = getModule(context).callAttr("get_update_status_json", checkNow).toString()
+ val json = JSONObject(payload)
+ return ProxyUpdateStatus(
+ currentVersion = json.optString("current_version").ifBlank { "unknown" },
+ latestVersion = json.optString("latest").ifBlank { null },
+ hasUpdate = json.optBoolean("has_update", false),
+ aheadOfRelease = json.optBoolean("ahead_of_release", false),
+ htmlUrl = json.optString("html_url").ifBlank { null },
+ error = json.optString("error").ifBlank { null },
+ )
+ }
+
private fun getModule(context: Context) =
getPython(context.applicationContext).getModule(MODULE_NAME)
@@ -63,3 +76,12 @@ data class ProxyTrafficStats(
val running: Boolean = false,
val lastError: String? = null,
)
+
+data class ProxyUpdateStatus(
+ val currentVersion: String = "unknown",
+ val latestVersion: String? = null,
+ val hasUpdate: Boolean = false,
+ val aheadOfRelease: Boolean = false,
+ val htmlUrl: String? = null,
+ val error: String? = null,
+)
diff --git a/android/app/src/main/python/android_proxy_bridge.py b/android/app/src/main/python/android_proxy_bridge.py
index b83201e..b99218b 100644
--- a/android/app/src/main/python/android_proxy_bridge.py
+++ b/android/app/src/main/python/android_proxy_bridge.py
@@ -6,7 +6,9 @@ from pathlib import Path
from typing import Iterable, Optional
from proxy.app_runtime import ProxyAppRuntime
+from proxy import __version__
import proxy.tg_ws_proxy as tg_ws_proxy
+from utils.update_check import RELEASES_PAGE_URL, get_status, run_check
_RUNTIME_LOCK = threading.RLock()
@@ -125,3 +127,23 @@ def get_runtime_stats_json() -> str:
payload["running"] = running
payload["last_error"] = _LAST_ERROR
return json.dumps(payload)
+
+
+def get_update_status_json(check_now: bool = False) -> str:
+ payload = {
+ "current_version": __version__,
+ "latest": "",
+ "has_update": False,
+ "ahead_of_release": False,
+ "html_url": RELEASES_PAGE_URL,
+ "error": "",
+ }
+ try:
+ if check_now:
+ run_check(__version__)
+ payload.update(get_status())
+ payload["current_version"] = __version__
+ payload["html_url"] = payload.get("html_url") or RELEASES_PAGE_URL
+ except Exception as exc:
+ payload["error"] = str(exc)
+ return json.dumps(payload)
diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml
index db5aed8..ec2c29f 100644
--- a/android/app/src/main/res/layout/activity_main.xml
+++ b/android/app/src/main/res/layout/activity_main.xml
@@ -95,6 +95,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+ app:cardCornerRadius="20dp">
-
-
+ android:orientation="vertical"
+ android:padding="18dp">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ app:cardCornerRadius="20dp">
-
-
+ android:orientation="vertical"
+ android:padding="18dp">
-
+
-
-
+
-
+
-
+
+
-
-
+
-
+
+
-
-
+
-
-
-
-
+
+
+
+
-
-
-
+ app:cardCornerRadius="20dp">
-
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 4431061..4436f31 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -17,6 +17,20 @@
Background restriction: not detected.
Background restriction: enabled, Android may block long-running work.
Some phones also require manual vendor settings such as Autostart, Lock in recents, or Unrestricted battery mode.
+ Proxy endpoint
+ Routing
+ Advanced
+ Actions
+ Updates
+ v%1$s
+ Check for updates on launch
+ Status will appear after background check.
+ Automatic update checks are disabled.
+ Installed version %1$s matches the latest GitHub release.
+ Installed version %1$s is newer than the latest GitHub release.
+ Version %1$s is available on GitHub (installed: %2$s).
+ Update check failed: %1$s
+ Open Release Page
Proxy host
Proxy port
DC to IP mappings (one DC:IP per line)
@@ -33,6 +47,7 @@
Disable Battery Optimization
Open App Settings
Last service error
+ Failed to open release page.
Settings saved
Foreground service start requested
Foreground service restart requested
diff --git a/tests/test_android_proxy_bridge.py b/tests/test_android_proxy_bridge.py
index 75cdb2b..5ccacec 100644
--- a/tests/test_android_proxy_bridge.py
+++ b/tests/test_android_proxy_bridge.py
@@ -124,6 +124,38 @@ class AndroidProxyBridgeTests(unittest.TestCase):
self.assertEqual(captured["log_max_mb"], 7.0)
self.assertTrue(captured["verbose"])
+ def test_get_update_status_json_merges_python_update_state(self):
+ original_run_check = android_proxy_bridge.run_check
+ original_get_status = android_proxy_bridge.get_status
+ try:
+ captured = {}
+
+ def fake_run_check(version):
+ captured["run_check_version"] = version
+
+ def fake_get_status():
+ return {
+ "latest": "1.3.1",
+ "has_update": True,
+ "ahead_of_release": False,
+ "html_url": "https://example.com/release",
+ "error": "",
+ }
+
+ android_proxy_bridge.run_check = fake_run_check
+ android_proxy_bridge.get_status = fake_get_status
+
+ result = json.loads(android_proxy_bridge.get_update_status_json(True))
+ finally:
+ android_proxy_bridge.run_check = original_run_check
+ android_proxy_bridge.get_status = original_get_status
+
+ self.assertEqual(captured["run_check_version"], android_proxy_bridge.__version__)
+ self.assertEqual(result["current_version"], android_proxy_bridge.__version__)
+ self.assertEqual(result["latest"], "1.3.1")
+ self.assertTrue(result["has_update"])
+ self.assertEqual(result["html_url"], "https://example.com/release")
+
if __name__ == "__main__":
unittest.main()
diff --git a/utils/update_check.py b/utils/update_check.py
index 026dd41..86653fe 100644
--- a/utils/update_check.py
+++ b/utils/update_check.py
@@ -16,7 +16,7 @@ from typing import Any, Dict, Optional, Tuple
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
-REPO = "Flowseal/tg-ws-proxy"
+REPO = "Dark-Avery/tg-ws-proxy"
RELEASES_LATEST_API = f"https://api.github.com/repos/{REPO}/releases/latest"
RELEASES_PAGE_URL = f"https://github.com/{REPO}/releases/latest"