Skip to content

Commit de06c25

Browse files
authored
Merge pull request #22 from andre487/feat/traffic-totals
Show total VPN session traffic
2 parents 627ccd4 + a5fc9bf commit de06c25

7 files changed

Lines changed: 277 additions & 25 deletions

File tree

‎app/src/main/java/net/megaproxy487/MainActivity.kt‎

Lines changed: 70 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ import androidx.compose.ui.Modifier
6161
import androidx.compose.ui.graphics.Color
6262
import androidx.compose.ui.graphics.luminance
6363
import androidx.compose.ui.res.stringResource
64+
import androidx.compose.ui.res.pluralStringResource
65+
import androidx.compose.ui.text.style.TextAlign
6466
import androidx.compose.ui.semantics.LiveRegionMode
6567
import androidx.compose.ui.semantics.liveRegion
6668
import androidx.compose.ui.semantics.semantics
@@ -663,23 +665,51 @@ private data class RefreshedMainConfig(
663665

664666
@Composable
665667
private fun ConnectionStatsCard(stats: DisplayedConnectionStats) {
668+
val context = androidx.compose.ui.platform.LocalContext.current
669+
val unitSystem = TrafficUnitPreferences.current(context)
666670
Card(Modifier.fillMaxWidth()) {
667671
Row(
668672
Modifier.fillMaxWidth().padding(14.dp),
669-
horizontalArrangement = Arrangement.SpaceBetween,
673+
horizontalArrangement = Arrangement.SpaceEvenly,
670674
) {
671-
StatValue("Download", "↓ ${formatRate(stats.downloadBytesPerSecond)}")
672-
StatValue("Upload", "↑ ${formatRate(stats.uploadBytesPerSecond)}")
675+
StatValue(
676+
label = stringResource(R.string.traffic_download),
677+
value = "↓ ${formatTrafficRate(stats.downloadBytesPerSecond, unitSystem)}",
678+
supportingValue = stringResource(
679+
R.string.traffic_total,
680+
formatTrafficBytes(stats.native.downloadBytes, unitSystem),
681+
),
682+
modifier = Modifier.weight(1f),
683+
)
684+
StatValue(
685+
label = stringResource(R.string.traffic_upload),
686+
value = "↑ ${formatTrafficRate(stats.uploadBytesPerSecond, unitSystem)}",
687+
supportingValue = stringResource(
688+
R.string.traffic_total,
689+
formatTrafficBytes(stats.native.uploadBytes, unitSystem),
690+
),
691+
modifier = Modifier.weight(1f),
692+
)
673693
val latency = stats.native.proxyLatencyMillis
674694
val ageMillis = System.currentTimeMillis() - stats.native.proxyLatencyAtMillis
675695
StatValue(
676-
"Proxy latency",
677-
if (latency <= 0) "—" else "${latency.toInt()} ms${formatAge(ageMillis)}",
696+
label = stringResource(R.string.proxy_latency),
697+
value = if (latency <= 0) "—" else stringResource(
698+
R.string.latency_milliseconds,
699+
latency.toInt(),
700+
formatAge(ageMillis),
701+
),
702+
modifier = Modifier.weight(1f),
678703
)
679704
}
705+
val samples = stats.native.connectionSamples
680706
Text(
681-
if (stats.native.connectionSamples == 0) "Connection errors: no samples"
682-
else "Connection errors: ${"%.1f".format(stats.native.connectionErrorRate * 100)}% · ${stats.native.connectionSamples} samples",
707+
if (samples == 0) stringResource(R.string.connection_errors_no_samples)
708+
else stringResource(
709+
R.string.connection_errors,
710+
stats.native.connectionErrorRate * 100,
711+
pluralStringResource(R.plurals.connection_samples, samples, samples),
712+
),
683713
style = MaterialTheme.typography.bodySmall,
684714
color = MaterialTheme.colorScheme.onSurfaceVariant,
685715
modifier = Modifier.padding(start = 14.dp, end = 14.dp, bottom = 12.dp),
@@ -688,21 +718,42 @@ private fun ConnectionStatsCard(stats: DisplayedConnectionStats) {
688718
}
689719

690720
@Composable
691-
private fun StatValue(label: String, value: String) {
692-
Column(horizontalAlignment = Alignment.CenterHorizontally) {
693-
Text(value, style = MaterialTheme.typography.titleSmall)
694-
Text(label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
721+
private fun StatValue(
722+
label: String,
723+
value: String,
724+
modifier: Modifier = Modifier,
725+
supportingValue: String? = null,
726+
) {
727+
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
728+
Text(value, style = MaterialTheme.typography.titleSmall, textAlign = TextAlign.Center)
729+
Text(
730+
label,
731+
style = MaterialTheme.typography.labelSmall,
732+
color = MaterialTheme.colorScheme.onSurfaceVariant,
733+
textAlign = TextAlign.Center,
734+
)
735+
supportingValue?.let {
736+
Text(
737+
it,
738+
style = MaterialTheme.typography.labelSmall,
739+
color = MaterialTheme.colorScheme.onSurfaceVariant,
740+
textAlign = TextAlign.Center,
741+
)
742+
}
695743
}
696744
}
697745

698-
private fun formatRate(bytesPerSecond: Double): String = when {
699-
bytesPerSecond >= 1_000_000 -> "%.1f MB/s".format(bytesPerSecond / 1_000_000)
700-
bytesPerSecond >= 1_000 -> "%.1f KB/s".format(bytesPerSecond / 1_000)
701-
else -> "${bytesPerSecond.toInt()} B/s"
702-
}
703-
746+
@Composable
704747
private fun formatAge(ageMillis: Long): String = when {
705748
ageMillis < 30_000 -> ""
706-
ageMillis < 120_000 -> " · ${ageMillis / 1_000}s ago"
707-
else -> " · ${ageMillis / 60_000}m ago"
749+
ageMillis < 120_000 -> pluralStringResource(
750+
R.plurals.latency_seconds_ago,
751+
(ageMillis / 1_000).toInt(),
752+
(ageMillis / 1_000).toInt(),
753+
)
754+
else -> pluralStringResource(
755+
R.plurals.latency_minutes_ago,
756+
(ageMillis / 60_000).toInt(),
757+
(ageMillis / 60_000).toInt(),
758+
)
708759
}

‎app/src/main/java/net/megaproxy487/SettingsScreens.kt‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ internal fun SettingsHomeScreen(activity: Activity, onBack: () -> Unit, onNaviga
8787
mutableStateOf(powerManager.isIgnoringBatteryOptimizations(activity.packageName))
8888
}
8989
var showLanguageDialog by remember { mutableStateOf(false) }
90+
var showTrafficUnitsDialog by remember { mutableStateOf(false) }
91+
var trafficUnitSystem by remember { mutableStateOf(TrafficUnitPreferences.current(activity)) }
9092
var supportError by remember { mutableStateOf<String?>(null) }
9193
val scope = rememberCoroutineScope()
9294

@@ -122,6 +124,16 @@ internal fun SettingsHomeScreen(activity: Activity, onBack: () -> Unit, onNaviga
122124
stringResource(R.string.language),
123125
"${stringResource(R.string.language_description)} · ${if (AppLanguageManager.current(activity) == AppLanguage.RUSSIAN) stringResource(R.string.language_russian) else stringResource(R.string.language_english)}",
124126
) { showLanguageDialog = true }
127+
SettingsButton(
128+
stringResource(R.string.traffic_units),
129+
stringResource(
130+
R.string.traffic_units_description,
131+
stringResource(
132+
if (trafficUnitSystem == TrafficUnitSystem.IEC) R.string.traffic_units_iec
133+
else R.string.traffic_units_si,
134+
),
135+
),
136+
) { showTrafficUnitsDialog = true }
125137
Text(stringResource(R.string.support), style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(top = 8.dp))
126138
SettingsButton(stringResource(R.string.feedback), stringResource(R.string.feedback_description)) {
127139
scope.launch {
@@ -188,6 +200,44 @@ internal fun SettingsHomeScreen(activity: Activity, onBack: () -> Unit, onNaviga
188200
dismissButton = { TextButton(onClick = { showLanguageDialog = false }) { Text(stringResource(android.R.string.cancel)) } },
189201
)
190202
}
203+
204+
if (showTrafficUnitsDialog) {
205+
AlertDialog(
206+
onDismissRequest = { showTrafficUnitsDialog = false },
207+
title = { Text(stringResource(R.string.traffic_units)) },
208+
text = {
209+
Column {
210+
TrafficUnitSystem.entries.forEach { unitSystem ->
211+
val label = stringResource(
212+
if (unitSystem == TrafficUnitSystem.IEC) R.string.traffic_units_iec
213+
else R.string.traffic_units_si,
214+
)
215+
Row(
216+
Modifier.fillMaxWidth().heightIn(min = 56.dp).toggleable(
217+
value = trafficUnitSystem == unitSystem,
218+
role = Role.RadioButton,
219+
onValueChange = {
220+
trafficUnitSystem = unitSystem
221+
TrafficUnitPreferences.set(activity, unitSystem)
222+
showTrafficUnitsDialog = false
223+
},
224+
),
225+
verticalAlignment = Alignment.CenterVertically,
226+
) {
227+
androidx.compose.material3.RadioButton(trafficUnitSystem == unitSystem, null)
228+
Text(label)
229+
}
230+
}
231+
}
232+
},
233+
confirmButton = {},
234+
dismissButton = {
235+
TextButton(onClick = { showTrafficUnitsDialog = false }) {
236+
Text(stringResource(android.R.string.cancel))
237+
}
238+
},
239+
)
240+
}
191241
}
192242

193243
@OptIn(ExperimentalMaterial3Api::class)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package net.megaproxy487
2+
3+
import java.util.Locale
4+
5+
internal enum class TrafficUnitSystem {
6+
IEC,
7+
SI,
8+
}
9+
10+
internal fun formatTrafficBytes(
11+
bytes: Long,
12+
unitSystem: TrafficUnitSystem = TrafficUnitSystem.IEC,
13+
locale: Locale = Locale.getDefault(),
14+
): String {
15+
val base = if (unitSystem == TrafficUnitSystem.IEC) 1_024 else 1_000
16+
val units = if (unitSystem == TrafficUnitSystem.IEC) {
17+
arrayOf("B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB")
18+
} else {
19+
arrayOf("B", "KB", "MB", "GB", "TB", "PB", "EB")
20+
}
21+
var value = bytes.coerceAtLeast(0).toDouble()
22+
var unitIndex = 0
23+
while (value >= base && unitIndex < units.lastIndex) {
24+
value /= base
25+
unitIndex++
26+
}
27+
return if (unitIndex == 0) {
28+
"${value.toLong()} ${units[unitIndex]}"
29+
} else {
30+
String.format(locale, "%.1f %s", value, units[unitIndex])
31+
}
32+
}
33+
34+
internal fun formatTrafficRate(
35+
bytesPerSecond: Double,
36+
unitSystem: TrafficUnitSystem = TrafficUnitSystem.IEC,
37+
locale: Locale = Locale.getDefault(),
38+
): String = "${formatTrafficBytes(bytesPerSecond.coerceAtLeast(0.0).toLong(), unitSystem, locale)}/s"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package net.megaproxy487
2+
3+
import android.content.Context
4+
5+
internal object TrafficUnitPreferences {
6+
private const val PREFERENCES = "display_preferences"
7+
private const val KEY_TRAFFIC_UNITS = "traffic_units"
8+
9+
fun current(context: Context): TrafficUnitSystem {
10+
val stored = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
11+
.getString(KEY_TRAFFIC_UNITS, null)
12+
return TrafficUnitSystem.entries.firstOrNull { it.name == stored } ?: TrafficUnitSystem.IEC
13+
}
14+
15+
fun set(context: Context, unitSystem: TrafficUnitSystem) {
16+
context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
17+
.edit()
18+
.putString(KEY_TRAFFIC_UNITS, unitSystem.name)
19+
.apply()
20+
}
21+
}

‎app/src/main/res/values-ru/strings.xml‎

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,8 @@
161161
<string name="bypass_local_networks">Обходить локальные сети</string>
162162
<string name="bypass_local_networks_description">Прямой TCP-доступ к частным и локальным IP-адресам.</string>
163163
<string name="search_applications">Поиск приложений</string>
164-
<string name="rotated_log_limit">Размер журнала с ротацией (МБ)</string>
165-
<string name="rotated_log_limit_description">Общий размер двух сегментов журнала: 1–100 МБ.</string>
164+
<string name="rotated_log_limit">Размер журнала с ротацией (MiB)</string>
165+
<string name="rotated_log_limit_description">Общий размер двух сегментов журнала: 1–100 MiB.</string>
166166
<string name="no_diagnostic_events">Диагностических событий пока нет.</string>
167167
<string name="clear_diagnostic_log_title">Очистить диагностический журнал?</string>
168168
<string name="clear_diagnostic_log_message">Оба сегмента журнала будут удалены.</string>
@@ -216,6 +216,35 @@
216216
<string name="megaproxy_active">MegaProxy активен</string><string name="review_ssh_key">Проверить ключ SSH</string>
217217
<string name="always_on_all_failover_status">Постоянный VPN остаётся активным при автоматическом переключении. Каждое переключение отображается в постоянном уведомлении и на главном экране.</string>
218218
<string name="always_on_selected_failover_status">Постоянный VPN остаётся активным при автоматическом переключении. В уведомлении и на главном экране отображается фактически используемый профиль.</string>
219-
<string name="ssh_keepalive">Интервал keepalive SSH, секунд (0 — отключено)</string><string name="ssh_max_channels">Максимум параллельных каналов SSH</string><string name="ssh_rotation_minutes">Сменить SSH-сессию через минут (0 — отключено)</string><string name="ssh_rotation_mb">Сменить SSH-сессию после МБ (0 — отключено)</string>
219+
<string name="ssh_keepalive">Интервал keepalive SSH, секунд (0 — отключено)</string><string name="ssh_max_channels">Максимум параллельных каналов SSH</string><string name="ssh_rotation_minutes">Сменить SSH-сессию через минут (0 — отключено)</string><string name="ssh_rotation_mb">Сменить SSH-сессию после MiB (0 — отключено)</string>
220220
<string name="could_not_open_email">Не удалось открыть почтовое приложение</string><string name="invalid_ja3">JA3 должен содержать пять полей: версия,шифры,расширения,группы,точки</string>
221+
<string name="traffic_download">Получено</string>
222+
<string name="traffic_upload">Отправлено</string>
223+
<string name="traffic_total">Всего: %1$s</string>
224+
<string name="traffic_units">Единицы трафика</string>
225+
<string name="traffic_units_description">Единицы скорости и суммарного трафика · %1$s</string>
226+
<string name="traffic_units_iec">Двоичные IEC (KiB, MiB, GiB · 1024)</string>
227+
<string name="traffic_units_si">Десятичные SI (KB, MB, GB · 1000)</string>
228+
<string name="proxy_latency">Задержка прокси</string>
229+
<string name="latency_milliseconds">%1$d мс%2$s</string>
230+
<string name="connection_errors_no_samples">Ошибки соединения: нет данных</string>
231+
<string name="connection_errors">Ошибки соединения: %1$.1f%% · %2$s</string>
232+
<plurals name="connection_samples">
233+
<item quantity="one">%1$d попытка</item>
234+
<item quantity="few">%1$d попытки</item>
235+
<item quantity="many">%1$d попыток</item>
236+
<item quantity="other">%1$d попытки</item>
237+
</plurals>
238+
<plurals name="latency_seconds_ago">
239+
<item quantity="one"> · %1$d секунду назад</item>
240+
<item quantity="few"> · %1$d секунды назад</item>
241+
<item quantity="many"> · %1$d секунд назад</item>
242+
<item quantity="other"> · %1$d секунды назад</item>
243+
</plurals>
244+
<plurals name="latency_minutes_ago">
245+
<item quantity="one"> · %1$d минуту назад</item>
246+
<item quantity="few"> · %1$d минуты назад</item>
247+
<item quantity="many"> · %1$d минут назад</item>
248+
<item quantity="other"> · %1$d минуты назад</item>
249+
</plurals>
221250
</resources>

‎app/src/main/res/values/strings.xml‎

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,8 @@
162162
<string name="bypass_local_networks">Bypass local networks</string>
163163
<string name="bypass_local_networks_description">Direct TCP access to private and link-local IP addresses.</string>
164164
<string name="search_applications">Search applications</string>
165-
<string name="rotated_log_limit">Rotated log limit (MB)</string>
166-
<string name="rotated_log_limit_description">Total size across both log segments; 1–100 MB.</string>
165+
<string name="rotated_log_limit">Rotated log limit (MiB)</string>
166+
<string name="rotated_log_limit_description">Total size across both log segments; 1–100 MiB.</string>
167167
<string name="no_diagnostic_events">No diagnostic events yet.</string>
168168
<string name="clear_diagnostic_log_title">Clear diagnostic log?</string>
169169
<string name="clear_diagnostic_log_message">Both rotated log segments will be deleted.</string>
@@ -217,6 +217,29 @@
217217
<string name="megaproxy_active">MegaProxy is active</string><string name="review_ssh_key">Review SSH key</string>
218218
<string name="always_on_all_failover_status">Always-on VPN remains active during automatic switching. Every switch is reported in the persistent notification and on the main screen.</string>
219219
<string name="always_on_selected_failover_status">Always-on VPN remains active during automatic switching. The persistent notification and the main screen show the profile actually in use.</string>
220-
<string name="ssh_keepalive">SSH keepalive, seconds (0 = disabled)</string><string name="ssh_max_channels">Maximum parallel SSH channels</string><string name="ssh_rotation_minutes">Rotate SSH session after minutes (0 = disabled)</string><string name="ssh_rotation_mb">Rotate SSH session after MB (0 = disabled)</string>
220+
<string name="ssh_keepalive">SSH keepalive, seconds (0 = disabled)</string><string name="ssh_max_channels">Maximum parallel SSH channels</string><string name="ssh_rotation_minutes">Rotate SSH session after minutes (0 = disabled)</string><string name="ssh_rotation_mb">Rotate SSH session after MiB (0 = disabled)</string>
221221
<string name="could_not_open_email">Could not open an email client</string><string name="invalid_ja3">JA3 must contain five fields: version,ciphers,extensions,groups,points</string>
222+
<string name="traffic_download">Download</string>
223+
<string name="traffic_upload">Upload</string>
224+
<string name="traffic_total">Total: %1$s</string>
225+
<string name="traffic_units">Traffic units</string>
226+
<string name="traffic_units_description">Units used for speed and total traffic · %1$s</string>
227+
<string name="traffic_units_iec">IEC binary (KiB, MiB, GiB · 1024)</string>
228+
<string name="traffic_units_si">SI decimal (KB, MB, GB · 1000)</string>
229+
<string name="proxy_latency">Proxy latency</string>
230+
<string name="latency_milliseconds">%1$d ms%2$s</string>
231+
<string name="connection_errors_no_samples">Connection errors: no samples</string>
232+
<string name="connection_errors">Connection errors: %1$.1f%% · %2$s</string>
233+
<plurals name="connection_samples">
234+
<item quantity="one">%1$d sample</item>
235+
<item quantity="other">%1$d samples</item>
236+
</plurals>
237+
<plurals name="latency_seconds_ago">
238+
<item quantity="one"> · %1$d sec ago</item>
239+
<item quantity="other"> · %1$d sec ago</item>
240+
</plurals>
241+
<plurals name="latency_minutes_ago">
242+
<item quantity="one"> · %1$d min ago</item>
243+
<item quantity="other"> · %1$d min ago</item>
244+
</plurals>
222245
</resources>

0 commit comments

Comments
 (0)