Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 70 additions & 19 deletions app/src/main/java/net/megaproxy487/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics
Expand Down Expand Up @@ -663,23 +665,51 @@ private data class RefreshedMainConfig(

@Composable
private fun ConnectionStatsCard(stats: DisplayedConnectionStats) {
val context = androidx.compose.ui.platform.LocalContext.current
val unitSystem = TrafficUnitPreferences.current(context)
Card(Modifier.fillMaxWidth()) {
Row(
Modifier.fillMaxWidth().padding(14.dp),
horizontalArrangement = Arrangement.SpaceBetween,
horizontalArrangement = Arrangement.SpaceEvenly,
) {
StatValue("Download", "↓ ${formatRate(stats.downloadBytesPerSecond)}")
StatValue("Upload", "↑ ${formatRate(stats.uploadBytesPerSecond)}")
StatValue(
label = stringResource(R.string.traffic_download),
value = "↓ ${formatTrafficRate(stats.downloadBytesPerSecond, unitSystem)}",
supportingValue = stringResource(
R.string.traffic_total,
formatTrafficBytes(stats.native.downloadBytes, unitSystem),
),
modifier = Modifier.weight(1f),
)
StatValue(
label = stringResource(R.string.traffic_upload),
value = "↑ ${formatTrafficRate(stats.uploadBytesPerSecond, unitSystem)}",
supportingValue = stringResource(
R.string.traffic_total,
formatTrafficBytes(stats.native.uploadBytes, unitSystem),
),
modifier = Modifier.weight(1f),
)
val latency = stats.native.proxyLatencyMillis
val ageMillis = System.currentTimeMillis() - stats.native.proxyLatencyAtMillis
StatValue(
"Proxy latency",
if (latency <= 0) "—" else "${latency.toInt()} ms${formatAge(ageMillis)}",
label = stringResource(R.string.proxy_latency),
value = if (latency <= 0) "—" else stringResource(
R.string.latency_milliseconds,
latency.toInt(),
formatAge(ageMillis),
),
modifier = Modifier.weight(1f),
)
}
val samples = stats.native.connectionSamples
Text(
if (stats.native.connectionSamples == 0) "Connection errors: no samples"
else "Connection errors: ${"%.1f".format(stats.native.connectionErrorRate * 100)}% · ${stats.native.connectionSamples} samples",
if (samples == 0) stringResource(R.string.connection_errors_no_samples)
else stringResource(
R.string.connection_errors,
stats.native.connectionErrorRate * 100,
pluralStringResource(R.plurals.connection_samples, samples, samples),
),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 14.dp, end = 14.dp, bottom = 12.dp),
Expand All @@ -688,21 +718,42 @@ private fun ConnectionStatsCard(stats: DisplayedConnectionStats) {
}

@Composable
private fun StatValue(label: String, value: String) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(value, style = MaterialTheme.typography.titleSmall)
Text(label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
private fun StatValue(
label: String,
value: String,
modifier: Modifier = Modifier,
supportingValue: String? = null,
) {
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
Text(value, style = MaterialTheme.typography.titleSmall, textAlign = TextAlign.Center)
Text(
label,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
supportingValue?.let {
Text(
it,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
}
}
}

private fun formatRate(bytesPerSecond: Double): String = when {
bytesPerSecond >= 1_000_000 -> "%.1f MB/s".format(bytesPerSecond / 1_000_000)
bytesPerSecond >= 1_000 -> "%.1f KB/s".format(bytesPerSecond / 1_000)
else -> "${bytesPerSecond.toInt()} B/s"
}

@Composable
private fun formatAge(ageMillis: Long): String = when {
ageMillis < 30_000 -> ""
ageMillis < 120_000 -> " · ${ageMillis / 1_000}s ago"
else -> " · ${ageMillis / 60_000}m ago"
ageMillis < 120_000 -> pluralStringResource(
R.plurals.latency_seconds_ago,
(ageMillis / 1_000).toInt(),
(ageMillis / 1_000).toInt(),
)
else -> pluralStringResource(
R.plurals.latency_minutes_ago,
(ageMillis / 60_000).toInt(),
(ageMillis / 60_000).toInt(),
)
}
50 changes: 50 additions & 0 deletions app/src/main/java/net/megaproxy487/SettingsScreens.kt
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ internal fun SettingsHomeScreen(activity: Activity, onBack: () -> Unit, onNaviga
mutableStateOf(powerManager.isIgnoringBatteryOptimizations(activity.packageName))
}
var showLanguageDialog by remember { mutableStateOf(false) }
var showTrafficUnitsDialog by remember { mutableStateOf(false) }
var trafficUnitSystem by remember { mutableStateOf(TrafficUnitPreferences.current(activity)) }
var supportError by remember { mutableStateOf<String?>(null) }
val scope = rememberCoroutineScope()

Expand Down Expand Up @@ -122,6 +124,16 @@ internal fun SettingsHomeScreen(activity: Activity, onBack: () -> Unit, onNaviga
stringResource(R.string.language),
"${stringResource(R.string.language_description)} · ${if (AppLanguageManager.current(activity) == AppLanguage.RUSSIAN) stringResource(R.string.language_russian) else stringResource(R.string.language_english)}",
) { showLanguageDialog = true }
SettingsButton(
stringResource(R.string.traffic_units),
stringResource(
R.string.traffic_units_description,
stringResource(
if (trafficUnitSystem == TrafficUnitSystem.IEC) R.string.traffic_units_iec
else R.string.traffic_units_si,
),
),
) { showTrafficUnitsDialog = true }
Text(stringResource(R.string.support), style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(top = 8.dp))
SettingsButton(stringResource(R.string.feedback), stringResource(R.string.feedback_description)) {
scope.launch {
Expand Down Expand Up @@ -188,6 +200,44 @@ internal fun SettingsHomeScreen(activity: Activity, onBack: () -> Unit, onNaviga
dismissButton = { TextButton(onClick = { showLanguageDialog = false }) { Text(stringResource(android.R.string.cancel)) } },
)
}

if (showTrafficUnitsDialog) {
AlertDialog(
onDismissRequest = { showTrafficUnitsDialog = false },
title = { Text(stringResource(R.string.traffic_units)) },
text = {
Column {
TrafficUnitSystem.entries.forEach { unitSystem ->
val label = stringResource(
if (unitSystem == TrafficUnitSystem.IEC) R.string.traffic_units_iec
else R.string.traffic_units_si,
)
Row(
Modifier.fillMaxWidth().heightIn(min = 56.dp).toggleable(
value = trafficUnitSystem == unitSystem,
role = Role.RadioButton,
onValueChange = {
trafficUnitSystem = unitSystem
TrafficUnitPreferences.set(activity, unitSystem)
showTrafficUnitsDialog = false
},
),
verticalAlignment = Alignment.CenterVertically,
) {
androidx.compose.material3.RadioButton(trafficUnitSystem == unitSystem, null)
Text(label)
}
}
}
},
confirmButton = {},
dismissButton = {
TextButton(onClick = { showTrafficUnitsDialog = false }) {
Text(stringResource(android.R.string.cancel))
}
},
)
}
}

@OptIn(ExperimentalMaterial3Api::class)
Expand Down
38 changes: 38 additions & 0 deletions app/src/main/java/net/megaproxy487/TrafficFormatting.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package net.megaproxy487

import java.util.Locale

internal enum class TrafficUnitSystem {
IEC,
SI,
}

internal fun formatTrafficBytes(
bytes: Long,
unitSystem: TrafficUnitSystem = TrafficUnitSystem.IEC,
locale: Locale = Locale.getDefault(),
): String {
val base = if (unitSystem == TrafficUnitSystem.IEC) 1_024 else 1_000
val units = if (unitSystem == TrafficUnitSystem.IEC) {
arrayOf("B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB")
} else {
arrayOf("B", "KB", "MB", "GB", "TB", "PB", "EB")
}
var value = bytes.coerceAtLeast(0).toDouble()
var unitIndex = 0
while (value >= base && unitIndex < units.lastIndex) {
value /= base
unitIndex++
}
return if (unitIndex == 0) {
"${value.toLong()} ${units[unitIndex]}"
} else {
String.format(locale, "%.1f %s", value, units[unitIndex])
}
}

internal fun formatTrafficRate(
bytesPerSecond: Double,
unitSystem: TrafficUnitSystem = TrafficUnitSystem.IEC,
locale: Locale = Locale.getDefault(),
): String = "${formatTrafficBytes(bytesPerSecond.coerceAtLeast(0.0).toLong(), unitSystem, locale)}/s"
21 changes: 21 additions & 0 deletions app/src/main/java/net/megaproxy487/TrafficUnitPreferences.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package net.megaproxy487

import android.content.Context

internal object TrafficUnitPreferences {
private const val PREFERENCES = "display_preferences"
private const val KEY_TRAFFIC_UNITS = "traffic_units"

fun current(context: Context): TrafficUnitSystem {
val stored = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
.getString(KEY_TRAFFIC_UNITS, null)
return TrafficUnitSystem.entries.firstOrNull { it.name == stored } ?: TrafficUnitSystem.IEC
}

fun set(context: Context, unitSystem: TrafficUnitSystem) {
context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
.edit()
.putString(KEY_TRAFFIC_UNITS, unitSystem.name)
.apply()
}
}
35 changes: 32 additions & 3 deletions app/src/main/res/values-ru/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@
<string name="bypass_local_networks">Обходить локальные сети</string>
<string name="bypass_local_networks_description">Прямой TCP-доступ к частным и локальным IP-адресам.</string>
<string name="search_applications">Поиск приложений</string>
<string name="rotated_log_limit">Размер журнала с ротацией (МБ)</string>
<string name="rotated_log_limit_description">Общий размер двух сегментов журнала: 1–100 МБ.</string>
<string name="rotated_log_limit">Размер журнала с ротацией (MiB)</string>
<string name="rotated_log_limit_description">Общий размер двух сегментов журнала: 1–100 MiB.</string>
<string name="no_diagnostic_events">Диагностических событий пока нет.</string>
<string name="clear_diagnostic_log_title">Очистить диагностический журнал?</string>
<string name="clear_diagnostic_log_message">Оба сегмента журнала будут удалены.</string>
Expand Down Expand Up @@ -216,6 +216,35 @@
<string name="megaproxy_active">MegaProxy активен</string><string name="review_ssh_key">Проверить ключ SSH</string>
<string name="always_on_all_failover_status">Постоянный VPN остаётся активным при автоматическом переключении. Каждое переключение отображается в постоянном уведомлении и на главном экране.</string>
<string name="always_on_selected_failover_status">Постоянный VPN остаётся активным при автоматическом переключении. В уведомлении и на главном экране отображается фактически используемый профиль.</string>
<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>
<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>
<string name="could_not_open_email">Не удалось открыть почтовое приложение</string><string name="invalid_ja3">JA3 должен содержать пять полей: версия,шифры,расширения,группы,точки</string>
<string name="traffic_download">Получено</string>
<string name="traffic_upload">Отправлено</string>
<string name="traffic_total">Всего: %1$s</string>
<string name="traffic_units">Единицы трафика</string>
<string name="traffic_units_description">Единицы скорости и суммарного трафика · %1$s</string>
<string name="traffic_units_iec">Двоичные IEC (KiB, MiB, GiB · 1024)</string>
<string name="traffic_units_si">Десятичные SI (KB, MB, GB · 1000)</string>
<string name="proxy_latency">Задержка прокси</string>
<string name="latency_milliseconds">%1$d мс%2$s</string>
<string name="connection_errors_no_samples">Ошибки соединения: нет данных</string>
<string name="connection_errors">Ошибки соединения: %1$.1f%% · %2$s</string>
<plurals name="connection_samples">
<item quantity="one">%1$d попытка</item>
<item quantity="few">%1$d попытки</item>
<item quantity="many">%1$d попыток</item>
<item quantity="other">%1$d попытки</item>
</plurals>
<plurals name="latency_seconds_ago">
<item quantity="one"> · %1$d секунду назад</item>
<item quantity="few"> · %1$d секунды назад</item>
<item quantity="many"> · %1$d секунд назад</item>
<item quantity="other"> · %1$d секунды назад</item>
</plurals>
<plurals name="latency_minutes_ago">
<item quantity="one"> · %1$d минуту назад</item>
<item quantity="few"> · %1$d минуты назад</item>
<item quantity="many"> · %1$d минут назад</item>
<item quantity="other"> · %1$d минуты назад</item>
</plurals>
</resources>
29 changes: 26 additions & 3 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,8 @@
<string name="bypass_local_networks">Bypass local networks</string>
<string name="bypass_local_networks_description">Direct TCP access to private and link-local IP addresses.</string>
<string name="search_applications">Search applications</string>
<string name="rotated_log_limit">Rotated log limit (MB)</string>
<string name="rotated_log_limit_description">Total size across both log segments; 1–100 MB.</string>
<string name="rotated_log_limit">Rotated log limit (MiB)</string>
<string name="rotated_log_limit_description">Total size across both log segments; 1–100 MiB.</string>
<string name="no_diagnostic_events">No diagnostic events yet.</string>
<string name="clear_diagnostic_log_title">Clear diagnostic log?</string>
<string name="clear_diagnostic_log_message">Both rotated log segments will be deleted.</string>
Expand Down Expand Up @@ -217,6 +217,29 @@
<string name="megaproxy_active">MegaProxy is active</string><string name="review_ssh_key">Review SSH key</string>
<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>
<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>
<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>
<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>
<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>
<string name="traffic_download">Download</string>
<string name="traffic_upload">Upload</string>
<string name="traffic_total">Total: %1$s</string>
<string name="traffic_units">Traffic units</string>
<string name="traffic_units_description">Units used for speed and total traffic · %1$s</string>
<string name="traffic_units_iec">IEC binary (KiB, MiB, GiB · 1024)</string>
<string name="traffic_units_si">SI decimal (KB, MB, GB · 1000)</string>
<string name="proxy_latency">Proxy latency</string>
<string name="latency_milliseconds">%1$d ms%2$s</string>
<string name="connection_errors_no_samples">Connection errors: no samples</string>
<string name="connection_errors">Connection errors: %1$.1f%% · %2$s</string>
<plurals name="connection_samples">
<item quantity="one">%1$d sample</item>
<item quantity="other">%1$d samples</item>
</plurals>
<plurals name="latency_seconds_ago">
<item quantity="one"> · %1$d sec ago</item>
<item quantity="other"> · %1$d sec ago</item>
</plurals>
<plurals name="latency_minutes_ago">
<item quantity="one"> · %1$d min ago</item>
<item quantity="other"> · %1$d min ago</item>
</plurals>
</resources>
Loading