feat(android): add restart action, log viewer, and persistent service error state
This commit is contained in:
parent
c61e2e84ed
commit
6cbec90360
|
|
@ -15,6 +15,10 @@
|
|||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.TgWsProxy">
|
||||
|
||||
<activity
|
||||
android:name=".LogViewerActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
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.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
|
||||
|
|
@ -43,7 +42,9 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
binding.startButton.setOnClickListener { onStartClicked() }
|
||||
binding.stopButton.setOnClickListener { ProxyForegroundService.stop(this) }
|
||||
binding.restartButton.setOnClickListener { onRestartClicked() }
|
||||
binding.saveButton.setOnClickListener { onSaveClicked(showMessage = true) }
|
||||
binding.openLogsButton.setOnClickListener { onOpenLogsClicked() }
|
||||
binding.openTelegramButton.setOnClickListener { onOpenTelegramClicked() }
|
||||
binding.disableBatteryOptimizationButton.setOnClickListener {
|
||||
AndroidSystemStatus.openBatteryOptimizationSettings(this)
|
||||
|
|
@ -86,6 +87,16 @@ class MainActivity : AppCompatActivity() {
|
|||
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() {
|
||||
val config = onSaveClicked(showMessage = false) ?: return
|
||||
if (!TelegramProxyIntent.open(this, config)) {
|
||||
|
|
@ -127,6 +138,7 @@ class MainActivity : AppCompatActivity() {
|
|||
)
|
||||
binding.startButton.isEnabled = !isStarting && !isRunning
|
||||
binding.stopButton.isEnabled = isStarting || isRunning
|
||||
binding.restartButton.isEnabled = !isStarting
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -162,13 +174,10 @@ class MainActivity : AppCompatActivity() {
|
|||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
ProxyServiceState.lastError.collect { error ->
|
||||
if (error.isNullOrBlank()) {
|
||||
if (!binding.errorText.isVisible) {
|
||||
return@collect
|
||||
}
|
||||
binding.errorText.isVisible = false
|
||||
binding.lastErrorCard.isVisible = false
|
||||
} else {
|
||||
binding.errorText.text = error
|
||||
binding.errorText.isVisible = true
|
||||
binding.lastErrorValue.text = error
|
||||
binding.lastErrorCard.isVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,33 +43,24 @@ class ProxyForegroundService : Service() {
|
|||
START_NOT_STICKY
|
||||
}
|
||||
|
||||
else -> {
|
||||
val config = settingsStore.load().validate().normalized
|
||||
if (config == null) {
|
||||
ProxyServiceState.markFailed(getString(R.string.saved_config_invalid))
|
||||
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 {
|
||||
startProxyRuntime(config)
|
||||
}
|
||||
START_STICKY
|
||||
ACTION_RESTART -> {
|
||||
val config = loadValidatedConfig() ?: return START_NOT_STICKY
|
||||
ProxyServiceState.clearError()
|
||||
beginProxyStart(config)
|
||||
serviceScope.launch {
|
||||
stopRuntimeOnly()
|
||||
startProxyRuntime(config)
|
||||
}
|
||||
START_STICKY
|
||||
}
|
||||
|
||||
else -> {
|
||||
val config = loadValidatedConfig() ?: return START_NOT_STICKY
|
||||
beginProxyStart(config)
|
||||
serviceScope.launch {
|
||||
startProxyRuntime(config)
|
||||
}
|
||||
START_STICKY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
stopTrafficUpdates()
|
||||
runCatching { PythonProxyBridge.stop(this) }
|
||||
stopRuntimeOnly()
|
||||
ProxyServiceState.markStopped()
|
||||
|
||||
if (removeNotification) {
|
||||
|
|
@ -146,6 +164,11 @@ class ProxyForegroundService : Service() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun stopRuntimeOnly() {
|
||||
stopTrafficUpdates()
|
||||
runCatching { PythonProxyBridge.stop(this) }
|
||||
}
|
||||
|
||||
private fun updateNotification(payload: NotificationPayload) {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
manager.notify(NOTIFICATION_ID, buildNotification(payload))
|
||||
|
|
@ -153,9 +176,9 @@ class ProxyForegroundService : Service() {
|
|||
|
||||
private fun buildNotificationPayload(
|
||||
config: NormalizedProxyConfig,
|
||||
trafficState: TrafficState,
|
||||
statusText: String,
|
||||
): NotificationPayload {
|
||||
val trafficState = readTrafficState()
|
||||
val endpointText = getString(R.string.notification_endpoint, config.host, config.port)
|
||||
val detailsText = getString(
|
||||
R.string.notification_details,
|
||||
|
|
@ -176,9 +199,19 @@ class ProxyForegroundService : Service() {
|
|||
stopTrafficUpdates()
|
||||
trafficJob = serviceScope.launch {
|
||||
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(
|
||||
buildNotificationPayload(
|
||||
config = config,
|
||||
trafficState = trafficState,
|
||||
statusText = getString(
|
||||
R.string.notification_running,
|
||||
config.host,
|
||||
|
|
@ -213,6 +246,8 @@ class ProxyForegroundService : Service() {
|
|||
downBytesPerSecond = 0L,
|
||||
totalBytesUp = current.bytesUp,
|
||||
totalBytesDown = current.bytesDown,
|
||||
running = current.running,
|
||||
lastError = current.lastError,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -224,6 +259,8 @@ class ProxyForegroundService : Service() {
|
|||
downBytesPerSecond = (downDelta * 1000L) / elapsedMillis,
|
||||
totalBytesUp = current.bytesUp,
|
||||
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 ACTION_START = "org.flowseal.tgwsproxy.action.START"
|
||||
private const val ACTION_STOP = "org.flowseal.tgwsproxy.action.STOP"
|
||||
private const val ACTION_RESTART = "org.flowseal.tgwsproxy.action.RESTART"
|
||||
|
||||
fun start(context: Context) {
|
||||
val intent = Intent(context, ProxyForegroundService::class.java).apply {
|
||||
|
|
@ -324,6 +362,13 @@ class ProxyForegroundService : Service() {
|
|||
}
|
||||
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(
|
||||
val upBytesPerSecond: Long,
|
||||
val downBytesPerSecond: Long,
|
||||
val totalBytesUp: Long,
|
||||
val totalBytesDown: Long,
|
||||
val upBytesPerSecond: Long = 0L,
|
||||
val downBytesPerSecond: Long = 0L,
|
||||
val totalBytesUp: Long = 0L,
|
||||
val totalBytesDown: Long = 0L,
|
||||
val running: Boolean = false,
|
||||
val lastError: String? = null,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ object PythonProxyBridge {
|
|||
bytesUp = json.optLong("bytes_up", 0L),
|
||||
bytesDown = json.optLong("bytes_down", 0L),
|
||||
running = json.optBoolean("running", false),
|
||||
lastError = json.optString("last_error").ifBlank { null },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -57,4 +58,5 @@ data class ProxyTrafficStats(
|
|||
val bytesUp: Long = 0L,
|
||||
val bytesDown: Long = 0L,
|
||||
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["running"] = running
|
||||
payload["last_error"] = _LAST_ERROR
|
||||
return json.dumps(payload)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</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
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
@ -199,6 +230,14 @@
|
|||
android:enabled="false"
|
||||
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
|
||||
android:id="@+id/openTelegramButton"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
|
|
@ -207,5 +246,13 @@
|
|||
android:layout_marginTop="12dp"
|
||||
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>
|
||||
</ScrollView>
|
||||
|
|
|
|||
|
|
@ -24,11 +24,15 @@
|
|||
<string name="save_button">Save Settings</string>
|
||||
<string name="start_button">Start 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_logs_button">Open Logs</string>
|
||||
<string name="disable_battery_optimization_button">Disable Battery Optimization</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="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="notification_title">TG WS Proxy</string>
|
||||
<string name="notification_channel_name">Proxy service</string>
|
||||
|
|
@ -40,4 +44,13 @@
|
|||
<string name="notification_action_stop">Stop</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_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>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class FakeJavaArrayList:
|
|||
class AndroidProxyBridgeTests(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
tg_ws_proxy.reset_stats()
|
||||
android_proxy_bridge._LAST_ERROR = None
|
||||
|
||||
def test_normalize_dc_ip_list_with_python_iterable(self):
|
||||
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_down"], 4096)
|
||||
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):
|
||||
result = android_proxy_bridge._normalize_dc_ip_list(FakeJavaArrayList([
|
||||
|
|
|
|||
Loading…
Reference in New Issue