From db5a6cc696219938fbacdf635377a867df75a94b Mon Sep 17 00:00:00 2001 From: Dark-Avery Date: Mon, 16 Mar 2026 22:18:06 +0300 Subject: [PATCH] feat(android): add Telegram proxy intent and background-limit status checks --- android/app/src/main/AndroidManifest.xml | 1 + .../flowseal/tgwsproxy/AndroidSystemStatus.kt | 62 +++++++++++++++++++ .../org/flowseal/tgwsproxy/MainActivity.kt | 51 +++++++++++++++ .../flowseal/tgwsproxy/TelegramProxyIntent.kt | 23 +++++++ .../app/src/main/res/layout/activity_main.xml | 60 ++++++++++++++++++ android/app/src/main/res/values/strings.xml | 12 ++++ 6 files changed, 209 insertions(+) create mode 100644 android/app/src/main/java/org/flowseal/tgwsproxy/AndroidSystemStatus.kt create mode 100644 android/app/src/main/java/org/flowseal/tgwsproxy/TelegramProxyIntent.kt diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 132b7ef..989d4c9 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ + = Build.VERSION_CODES.M) { + powerManager.isIgnoringBatteryOptimizations(context.packageName) + } else { + true + } + + val backgroundRestricted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + activityManager.isBackgroundRestricted + } else { + false + } + + return AndroidSystemStatus( + ignoringBatteryOptimizations = ignoringBatteryOptimizations, + backgroundRestricted = backgroundRestricted, + ) + } + + fun openBatteryOptimizationSettings(context: Context) { + val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { + data = Uri.parse("package:${context.packageName}") + } + } else { + Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", context.packageName, null) + } + } + + context.startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + } + + fun openAppSettings(context: Context) { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", context.packageName, null) + } + context.startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + } + } +} 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 fe311c0..4768752 100644 --- a/android/app/src/main/java/org/flowseal/tgwsproxy/MainActivity.kt +++ b/android/app/src/main/java/org/flowseal/tgwsproxy/MainActivity.kt @@ -1,9 +1,11 @@ package org.flowseal.tgwsproxy import android.Manifest +import android.content.Intent import android.content.pm.PackageManager import android.os.Build import android.os.Bundle +import android.provider.Settings import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity @@ -42,10 +44,23 @@ class MainActivity : AppCompatActivity() { binding.startButton.setOnClickListener { onStartClicked() } binding.stopButton.setOnClickListener { ProxyForegroundService.stop(this) } binding.saveButton.setOnClickListener { onSaveClicked(showMessage = true) } + binding.openTelegramButton.setOnClickListener { onOpenTelegramClicked() } + binding.disableBatteryOptimizationButton.setOnClickListener { + AndroidSystemStatus.openBatteryOptimizationSettings(this) + } + binding.openAppSettingsButton.setOnClickListener { + AndroidSystemStatus.openAppSettings(this) + } renderConfig(settingsStore.load()) requestNotificationPermissionIfNeeded() observeServiceState() + renderSystemStatus() + } + + override fun onResume() { + super.onResume() + renderSystemStatus() } private fun onSaveClicked(showMessage: Boolean): NormalizedProxyConfig? { @@ -71,6 +86,13 @@ class MainActivity : AppCompatActivity() { Snackbar.make(binding.root, R.string.service_start_requested, Snackbar.LENGTH_SHORT).show() } + private fun onOpenTelegramClicked() { + val config = onSaveClicked(showMessage = false) ?: return + if (!TelegramProxyIntent.open(this, config)) { + Snackbar.make(binding.root, R.string.telegram_not_found, Snackbar.LENGTH_LONG).show() + } + } + private fun renderConfig(config: ProxyConfig) { binding.hostInput.setText(config.host) binding.portInput.setText(config.portText) @@ -153,6 +175,35 @@ class MainActivity : AppCompatActivity() { } } + private fun renderSystemStatus() { + val status = AndroidSystemStatus.read(this) + + binding.systemStatusValue.text = getString( + if (status.canKeepRunningReliably) { + R.string.system_status_ready + } else { + R.string.system_status_attention + }, + ) + + val lines = mutableListOf() + lines += if (status.ignoringBatteryOptimizations) { + getString(R.string.system_check_battery_ignored) + } else { + getString(R.string.system_check_battery_active) + } + lines += if (status.backgroundRestricted) { + getString(R.string.system_check_background_restricted) + } else { + getString(R.string.system_check_background_ok) + } + lines += getString(R.string.system_check_oem_note) + binding.systemStatusHint.text = lines.joinToString("\n") + + binding.disableBatteryOptimizationButton.isVisible = !status.ignoringBatteryOptimizations + binding.openAppSettingsButton.isVisible = status.backgroundRestricted || !status.ignoringBatteryOptimizations + } + private fun requestNotificationPermissionIfNeeded() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { return diff --git a/android/app/src/main/java/org/flowseal/tgwsproxy/TelegramProxyIntent.kt b/android/app/src/main/java/org/flowseal/tgwsproxy/TelegramProxyIntent.kt new file mode 100644 index 0000000..213126e --- /dev/null +++ b/android/app/src/main/java/org/flowseal/tgwsproxy/TelegramProxyIntent.kt @@ -0,0 +1,23 @@ +package org.flowseal.tgwsproxy + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri + +object TelegramProxyIntent { + fun open(context: Context, config: NormalizedProxyConfig): Boolean { + val uri = Uri.parse( + "tg://socks?server=${Uri.encode(config.host)}&port=${config.port}" + ) + val intent = Intent(Intent.ACTION_VIEW, uri) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + return try { + context.startActivity(intent) + true + } catch (_: ActivityNotFoundException) { + false + } + } +} diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml index 04e35a2..e1dad84 100644 --- a/android/app/src/main/res/layout/activity_main.xml +++ b/android/app/src/main/res/layout/activity_main.xml @@ -64,6 +64,58 @@ + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 85b1c1c..b1f6041 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -9,6 +9,14 @@ Configure the proxy settings, then start the foreground service. Starting embedded Python proxy for %1$s:%2$d. Foreground service active for %1$s:%2$d. + Android background limits + Ready + Needs attention + Battery optimization: disabled for this app. + Battery optimization: still enabled, Android may stop the proxy in background. + 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 host Proxy port DC to IP mappings (one DC:IP per line) @@ -16,8 +24,12 @@ Save Settings Start Service Stop Service + Open in Telegram + Disable Battery Optimization + Open App Settings Settings saved Foreground service start requested + Telegram app was not found for tg://socks. TG WS Proxy Proxy service Keeps the Telegram proxy service alive in the foreground.