summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java38
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/Statistics.java46
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java2
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt23
-rw-r--r--ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt58
-rw-r--r--ui/src/main/res/layout/tunnel_detail_peer.xml28
-rw-r--r--ui/src/main/res/values/strings.xml6
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 cf98e106..968f2ade 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 35a472bd..ffec25f1 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 143a3ab4..123e5a45 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 8b155e24..2aec921a 100644
--- a/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt
+++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt
@@ -23,6 +23,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.
@@ -106,6 +109,7 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider {
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) {
@@ -117,11 +121,20 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider {
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) {
@@ -129,6 +142,8 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider {
?: 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 974fe530..f9abd590 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 85c55887..865c7f07 100644
--- a/ui/src/main/res/values/strings.xml
+++ b/ui/src/main/res/values/strings.xml
@@ -116,6 +116,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>
@@ -144,6 +148,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>