diff options
7 files changed, 178 insertions, 23 deletions
diff --git a/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java b/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java index c8158a72..5d544f56 100644 --- a/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java +++ b/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java @@ -49,6 +49,8 @@ import io.grpc.ManagedChannelBuilder; import io.grpc.okhttp.OkHttpChannelBuilder; import io.grpc.stub.StreamObserver; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; @@ -179,10 +181,17 @@ public final class GoBackend implements Backend { Key key = null; long rx = 0; long tx = 0; + long handshakeSec = 0; + int handshakeNSec = 0; for (final String line : config.split("\\n")) { if (line.startsWith("public_key=")) { - if (key != null) - stats.add(key, rx, tx); + if (key != null) { + LocalDateTime handshake = null; + if (handshakeSec > 0) { + handshake = LocalDateTime.ofEpochSecond(handshakeSec, handshakeNSec, ZoneOffset.UTC); + } + stats.add(key, rx, tx, handshake); + } rx = 0; tx = 0; try { @@ -206,10 +215,31 @@ public final class GoBackend implements Backend { } catch (final NumberFormatException ignored) { tx = 0; } + } else if (line.startsWith("last_handshake_time_sec=")) { + if (key == null) + continue; + try { + handshakeSec = Long.parseLong(line.substring(24)); + } catch (final NumberFormatException ignored) { + handshakeSec = 0; + } + } else if (line.startsWith("last_handshake_time_nsec=")) { + if (key == null) + continue; + try { + handshakeNSec = Integer.parseInt(line.substring(25)); + } catch (final NumberFormatException ignored) { + handshakeNSec = 0; + } + } + } + if (key != null) { + LocalDateTime handshake = null; + if (handshakeSec > 0) { + handshake = LocalDateTime.ofEpochSecond(handshakeSec, handshakeNSec, ZoneOffset.UTC); } + stats.add(key, rx, tx, handshake); } - if (key != null) - stats.add(key, rx, tx); return stats; } diff --git a/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java b/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java index 5d658019..322e766f 100644 --- a/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java +++ b/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java @@ -11,6 +11,7 @@ import android.util.Pair; import com.wireguard.crypto.Key; import com.wireguard.util.NonNullForAll; +import java.time.LocalDateTime; import java.util.HashMap; import java.util.Map; @@ -19,7 +20,19 @@ import java.util.Map; */ @NonNullForAll public class Statistics { - private final Map<Key, Pair<Long, Long>> peerBytes = new HashMap<>(); + private static class Stat { + long rx; + long tx; + LocalDateTime lastHandshake; + + Stat(long rx, long tx, LocalDateTime lastHandshake) { + this.rx = rx; + this.tx = tx; + this.lastHandshake = lastHandshake; + } + } + + private final Map<Key, Stat> peerBytes = new HashMap<>(); private long lastTouched = SystemClock.elapsedRealtime(); Statistics() { @@ -34,8 +47,8 @@ public class Statistics { * @param tx The transmitted traffic for the {@link com.wireguard.config.Peer} referenced by * the provided {@link Key}. This value is in bytes. */ - void add(final Key key, final long rx, final long tx) { - peerBytes.put(key, Pair.create(rx, tx)); + void add(final Key key, final long rx, final long tx, final LocalDateTime lastHandshake) { + peerBytes.put(key, new Stat(rx, tx, lastHandshake)); lastTouched = SystemClock.elapsedRealtime(); } @@ -56,10 +69,10 @@ public class Statistics { * @return a long representing the number of bytes received by this peer. */ public long peerRx(final Key peer) { - final Pair<Long, Long> rxTx = peerBytes.get(peer); - if (rxTx == null) + final Stat stat = peerBytes.get(peer); + if (stat == null) return 0; - return rxTx.first; + return stat.rx; } /** @@ -70,10 +83,17 @@ public class Statistics { * @return a long representing the number of bytes transmitted by this peer. */ public long peerTx(final Key peer) { - final Pair<Long, Long> rxTx = peerBytes.get(peer); - if (rxTx == null) + final Stat stat = peerBytes.get(peer); + if (stat == null) return 0; - return rxTx.second; + return stat.tx; + } + + public LocalDateTime peerLastHandshake(final Key peer) { + final Stat stat = peerBytes.get(peer); + if (stat == null) + return null; + return stat.lastHandshake; } /** @@ -93,8 +113,8 @@ public class Statistics { */ public long totalRx() { long rx = 0; - for (final Pair<Long, Long> val : peerBytes.values()) { - rx += val.first; + for (final Stat val : peerBytes.values()) { + rx += val.rx; } return rx; } @@ -106,8 +126,8 @@ public class Statistics { */ public long totalTx() { long tx = 0; - for (final Pair<Long, Long> val : peerBytes.values()) { - tx += val.second; + for (final Stat val : peerBytes.values()) { + tx += val.tx; } return tx; } diff --git a/tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java b/tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java index 3121c996..d0dc6a46 100644 --- a/tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java +++ b/tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java @@ -93,7 +93,7 @@ public final class WgQuickBackend implements Backend { if (parts.length != 3) continue; try { - stats.add(Key.fromBase64(parts[0]), Long.parseLong(parts[1]), Long.parseLong(parts[2])); + stats.add(Key.fromBase64(parts[0]), Long.parseLong(parts[1]), Long.parseLong(parts[2]), null); } catch (final Exception ignored) { } } diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt index 7046cb96..fad1b5d4 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt @@ -20,6 +20,9 @@ import com.wireguard.android.model.ObservableTunnel import com.wireguard.android.util.QuantityFormatter import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import java.time.Duration +import java.time.LocalDateTime +import java.time.ZoneId /** * Fragment that shows details about a specific tunnel. @@ -99,6 +102,7 @@ class TunnelDetailFragment : BaseFragment() { val state = tunnel.state if (state != Tunnel.State.UP && lastState == state) return lastState = state + var now = LocalDateTime.now(ZoneId.of("UTC")) try { val statistics = tunnel.getStatisticsAsync() for (i in 0 until binding.peersLayout.childCount) { @@ -110,11 +114,20 @@ class TunnelDetailFragment : BaseFragment() { if (rx == 0L && tx == 0L) { peer.transferLabel.visibility = View.GONE peer.transferText.visibility = View.GONE - continue + } else { + peer.transferText.text = getString(R.string.transfer_rx_tx, QuantityFormatter.formatBytes(rx), QuantityFormatter.formatBytes(tx)) + peer.transferLabel.visibility = View.VISIBLE + peer.transferText.visibility = View.VISIBLE + } + val lastHandshake:LocalDateTime? = statistics.peerLastHandshake(publicKey) + if (lastHandshake == null) { + peer.lastHandshakeLabel.visibility = View.GONE + peer.lastHandshakeText.visibility = View.GONE + } else { + peer.lastHandshakeText.text = getString(R.string.last_handshake_ago, QuantityFormatter.formatDuration(Duration.between(lastHandshake, now))) + peer.lastHandshakeLabel.visibility = View.VISIBLE + peer.lastHandshakeText.visibility = View.VISIBLE } - peer.transferText.text = getString(R.string.transfer_rx_tx, QuantityFormatter.formatBytes(rx), QuantityFormatter.formatBytes(tx)) - peer.transferLabel.visibility = View.VISIBLE - peer.transferText.visibility = View.VISIBLE } } catch (e: Throwable) { for (i in 0 until binding.peersLayout.childCount) { @@ -122,6 +135,8 @@ class TunnelDetailFragment : BaseFragment() { ?: continue peer.transferLabel.visibility = View.GONE peer.transferText.visibility = View.GONE + peer.lastHandshakeLabel.visibility = View.GONE + peer.lastHandshakeText.visibility = View.GONE } } } diff --git a/ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt b/ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt index 4a9ffed4..cd2adbcb 100644 --- a/ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt +++ b/ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt @@ -8,6 +8,8 @@ package com.wireguard.android.util import com.wireguard.android.Application import com.wireguard.android.R +import java.time.Duration + object QuantityFormatter { fun formatBytes(bytes: Long): String { val context = Application.get().applicationContext @@ -19,4 +21,58 @@ object QuantityFormatter { else -> context.getString(R.string.transfer_tibibytes, bytes / (1024.0 * 1024.0 * 1024.0) / 1024.0) } } -}
\ No newline at end of file + + fun formatDuration(duration: Duration): String { + val context = Application.get().applicationContext + val str = formatHours(duration.getSeconds()) + return when { + str != "" -> str + else -> context.getString(R.string.duration_seconds, 0) + } + } + + fun formatHours(seconds: Long): String { + val context = Application.get().applicationContext + val hours = seconds / 3600 + val restSeconds = seconds % 3600 + val str = formatMinutes(restSeconds) + + return when { + hours > 0 -> context.getString(R.string.duration_hours, hours) + if (str != "") ", " + str else "" + else -> str + } + } + + fun formatDays(seconds: Long): String { + val context = Application.get().applicationContext + val days = seconds / 3600 / 24 + val restSeconds = seconds % (3600 * 24) + val str = formatHours(restSeconds) + + return when { + days > 0 -> context.getString(R.string.duration_days, days) + if (str != "") ", " + str else "" + else -> str + } + } + + fun formatMinutes(seconds: Long): String { + val context = Application.get().applicationContext + val minutes = seconds / 60 + val restSeconds = seconds % 60 + val str = formatSeconds(restSeconds) + + return when { + minutes > 0 -> context.getString(R.string.duration_minutes, minutes) + if (str != "") ", " + str else "" + else -> str + } + } + + fun formatSeconds(seconds: Long): String { + val context = Application.get().applicationContext + + return when { + seconds > 0 -> context.getString(R.string.duration_seconds, seconds) + else -> "" + } + } +} diff --git a/ui/src/main/res/layout/tunnel_detail_peer.xml b/ui/src/main/res/layout/tunnel_detail_peer.xml index 0fbee8f1..f295bb13 100644 --- a/ui/src/main/res/layout/tunnel_detail_peer.xml +++ b/ui/src/main/res/layout/tunnel_detail_peer.xml @@ -194,6 +194,34 @@ app:layout_constraintTop_toBottomOf="@+id/transfer_label" tools:text="1024 MB" tools:visibility="visible" /> + + <TextView + android:id="@+id/last_handshake_label" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@+id/persistent_keepalive_text" + android:layout_marginTop="8dp" + android:labelFor="@+id/last_handshake_text" + android:text="@string/last_handshake" + android:visibility="gone" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/transfer_text" + tools:visibility="visible" /> + + <TextView + android:id="@+id/last_handshake_text" + style="@style/DetailText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@+id/last_handshake_label" + android:contentDescription="@string/last_handshake" + android:nextFocusUp="@id/transfer_text" + android:onClick="@{ClipboardUtils::copyTextView}" + android:visibility="gone" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/last_handshake_label" + tools:text="29 seconds ago" + tools:visibility="visible" /> </androidx.constraintlayout.widget.ConstraintLayout> </com.google.android.material.card.MaterialCardView> </layout> diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index c10c4ea7..3b32ab79 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -113,6 +113,10 @@ <string name="disable_config_export_description">Disabling config exporting makes private keys less accessible</string> <string name="dns_servers">DNS servers</string> <string name="dns_search_domains">Search domains</string> + <string name="duration_days">%d days</string> + <string name="duration_hours">%d hours</string> + <string name="duration_minutes">%d minutes</string> + <string name="duration_seconds">%d seconds</string> <string name="edit">Edit</string> <string name="endpoint">Endpoint</string> <string name="error_down">Error bringing down tunnel: %s</string> @@ -141,6 +145,8 @@ <string name="key_length_explanation_base64">: WireGuard base64 keys must be 44 characters (32 bytes)</string> <string name="key_length_explanation_binary">: WireGuard keys must be 32 bytes</string> <string name="key_length_explanation_hex">: WireGuard hex keys must be 64 characters (32 bytes)</string> + <string name="last_handshake">Latest handshake</string> + <string name="last_handshake_ago">%s ago</string> <string name="listen_port">Listen port</string> <string name="log_export_error">Unable to export log: %s</string> <string name="log_export_subject">WireGuard Android Log File</string> |