mirror of
https://github.com/Flowseal/tg-ws-proxy.git
synced 2026-05-25 08:51:43 +03:00
feat(android): harden update checks, ci, and direct-only ui
This commit is contained in:
@@ -8,6 +8,14 @@ plugins {
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
fun loadProxyVersionName(): String {
|
||||
val versionFile = rootProject.projectDir.resolve("../proxy/__init__.py")
|
||||
val match = Regex("""__version__\s*=\s*"([^"]+)"""")
|
||||
.find(versionFile.readText())
|
||||
?: throw GradleException("Failed to parse proxy version from ${versionFile.absolutePath}")
|
||||
return match.groupValues[1]
|
||||
}
|
||||
|
||||
data class ReleaseSigningEnv(
|
||||
val keystoreFile: File,
|
||||
val storePassword: String,
|
||||
@@ -51,12 +59,16 @@ val stagePythonSources by tasks.registering(Sync::class) {
|
||||
from(rootProject.projectDir.resolve("../proxy")) {
|
||||
into("proxy")
|
||||
}
|
||||
from(rootProject.projectDir.resolve("../utils")) {
|
||||
into("utils")
|
||||
}
|
||||
into(stagedPythonSourcesDir)
|
||||
}
|
||||
val releaseSigningRequested = gradle.startParameter.taskNames.any {
|
||||
it.contains("release", ignoreCase = true)
|
||||
}
|
||||
val releaseSigningEnv = loadReleaseSigningEnv(releaseSigningRequested)
|
||||
val appVersionName = loadProxyVersionName()
|
||||
|
||||
android {
|
||||
namespace = "org.flowseal.tgwsproxy"
|
||||
@@ -67,7 +79,7 @@ android {
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "0.1.0"
|
||||
versionName = appVersionName
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -63,7 +63,12 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
val config = settingsStore.load()
|
||||
renderConfig(config)
|
||||
refreshUpdateStatus(checkNow = config.checkUpdates)
|
||||
if (config.checkUpdates) {
|
||||
refreshUpdateStatus(checkNow = true)
|
||||
} else {
|
||||
currentUpdateStatus = null
|
||||
renderUpdateStatus(null, false)
|
||||
}
|
||||
requestNotificationPermissionIfNeeded()
|
||||
observeServiceState()
|
||||
renderSystemStatus()
|
||||
@@ -88,7 +93,12 @@ class MainActivity : AppCompatActivity() {
|
||||
if (showMessage) {
|
||||
Snackbar.make(binding.root, R.string.settings_saved, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
refreshUpdateStatus(checkNow = config.checkUpdates)
|
||||
if (config.checkUpdates) {
|
||||
refreshUpdateStatus(checkNow = true)
|
||||
} else {
|
||||
currentUpdateStatus = null
|
||||
renderUpdateStatus(null, false)
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -141,7 +151,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun onOpenReleasePageClicked() {
|
||||
val url = currentUpdateStatus?.htmlUrl ?: "https://github.com/Dark-Avery/tg-ws-proxy/releases/latest"
|
||||
val url = currentUpdateStatus?.htmlUrl ?: "https://github.com/Flowseal/tg-ws-proxy/releases/latest"
|
||||
val opened = runCatching {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
||||
}.isSuccess
|
||||
@@ -152,8 +162,15 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
private fun refreshUpdateStatus(checkNow: Boolean) {
|
||||
lifecycleScope.launch {
|
||||
val status = withContext(Dispatchers.IO) {
|
||||
PythonProxyBridge.getUpdateStatus(this@MainActivity, checkNow)
|
||||
val status = runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
PythonProxyBridge.getUpdateStatus(this@MainActivity, checkNow)
|
||||
}
|
||||
}.getOrElse { exc ->
|
||||
ProxyUpdateStatus(
|
||||
currentVersion = "unknown",
|
||||
error = exc.message ?: exc.javaClass.simpleName,
|
||||
)
|
||||
}
|
||||
currentUpdateStatus = status
|
||||
renderUpdateStatus(status, binding.checkUpdatesSwitch.isChecked)
|
||||
@@ -161,7 +178,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun renderUpdateStatus(status: ProxyUpdateStatus?, checkUpdatesEnabled: Boolean) {
|
||||
val currentVersion = status?.currentVersion ?: "unknown"
|
||||
val currentVersion = status?.currentVersion?.takeIf { it.isNotBlank() } ?: currentAppVersionName()
|
||||
binding.currentVersionValue.text = getString(
|
||||
R.string.updates_current_version_format,
|
||||
currentVersion,
|
||||
@@ -176,6 +193,9 @@ class MainActivity : AppCompatActivity() {
|
||||
!status.error.isNullOrBlank() -> {
|
||||
getString(R.string.updates_status_error, status.error)
|
||||
}
|
||||
!status.checked -> {
|
||||
getString(R.string.updates_status_idle)
|
||||
}
|
||||
status.hasUpdate && !status.latestVersion.isNullOrBlank() -> {
|
||||
getString(
|
||||
R.string.updates_status_available,
|
||||
@@ -192,6 +212,13 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun currentAppVersionName(): String {
|
||||
return runCatching {
|
||||
@Suppress("DEPRECATION")
|
||||
packageManager.getPackageInfo(packageName, 0).versionName
|
||||
}.getOrNull().orEmpty().ifBlank { "unknown" }
|
||||
}
|
||||
|
||||
private fun observeServiceState() {
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
|
||||
@@ -7,7 +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 checkUpdates: Boolean = false,
|
||||
val verbose: Boolean = false,
|
||||
) {
|
||||
fun validate(): ValidationResult {
|
||||
|
||||
@@ -27,7 +27,7 @@ class ProxySettingsStore(context: Context) {
|
||||
KEY_POOL_SIZE,
|
||||
ProxyConfig.DEFAULT_POOL_SIZE,
|
||||
).toString(),
|
||||
checkUpdates = preferences.getBoolean(KEY_CHECK_UPDATES, true),
|
||||
checkUpdates = preferences.getBoolean(KEY_CHECK_UPDATES, false),
|
||||
verbose = preferences.getBoolean(KEY_VERBOSE, false),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.json.JSONObject
|
||||
|
||||
object PythonProxyBridge {
|
||||
private const val MODULE_NAME = "android_proxy_bridge"
|
||||
private val pythonStartLock = Any()
|
||||
|
||||
fun start(context: Context, config: NormalizedProxyConfig): String {
|
||||
val module = getModule(context)
|
||||
@@ -54,6 +55,7 @@ object PythonProxyBridge {
|
||||
latestVersion = json.optString("latest").ifBlank { null },
|
||||
hasUpdate = json.optBoolean("has_update", false),
|
||||
aheadOfRelease = json.optBoolean("ahead_of_release", false),
|
||||
checked = json.optBoolean("checked", false),
|
||||
htmlUrl = json.optString("html_url").ifBlank { null },
|
||||
error = json.optString("error").ifBlank { null },
|
||||
)
|
||||
@@ -63,8 +65,19 @@ object PythonProxyBridge {
|
||||
getPython(context.applicationContext).getModule(MODULE_NAME)
|
||||
|
||||
private fun getPython(context: Context): Python {
|
||||
if (!Python.isStarted()) {
|
||||
Python.start(AndroidPlatform(context))
|
||||
if (Python.isStarted()) {
|
||||
return Python.getInstance()
|
||||
}
|
||||
synchronized(pythonStartLock) {
|
||||
if (!Python.isStarted()) {
|
||||
try {
|
||||
Python.start(AndroidPlatform(context))
|
||||
} catch (exc: IllegalStateException) {
|
||||
if (!Python.isStarted()) {
|
||||
throw exc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Python.getInstance()
|
||||
}
|
||||
@@ -82,6 +95,7 @@ data class ProxyUpdateStatus(
|
||||
val latestVersion: String? = null,
|
||||
val hasUpdate: Boolean = false,
|
||||
val aheadOfRelease: Boolean = false,
|
||||
val checked: Boolean = false,
|
||||
val htmlUrl: String? = null,
|
||||
val error: String? = null,
|
||||
)
|
||||
|
||||
@@ -8,7 +8,9 @@ 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
|
||||
|
||||
|
||||
RELEASES_PAGE_URL = "https://github.com/Flowseal/tg-ws-proxy/releases/latest"
|
||||
|
||||
|
||||
_RUNTIME_LOCK = threading.RLock()
|
||||
@@ -129,21 +131,30 @@ def get_runtime_stats_json() -> str:
|
||||
return json.dumps(payload)
|
||||
|
||||
|
||||
def _load_update_check():
|
||||
from utils import update_check
|
||||
return update_check
|
||||
|
||||
|
||||
def get_update_status_json(check_now: bool = False) -> str:
|
||||
payload = {
|
||||
"current_version": __version__,
|
||||
"latest": "",
|
||||
"has_update": False,
|
||||
"ahead_of_release": False,
|
||||
"checked": False,
|
||||
"html_url": RELEASES_PAGE_URL,
|
||||
"error": "",
|
||||
}
|
||||
try:
|
||||
update_check = _load_update_check()
|
||||
if check_now:
|
||||
run_check(__version__)
|
||||
payload.update(get_status())
|
||||
update_check.run_check(__version__)
|
||||
payload.update(update_check.get_status())
|
||||
payload["current_version"] = __version__
|
||||
payload["html_url"] = payload.get("html_url") or RELEASES_PAGE_URL
|
||||
payload["latest"] = payload.get("latest") or ""
|
||||
payload["html_url"] = payload.get("html_url") or update_check.RELEASES_PAGE_URL
|
||||
payload["error"] = payload.get("error") or ""
|
||||
except Exception as exc:
|
||||
payload["error"] = str(exc)
|
||||
return json.dumps(payload)
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
<string name="system_check_background_restricted">Background restriction: enabled, Android may block long-running work.</string>
|
||||
<string name="system_check_oem_note">Some phones also require manual vendor settings such as Autostart, Lock in recents, or Unrestricted battery mode.</string>
|
||||
<string name="endpoint_section_label">Proxy endpoint</string>
|
||||
<string name="routing_section_label">Routing</string>
|
||||
<string name="advanced_section_label">Advanced</string>
|
||||
<string name="actions_section_label">Actions</string>
|
||||
<string name="updates_label">Updates</string>
|
||||
<string name="updates_current_version_format">v%1$s</string>
|
||||
<string name="updates_check_label">Check for updates on launch</string>
|
||||
<string name="updates_status_initial">Status will appear after background check.</string>
|
||||
<string name="updates_status_initial">Checking GitHub release…</string>
|
||||
<string name="updates_status_idle">Not checked yet</string>
|
||||
<string name="updates_status_disabled">Automatic update checks are disabled.</string>
|
||||
<string name="updates_status_latest">Installed version %1$s matches the latest GitHub release.</string>
|
||||
<string name="updates_status_newer">Installed version %1$s is newer than the latest GitHub release.</string>
|
||||
|
||||
@@ -121,6 +121,22 @@ prefetch_chaquopy_runtime() {
|
||||
done
|
||||
}
|
||||
|
||||
cleanup_stale_build_state() {
|
||||
local stale_dirs=(
|
||||
"$ROOT_DIR/app/build/python/env"
|
||||
"$ROOT_DIR/app/build/intermediates/project_dex_archive"
|
||||
"$ROOT_DIR/app/build/intermediates/desugar_graph"
|
||||
"$ROOT_DIR/app/build/tmp/kotlin-classes"
|
||||
"$ROOT_DIR/app/build/snapshot/kotlin"
|
||||
)
|
||||
|
||||
for stale_dir in "${stale_dirs[@]}"; do
|
||||
if [[ -d "$stale_dir" ]]; then
|
||||
rm -rf "$stale_dir"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
prefetch_chaquopy_runtime
|
||||
|
||||
for attempt in $(seq 1 "$ATTEMPTS"); do
|
||||
@@ -130,6 +146,7 @@ for attempt in $(seq 1 "$ATTEMPTS"); do
|
||||
fi
|
||||
|
||||
if [[ "$attempt" -lt "$ATTEMPTS" ]]; then
|
||||
cleanup_stale_build_state
|
||||
echo "Build failed, retrying in ${SLEEP_SECONDS}s..."
|
||||
sleep "$SLEEP_SECONDS"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user