Merge pull request #3 from Dark-Avery/android_migration
add restart action, log viewer, service error state, tray icon
This commit is contained in:
commit
ee4d41ab9c
|
|
@ -15,6 +15,10 @@
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.TgWsProxy">
|
android:theme="@style/Theme.TgWsProxy">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".LogViewerActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package org.flowseal.tgwsproxy
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import org.flowseal.tgwsproxy.databinding.ActivityLogViewerBinding
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class LogViewerActivity : AppCompatActivity() {
|
||||||
|
private lateinit var binding: ActivityLogViewerBinding
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityLogViewerBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
binding.refreshLogsButton.setOnClickListener { renderLog() }
|
||||||
|
binding.closeLogsButton.setOnClickListener { finish() }
|
||||||
|
|
||||||
|
renderLog()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
renderLog()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderLog() {
|
||||||
|
val logFile = File(filesDir, "tg-ws-proxy/proxy.log")
|
||||||
|
binding.logPathValue.text = logFile.absolutePath
|
||||||
|
binding.logContentValue.text = readLogTail(logFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readLogTail(logFile: File, maxChars: Int = 40000): String {
|
||||||
|
if (!logFile.isFile) {
|
||||||
|
return getString(R.string.logs_empty)
|
||||||
|
}
|
||||||
|
|
||||||
|
val text = runCatching {
|
||||||
|
logFile.readText(Charsets.UTF_8)
|
||||||
|
}.getOrElse { error ->
|
||||||
|
return getString(R.string.logs_read_failed, error.message ?: error.javaClass.simpleName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.isBlank()) {
|
||||||
|
return getString(R.string.logs_empty)
|
||||||
|
}
|
||||||
|
if (text.length <= maxChars) {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
return getString(R.string.logs_truncated_prefix) + "\n\n" + text.takeLast(maxChars)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,6 @@ import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
|
@ -43,7 +42,9 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
binding.startButton.setOnClickListener { onStartClicked() }
|
binding.startButton.setOnClickListener { onStartClicked() }
|
||||||
binding.stopButton.setOnClickListener { ProxyForegroundService.stop(this) }
|
binding.stopButton.setOnClickListener { ProxyForegroundService.stop(this) }
|
||||||
|
binding.restartButton.setOnClickListener { onRestartClicked() }
|
||||||
binding.saveButton.setOnClickListener { onSaveClicked(showMessage = true) }
|
binding.saveButton.setOnClickListener { onSaveClicked(showMessage = true) }
|
||||||
|
binding.openLogsButton.setOnClickListener { onOpenLogsClicked() }
|
||||||
binding.openTelegramButton.setOnClickListener { onOpenTelegramClicked() }
|
binding.openTelegramButton.setOnClickListener { onOpenTelegramClicked() }
|
||||||
binding.disableBatteryOptimizationButton.setOnClickListener {
|
binding.disableBatteryOptimizationButton.setOnClickListener {
|
||||||
AndroidSystemStatus.openBatteryOptimizationSettings(this)
|
AndroidSystemStatus.openBatteryOptimizationSettings(this)
|
||||||
|
|
@ -86,6 +87,16 @@ class MainActivity : AppCompatActivity() {
|
||||||
Snackbar.make(binding.root, R.string.service_start_requested, Snackbar.LENGTH_SHORT).show()
|
Snackbar.make(binding.root, R.string.service_start_requested, Snackbar.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onRestartClicked() {
|
||||||
|
onSaveClicked(showMessage = false) ?: return
|
||||||
|
ProxyForegroundService.restart(this)
|
||||||
|
Snackbar.make(binding.root, R.string.service_restart_requested, Snackbar.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onOpenLogsClicked() {
|
||||||
|
startActivity(Intent(this, LogViewerActivity::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
private fun onOpenTelegramClicked() {
|
private fun onOpenTelegramClicked() {
|
||||||
val config = onSaveClicked(showMessage = false) ?: return
|
val config = onSaveClicked(showMessage = false) ?: return
|
||||||
if (!TelegramProxyIntent.open(this, config)) {
|
if (!TelegramProxyIntent.open(this, config)) {
|
||||||
|
|
@ -127,6 +138,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
)
|
)
|
||||||
binding.startButton.isEnabled = !isStarting && !isRunning
|
binding.startButton.isEnabled = !isStarting && !isRunning
|
||||||
binding.stopButton.isEnabled = isStarting || isRunning
|
binding.stopButton.isEnabled = isStarting || isRunning
|
||||||
|
binding.restartButton.isEnabled = !isStarting
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -162,13 +174,10 @@ class MainActivity : AppCompatActivity() {
|
||||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
ProxyServiceState.lastError.collect { error ->
|
ProxyServiceState.lastError.collect { error ->
|
||||||
if (error.isNullOrBlank()) {
|
if (error.isNullOrBlank()) {
|
||||||
if (!binding.errorText.isVisible) {
|
binding.lastErrorCard.isVisible = false
|
||||||
return@collect
|
|
||||||
}
|
|
||||||
binding.errorText.isVisible = false
|
|
||||||
} else {
|
} else {
|
||||||
binding.errorText.text = error
|
binding.lastErrorValue.text = error
|
||||||
binding.errorText.isVisible = true
|
binding.lastErrorCard.isVisible = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,33 +43,24 @@ class ProxyForegroundService : Service() {
|
||||||
START_NOT_STICKY
|
START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
ACTION_RESTART -> {
|
||||||
val config = settingsStore.load().validate().normalized
|
val config = loadValidatedConfig() ?: return START_NOT_STICKY
|
||||||
if (config == null) {
|
ProxyServiceState.clearError()
|
||||||
ProxyServiceState.markFailed(getString(R.string.saved_config_invalid))
|
beginProxyStart(config)
|
||||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
|
||||||
stopSelf()
|
|
||||||
START_NOT_STICKY
|
|
||||||
} else {
|
|
||||||
ProxyServiceState.markStarting(config)
|
|
||||||
startForeground(
|
|
||||||
NOTIFICATION_ID,
|
|
||||||
buildNotification(
|
|
||||||
buildNotificationPayload(
|
|
||||||
config = config,
|
|
||||||
statusText = getString(
|
|
||||||
R.string.notification_starting,
|
|
||||||
config.host,
|
|
||||||
config.port,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
|
stopRuntimeOnly()
|
||||||
startProxyRuntime(config)
|
startProxyRuntime(config)
|
||||||
}
|
}
|
||||||
START_STICKY
|
START_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
val config = loadValidatedConfig() ?: return START_NOT_STICKY
|
||||||
|
beginProxyStart(config)
|
||||||
|
serviceScope.launch {
|
||||||
|
startProxyRuntime(config)
|
||||||
|
}
|
||||||
|
START_STICKY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -92,7 +83,7 @@ class ProxyForegroundService : Service() {
|
||||||
.setStyle(
|
.setStyle(
|
||||||
NotificationCompat.BigTextStyle().bigText(payload.detailsText),
|
NotificationCompat.BigTextStyle().bigText(payload.detailsText),
|
||||||
)
|
)
|
||||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
.setSmallIcon(R.drawable.ic_proxy_notification)
|
||||||
.setContentIntent(createOpenAppPendingIntent())
|
.setContentIntent(createOpenAppPendingIntent())
|
||||||
.addAction(
|
.addAction(
|
||||||
0,
|
0,
|
||||||
|
|
@ -133,9 +124,36 @@ class ProxyForegroundService : Service() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun loadValidatedConfig(): NormalizedProxyConfig? {
|
||||||
|
val config = settingsStore.load().validate().normalized
|
||||||
|
if (config == null) {
|
||||||
|
ProxyServiceState.markFailed(getString(R.string.saved_config_invalid))
|
||||||
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun beginProxyStart(config: NormalizedProxyConfig) {
|
||||||
|
ProxyServiceState.markStarting(config)
|
||||||
|
startForeground(
|
||||||
|
NOTIFICATION_ID,
|
||||||
|
buildNotification(
|
||||||
|
buildNotificationPayload(
|
||||||
|
config = config,
|
||||||
|
trafficState = TrafficState(),
|
||||||
|
statusText = getString(
|
||||||
|
R.string.notification_starting,
|
||||||
|
config.host,
|
||||||
|
config.port,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun stopProxyRuntime(removeNotification: Boolean, stopService: Boolean) {
|
private fun stopProxyRuntime(removeNotification: Boolean, stopService: Boolean) {
|
||||||
stopTrafficUpdates()
|
stopRuntimeOnly()
|
||||||
runCatching { PythonProxyBridge.stop(this) }
|
|
||||||
ProxyServiceState.markStopped()
|
ProxyServiceState.markStopped()
|
||||||
|
|
||||||
if (removeNotification) {
|
if (removeNotification) {
|
||||||
|
|
@ -146,6 +164,11 @@ class ProxyForegroundService : Service() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun stopRuntimeOnly() {
|
||||||
|
stopTrafficUpdates()
|
||||||
|
runCatching { PythonProxyBridge.stop(this) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateNotification(payload: NotificationPayload) {
|
private fun updateNotification(payload: NotificationPayload) {
|
||||||
val manager = getSystemService(NotificationManager::class.java)
|
val manager = getSystemService(NotificationManager::class.java)
|
||||||
manager.notify(NOTIFICATION_ID, buildNotification(payload))
|
manager.notify(NOTIFICATION_ID, buildNotification(payload))
|
||||||
|
|
@ -153,9 +176,9 @@ class ProxyForegroundService : Service() {
|
||||||
|
|
||||||
private fun buildNotificationPayload(
|
private fun buildNotificationPayload(
|
||||||
config: NormalizedProxyConfig,
|
config: NormalizedProxyConfig,
|
||||||
|
trafficState: TrafficState,
|
||||||
statusText: String,
|
statusText: String,
|
||||||
): NotificationPayload {
|
): NotificationPayload {
|
||||||
val trafficState = readTrafficState()
|
|
||||||
val endpointText = getString(R.string.notification_endpoint, config.host, config.port)
|
val endpointText = getString(R.string.notification_endpoint, config.host, config.port)
|
||||||
val detailsText = getString(
|
val detailsText = getString(
|
||||||
R.string.notification_details,
|
R.string.notification_details,
|
||||||
|
|
@ -176,9 +199,19 @@ class ProxyForegroundService : Service() {
|
||||||
stopTrafficUpdates()
|
stopTrafficUpdates()
|
||||||
trafficJob = serviceScope.launch {
|
trafficJob = serviceScope.launch {
|
||||||
while (isActive && ProxyServiceState.isRunning.value) {
|
while (isActive && ProxyServiceState.isRunning.value) {
|
||||||
|
val trafficState = readTrafficState()
|
||||||
|
if (!trafficState.running) {
|
||||||
|
ProxyServiceState.markFailed(
|
||||||
|
trafficState.lastError ?: getString(R.string.proxy_runtime_stopped_unexpectedly),
|
||||||
|
)
|
||||||
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
|
stopSelf()
|
||||||
|
break
|
||||||
|
}
|
||||||
updateNotification(
|
updateNotification(
|
||||||
buildNotificationPayload(
|
buildNotificationPayload(
|
||||||
config = config,
|
config = config,
|
||||||
|
trafficState = trafficState,
|
||||||
statusText = getString(
|
statusText = getString(
|
||||||
R.string.notification_running,
|
R.string.notification_running,
|
||||||
config.host,
|
config.host,
|
||||||
|
|
@ -213,6 +246,8 @@ class ProxyForegroundService : Service() {
|
||||||
downBytesPerSecond = 0L,
|
downBytesPerSecond = 0L,
|
||||||
totalBytesUp = current.bytesUp,
|
totalBytesUp = current.bytesUp,
|
||||||
totalBytesDown = current.bytesDown,
|
totalBytesDown = current.bytesDown,
|
||||||
|
running = current.running,
|
||||||
|
lastError = current.lastError,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -224,6 +259,8 @@ class ProxyForegroundService : Service() {
|
||||||
downBytesPerSecond = (downDelta * 1000L) / elapsedMillis,
|
downBytesPerSecond = (downDelta * 1000L) / elapsedMillis,
|
||||||
totalBytesUp = current.bytesUp,
|
totalBytesUp = current.bytesUp,
|
||||||
totalBytesDown = current.bytesDown,
|
totalBytesDown = current.bytesDown,
|
||||||
|
running = current.running,
|
||||||
|
lastError = current.lastError,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -310,6 +347,7 @@ class ProxyForegroundService : Service() {
|
||||||
private const val NOTIFICATION_ID = 1001
|
private const val NOTIFICATION_ID = 1001
|
||||||
private const val ACTION_START = "org.flowseal.tgwsproxy.action.START"
|
private const val ACTION_START = "org.flowseal.tgwsproxy.action.START"
|
||||||
private const val ACTION_STOP = "org.flowseal.tgwsproxy.action.STOP"
|
private const val ACTION_STOP = "org.flowseal.tgwsproxy.action.STOP"
|
||||||
|
private const val ACTION_RESTART = "org.flowseal.tgwsproxy.action.RESTART"
|
||||||
|
|
||||||
fun start(context: Context) {
|
fun start(context: Context) {
|
||||||
val intent = Intent(context, ProxyForegroundService::class.java).apply {
|
val intent = Intent(context, ProxyForegroundService::class.java).apply {
|
||||||
|
|
@ -324,6 +362,13 @@ class ProxyForegroundService : Service() {
|
||||||
}
|
}
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun restart(context: Context) {
|
||||||
|
val intent = Intent(context, ProxyForegroundService::class.java).apply {
|
||||||
|
action = ACTION_RESTART
|
||||||
|
}
|
||||||
|
androidx.core.content.ContextCompat.startForegroundService(context, intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -340,8 +385,10 @@ private data class TrafficSample(
|
||||||
)
|
)
|
||||||
|
|
||||||
private data class TrafficState(
|
private data class TrafficState(
|
||||||
val upBytesPerSecond: Long,
|
val upBytesPerSecond: Long = 0L,
|
||||||
val downBytesPerSecond: Long,
|
val downBytesPerSecond: Long = 0L,
|
||||||
val totalBytesUp: Long,
|
val totalBytesUp: Long = 0L,
|
||||||
val totalBytesDown: Long,
|
val totalBytesDown: Long = 0L,
|
||||||
|
val running: Boolean = false,
|
||||||
|
val lastError: String? = null,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ object PythonProxyBridge {
|
||||||
bytesUp = json.optLong("bytes_up", 0L),
|
bytesUp = json.optLong("bytes_up", 0L),
|
||||||
bytesDown = json.optLong("bytes_down", 0L),
|
bytesDown = json.optLong("bytes_down", 0L),
|
||||||
running = json.optBoolean("running", false),
|
running = json.optBoolean("running", false),
|
||||||
|
lastError = json.optString("last_error").ifBlank { null },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,4 +58,5 @@ data class ProxyTrafficStats(
|
||||||
val bytesUp: Long = 0L,
|
val bytesUp: Long = 0L,
|
||||||
val bytesDown: Long = 0L,
|
val bytesDown: Long = 0L,
|
||||||
val running: Boolean = false,
|
val running: Boolean = false,
|
||||||
|
val lastError: String? = null,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -118,4 +118,5 @@ def get_runtime_stats_json() -> str:
|
||||||
|
|
||||||
payload = dict(tg_ws_proxy.get_stats_snapshot())
|
payload = dict(tg_ws_proxy.get_stats_snapshot())
|
||||||
payload["running"] = running
|
payload["running"] = running
|
||||||
|
payload["last_error"] = _LAST_ERROR
|
||||||
return json.dumps(payload)
|
return json.dumps(payload)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M5,4h14v3h-5v12h-4V7H5z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M8,20h8v2H8z" />
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fillViewport="true">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="20dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/logs_title"
|
||||||
|
android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/logs_subtitle"
|
||||||
|
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" />
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
app:cardCornerRadius="20dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="18dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/logs_path_label"
|
||||||
|
android:textAppearance="@style/TextAppearance.Material3.LabelLarge" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/logPathValue"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.Material3.BodySmall" />
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
app:cardCornerRadius="20dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="18dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/logContentValue"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||||
|
android:textIsSelectable="true" />
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/refreshLogsButton"
|
||||||
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/refresh_logs_button" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/closeLogsButton"
|
||||||
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:text="@string/close_logs_button" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
@ -64,6 +64,37 @@
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:id="@+id/lastErrorCard"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:cardCornerRadius="20dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="18dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/last_error_label"
|
||||||
|
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
||||||
|
android:textColor="?attr/colorError" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/lastErrorValue"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
||||||
|
android:textColor="?attr/colorError" />
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
@ -199,6 +230,14 @@
|
||||||
android:enabled="false"
|
android:enabled="false"
|
||||||
android:text="@string/stop_button" />
|
android:text="@string/stop_button" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/restartButton"
|
||||||
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:text="@string/restart_button" />
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/openTelegramButton"
|
android:id="@+id/openTelegramButton"
|
||||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
|
|
@ -207,5 +246,13 @@
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
android:text="@string/open_telegram_button" />
|
android:text="@string/open_telegram_button" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/openLogsButton"
|
||||||
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:text="@string/open_logs_button" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,15 @@
|
||||||
<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>
|
||||||
<string name="stop_button">Stop Service</string>
|
<string name="stop_button">Stop Service</string>
|
||||||
|
<string name="restart_button">Restart Proxy</string>
|
||||||
<string name="open_telegram_button">Open in Telegram</string>
|
<string name="open_telegram_button">Open in Telegram</string>
|
||||||
|
<string name="open_logs_button">Open Logs</string>
|
||||||
<string name="disable_battery_optimization_button">Disable Battery Optimization</string>
|
<string name="disable_battery_optimization_button">Disable Battery Optimization</string>
|
||||||
<string name="open_app_settings_button">Open App Settings</string>
|
<string name="open_app_settings_button">Open App Settings</string>
|
||||||
|
<string name="last_error_label">Last service error</string>
|
||||||
<string name="settings_saved">Settings saved</string>
|
<string name="settings_saved">Settings saved</string>
|
||||||
<string name="service_start_requested">Foreground service start requested</string>
|
<string name="service_start_requested">Foreground service start requested</string>
|
||||||
|
<string name="service_restart_requested">Foreground service restart requested</string>
|
||||||
<string name="telegram_not_found">Telegram app was not found for tg://socks.</string>
|
<string name="telegram_not_found">Telegram app was not found for tg://socks.</string>
|
||||||
<string name="notification_title">TG WS Proxy</string>
|
<string name="notification_title">TG WS Proxy</string>
|
||||||
<string name="notification_channel_name">Proxy service</string>
|
<string name="notification_channel_name">Proxy service</string>
|
||||||
|
|
@ -40,4 +44,13 @@
|
||||||
<string name="notification_action_stop">Stop</string>
|
<string name="notification_action_stop">Stop</string>
|
||||||
<string name="saved_config_invalid">Saved proxy settings are invalid.</string>
|
<string name="saved_config_invalid">Saved proxy settings are invalid.</string>
|
||||||
<string name="proxy_start_failed_generic">Failed to start embedded Python proxy.</string>
|
<string name="proxy_start_failed_generic">Failed to start embedded Python proxy.</string>
|
||||||
|
<string name="proxy_runtime_stopped_unexpectedly">Proxy runtime stopped unexpectedly.</string>
|
||||||
|
<string name="logs_title">Proxy Logs</string>
|
||||||
|
<string name="logs_subtitle">Shows the latest lines from the embedded Python proxy log.</string>
|
||||||
|
<string name="logs_path_label">Log file</string>
|
||||||
|
<string name="refresh_logs_button">Refresh Logs</string>
|
||||||
|
<string name="close_logs_button">Close</string>
|
||||||
|
<string name="logs_empty">The log file is empty or has not been created yet.</string>
|
||||||
|
<string name="logs_read_failed">Failed to read log file: %1$s</string>
|
||||||
|
<string name="logs_truncated_prefix">Showing the last part of the log file.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ class FakeJavaArrayList:
|
||||||
class AndroidProxyBridgeTests(unittest.TestCase):
|
class AndroidProxyBridgeTests(unittest.TestCase):
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
tg_ws_proxy.reset_stats()
|
tg_ws_proxy.reset_stats()
|
||||||
|
android_proxy_bridge._LAST_ERROR = None
|
||||||
|
|
||||||
def test_normalize_dc_ip_list_with_python_iterable(self):
|
def test_normalize_dc_ip_list_with_python_iterable(self):
|
||||||
result = android_proxy_bridge._normalize_dc_ip_list([
|
result = android_proxy_bridge._normalize_dc_ip_list([
|
||||||
|
|
@ -52,6 +53,14 @@ class AndroidProxyBridgeTests(unittest.TestCase):
|
||||||
self.assertEqual(result["bytes_up"], 1536)
|
self.assertEqual(result["bytes_up"], 1536)
|
||||||
self.assertEqual(result["bytes_down"], 4096)
|
self.assertEqual(result["bytes_down"], 4096)
|
||||||
self.assertFalse(result["running"])
|
self.assertFalse(result["running"])
|
||||||
|
self.assertIsNone(result["last_error"])
|
||||||
|
|
||||||
|
def test_get_runtime_stats_json_includes_last_error(self):
|
||||||
|
android_proxy_bridge._LAST_ERROR = "boom"
|
||||||
|
|
||||||
|
result = json.loads(android_proxy_bridge.get_runtime_stats_json())
|
||||||
|
|
||||||
|
self.assertEqual(result["last_error"], "boom")
|
||||||
|
|
||||||
def test_normalize_dc_ip_list_with_java_array_list_shape(self):
|
def test_normalize_dc_ip_list_with_java_array_list_shape(self):
|
||||||
result = android_proxy_bridge._normalize_dc_ip_list(FakeJavaArrayList([
|
result = android_proxy_bridge._normalize_dc_ip_list(FakeJavaArrayList([
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue