feat(android): add advanced upstream tuning settings

This commit is contained in:
Dark_Avery 2026-03-23 02:42:08 +03:00
parent b5b6a8021e
commit faea437556
8 changed files with 182 additions and 2 deletions

View File

@ -108,6 +108,9 @@ class MainActivity : AppCompatActivity() {
binding.hostInput.setText(config.host) binding.hostInput.setText(config.host)
binding.portInput.setText(config.portText) binding.portInput.setText(config.portText)
binding.dcIpInput.setText(config.dcIpText) binding.dcIpInput.setText(config.dcIpText)
binding.logMaxMbInput.setText(config.logMaxMbText)
binding.bufferKbInput.setText(config.bufferKbText)
binding.poolSizeInput.setText(config.poolSizeText)
binding.verboseSwitch.isChecked = config.verbose binding.verboseSwitch.isChecked = config.verbose
} }
@ -116,6 +119,9 @@ class MainActivity : AppCompatActivity() {
host = binding.hostInput.text?.toString().orEmpty(), host = binding.hostInput.text?.toString().orEmpty(),
portText = binding.portInput.text?.toString().orEmpty(), portText = binding.portInput.text?.toString().orEmpty(),
dcIpText = binding.dcIpInput.text?.toString().orEmpty(), dcIpText = binding.dcIpInput.text?.toString().orEmpty(),
logMaxMbText = binding.logMaxMbInput.text?.toString().orEmpty(),
bufferKbText = binding.bufferKbInput.text?.toString().orEmpty(),
poolSizeText = binding.poolSizeInput.text?.toString().orEmpty(),
verbose = binding.verboseSwitch.isChecked, verbose = binding.verboseSwitch.isChecked,
) )
} }

View File

@ -4,6 +4,9 @@ data class ProxyConfig(
val host: String = DEFAULT_HOST, val host: String = DEFAULT_HOST,
val portText: String = DEFAULT_PORT.toString(), val portText: String = DEFAULT_PORT.toString(),
val dcIpText: String = DEFAULT_DC_IP_LINES.joinToString("\n"), val dcIpText: String = DEFAULT_DC_IP_LINES.joinToString("\n"),
val logMaxMbText: String = formatDecimal(DEFAULT_LOG_MAX_MB),
val bufferKbText: String = DEFAULT_BUFFER_KB.toString(),
val poolSizeText: String = DEFAULT_POOL_SIZE.toString(),
val verbose: Boolean = false, val verbose: Boolean = false,
) { ) {
fun validate(): ValidationResult { fun validate(): ValidationResult {
@ -37,11 +40,44 @@ data class ProxyConfig(
} }
} }
val logMaxMbValue = logMaxMbText.trim().toDoubleOrNull()
?: return ValidationResult(
errorMessage = "Размер лог-файла должен быть числом."
)
if (logMaxMbValue <= 0.0) {
return ValidationResult(
errorMessage = "Размер лог-файла должен быть больше нуля."
)
}
val bufferKbValue = bufferKbText.trim().toIntOrNull()
?: return ValidationResult(
errorMessage = "Буфер сокета должен быть целым числом."
)
if (bufferKbValue < 4) {
return ValidationResult(
errorMessage = "Буфер сокета должен быть не меньше 4 KB."
)
}
val poolSizeValue = poolSizeText.trim().toIntOrNull()
?: return ValidationResult(
errorMessage = "Размер WS pool должен быть целым числом."
)
if (poolSizeValue < 0) {
return ValidationResult(
errorMessage = "Размер WS pool не может быть отрицательным."
)
}
return ValidationResult( return ValidationResult(
normalized = NormalizedProxyConfig( normalized = NormalizedProxyConfig(
host = hostValue, host = hostValue,
port = portValue, port = portValue,
dcIpList = lines, dcIpList = lines,
logMaxMb = logMaxMbValue,
bufferKb = bufferKbValue,
poolSize = poolSizeValue,
verbose = verbose, verbose = verbose,
) )
) )
@ -50,11 +86,22 @@ data class ProxyConfig(
companion object { companion object {
const val DEFAULT_HOST = "127.0.0.1" const val DEFAULT_HOST = "127.0.0.1"
const val DEFAULT_PORT = 1080 const val DEFAULT_PORT = 1080
const val DEFAULT_LOG_MAX_MB = 5.0
const val DEFAULT_BUFFER_KB = 256
const val DEFAULT_POOL_SIZE = 4
val DEFAULT_DC_IP_LINES = listOf( val DEFAULT_DC_IP_LINES = listOf(
"2:149.154.167.220", "2:149.154.167.220",
"4:149.154.167.220", "4:149.154.167.220",
) )
fun formatDecimal(value: Double): String {
return if (value % 1.0 == 0.0) {
value.toInt().toString()
} else {
value.toString()
}
}
private fun isIpv4Address(value: String): Boolean { private fun isIpv4Address(value: String): Boolean {
val octets = value.split(".") val octets = value.split(".")
if (octets.size != 4) { if (octets.size != 4) {
@ -80,5 +127,8 @@ data class NormalizedProxyConfig(
val host: String, val host: String,
val port: Int, val port: Int,
val dcIpList: List<String>, val dcIpList: List<String>,
val logMaxMb: Double,
val bufferKb: Int,
val poolSize: Int,
val verbose: Boolean, val verbose: Boolean,
) )

View File

@ -13,6 +13,20 @@ class ProxySettingsStore(context: Context) {
KEY_DC_IP_TEXT, KEY_DC_IP_TEXT,
ProxyConfig.DEFAULT_DC_IP_LINES.joinToString("\n"), ProxyConfig.DEFAULT_DC_IP_LINES.joinToString("\n"),
).orEmpty(), ).orEmpty(),
logMaxMbText = ProxyConfig.formatDecimal(
preferences.getFloat(
KEY_LOG_MAX_MB,
ProxyConfig.DEFAULT_LOG_MAX_MB.toFloat(),
).toDouble()
),
bufferKbText = preferences.getInt(
KEY_BUFFER_KB,
ProxyConfig.DEFAULT_BUFFER_KB,
).toString(),
poolSizeText = preferences.getInt(
KEY_POOL_SIZE,
ProxyConfig.DEFAULT_POOL_SIZE,
).toString(),
verbose = preferences.getBoolean(KEY_VERBOSE, false), verbose = preferences.getBoolean(KEY_VERBOSE, false),
) )
} }
@ -22,6 +36,9 @@ class ProxySettingsStore(context: Context) {
.putString(KEY_HOST, config.host) .putString(KEY_HOST, config.host)
.putInt(KEY_PORT, config.port) .putInt(KEY_PORT, config.port)
.putString(KEY_DC_IP_TEXT, config.dcIpList.joinToString("\n")) .putString(KEY_DC_IP_TEXT, config.dcIpList.joinToString("\n"))
.putFloat(KEY_LOG_MAX_MB, config.logMaxMb.toFloat())
.putInt(KEY_BUFFER_KB, config.bufferKb)
.putInt(KEY_POOL_SIZE, config.poolSize)
.putBoolean(KEY_VERBOSE, config.verbose) .putBoolean(KEY_VERBOSE, config.verbose)
.apply() .apply()
} }
@ -31,6 +48,9 @@ class ProxySettingsStore(context: Context) {
private const val KEY_HOST = "host" private const val KEY_HOST = "host"
private const val KEY_PORT = "port" private const val KEY_PORT = "port"
private const val KEY_DC_IP_TEXT = "dc_ip_text" private const val KEY_DC_IP_TEXT = "dc_ip_text"
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_VERBOSE = "verbose" private const val KEY_VERBOSE = "verbose"
} }
} }

View File

@ -17,6 +17,9 @@ object PythonProxyBridge {
config.host, config.host,
config.port, config.port,
config.dcIpList, config.dcIpList,
config.logMaxMb,
config.bufferKb,
config.poolSize,
config.verbose, config.verbose,
).toString() ).toString()
} }

View File

@ -41,7 +41,9 @@ def _normalize_dc_ip_list(dc_ip_list: Iterable[object]) -> list[str]:
def start_proxy(app_dir: str, host: str, port: int, def start_proxy(app_dir: str, host: str, port: int,
dc_ip_list: Iterable[object], verbose: bool = False) -> str: dc_ip_list: Iterable[object], log_max_mb: float = 5.0,
buf_kb: int = 256, pool_size: int = 4,
verbose: bool = False) -> str:
global _RUNTIME, _LAST_ERROR global _RUNTIME, _LAST_ERROR
with _RUNTIME_LOCK: with _RUNTIME_LOCK:
@ -59,12 +61,15 @@ def start_proxy(app_dir: str, host: str, port: int,
on_error=_remember_error, on_error=_remember_error,
) )
runtime.reset_log_file() runtime.reset_log_file()
runtime.setup_logging(verbose=verbose) runtime.setup_logging(verbose=verbose, log_max_mb=float(log_max_mb))
config = { config = {
"host": host, "host": host,
"port": int(port), "port": int(port),
"dc_ip": _normalize_dc_ip_list(dc_ip_list), "dc_ip": _normalize_dc_ip_list(dc_ip_list),
"log_max_mb": float(log_max_mb),
"buf_kb": int(buf_kb),
"pool_size": int(pool_size),
"verbose": bool(verbose), "verbose": bool(verbose),
} }
runtime.save_config(config) runtime.save_config(config)

View File

@ -197,6 +197,48 @@
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:text="@string/verbose_label" /> android:text="@string/verbose_label" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/log_max_mb_hint">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/logMaxMbInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/buffer_kb_hint">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/bufferKbInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/pool_size_hint">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/poolSizeInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<TextView <TextView
android:id="@+id/errorText" android:id="@+id/errorText"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -20,6 +20,9 @@
<string name="host_hint">Proxy host</string> <string name="host_hint">Proxy host</string>
<string name="port_hint">Proxy port</string> <string name="port_hint">Proxy port</string>
<string name="dc_ip_hint">DC to IP mappings (one DC:IP per line)</string> <string name="dc_ip_hint">DC to IP mappings (one DC:IP per line)</string>
<string name="log_max_mb_hint">Max log size before rotation (MB)</string>
<string name="buffer_kb_hint">Socket buffer size (KB)</string>
<string name="pool_size_hint">WS pool size per DC</string>
<string name="verbose_label">Verbose logging</string> <string name="verbose_label">Verbose logging</string>
<string name="save_button">Save Settings</string> <string name="save_button">Save Settings</string>
<string name="start_button">Start Service</string> <string name="start_button">Start Service</string>

View File

@ -73,6 +73,57 @@ class AndroidProxyBridgeTests(unittest.TestCase):
"4:149.154.167.220", "4:149.154.167.220",
]) ])
def test_start_proxy_saves_advanced_runtime_config(self):
captured = {}
class FakeRuntime:
def __init__(self, *args, **kwargs):
captured["runtime_init"] = kwargs
self.log_file = Path("/tmp/proxy.log")
def reset_log_file(self):
captured["reset_log_file"] = True
def setup_logging(self, verbose=False, log_max_mb=5):
captured["verbose"] = verbose
captured["log_max_mb"] = log_max_mb
def save_config(self, config):
captured["config"] = dict(config)
def start_proxy(self, config):
captured["start_proxy"] = dict(config)
return True
def is_proxy_running(self):
return True
def stop_proxy(self):
captured["stop_proxy"] = True
original_runtime = android_proxy_bridge.ProxyAppRuntime
try:
android_proxy_bridge.ProxyAppRuntime = FakeRuntime
log_path = android_proxy_bridge.start_proxy(
"/tmp/app",
"127.0.0.1",
1080,
["2:149.154.167.220"],
7.0,
512,
6,
True,
)
finally:
android_proxy_bridge.ProxyAppRuntime = original_runtime
self.assertEqual(log_path, "/tmp/proxy.log")
self.assertEqual(captured["config"]["log_max_mb"], 7.0)
self.assertEqual(captured["config"]["buf_kb"], 512)
self.assertEqual(captured["config"]["pool_size"], 6)
self.assertEqual(captured["log_max_mb"], 7.0)
self.assertTrue(captured["verbose"])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()