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.