mirror of
https://github.com/amnezia-vpn/amneziawg-android.git
synced 2026-07-03 14:05:13 +02:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 99336f6552 | |||
| 0b7b65296b | |||
| e2f931b4cb | |||
| d6ad7d11d0 | |||
| 914917036e | |||
| 6e9ca435ca | |||
| 0797e99559 | |||
| 0ba064ff48 | |||
| b4fe9efadb | |||
| b1b08ce716 | |||
| 20480992c4 | |||
| 9df90b259a | |||
| b94a412c09 | |||
| 0b0a553123 | |||
| 6d4d2f4ebf | |||
| e55358f685 | |||
| c2fdd0ca93 | |||
| 19044b0f53 | |||
| d4e77e6d57 | |||
| 6ba3e4f001 | |||
| 8152191277 | |||
| 8c6c909e98 | |||
| fdda87fd31 | |||
| 3907f6a4c1 | |||
| 652971d746 | |||
| 1bf8d7e4c4 | |||
| 3c834ac8e3 | |||
| 8669ae78e1 | |||
| 53f8c8dbc4 | |||
| 1328660b0b |
+1
-1
@@ -14,5 +14,5 @@ build/
|
||||
*.dex
|
||||
*.iml
|
||||
*.jks
|
||||
keystore.properties
|
||||
gradlew.bat
|
||||
maint/
|
||||
|
||||
+4
-4
@@ -1,20 +1,20 @@
|
||||
buildscript {
|
||||
ext {
|
||||
activityVersion = '1.6.1'
|
||||
activityVersion = '1.7.1'
|
||||
annotationsVersion = '1.6.0'
|
||||
appcompatVersion = '1.6.1'
|
||||
biometricVersion = '1.1.0'
|
||||
collectionVersion = '1.2.0'
|
||||
constraintLayoutVersion = '2.1.4'
|
||||
coordinatorLayoutVersion = '1.2.0'
|
||||
coreKtxVersion = '1.10.0-rc01'
|
||||
coreKtxVersion = '1.10.0'
|
||||
coroutinesVersion = '1.6.4'
|
||||
datastoreVersion = '1.0.0'
|
||||
desugarVersion = '1.1.5'
|
||||
fragmentVersion = '1.5.5'
|
||||
fragmentVersion = '1.5.7'
|
||||
jsr305Version = '3.0.2'
|
||||
junitVersion = '4.13.2'
|
||||
lifecycleRuntimeKtxVersion = '2.6.0'
|
||||
lifecycleRuntimeKtxVersion = '2.6.1'
|
||||
materialComponentsVersion = '1.8.0'
|
||||
preferenceVersion = '1.2.0'
|
||||
zxingEmbeddedVersion = '4.3.0'
|
||||
|
||||
+2
-4
@@ -42,8 +42,6 @@ android.generateManifestClass=true
|
||||
android.experimental.enableSourceSetPathsMap=true
|
||||
# Use relative paths for better Gradle caching of library build tasks
|
||||
android.experimental.cacheCompileLibResources=true
|
||||
# Disable automatic creation of publishing components
|
||||
android.disableAutomaticComponentCreation=true
|
||||
|
||||
# Default Android build features
|
||||
# Disable BuildConfig generation by default
|
||||
@@ -61,8 +59,8 @@ android.library.defaults.buildfeatures.androidresources=false
|
||||
|
||||
# Suppress warnings for some features that aren't yet stabilized
|
||||
android.suppressUnsupportedOptionWarnings=android.keepWorkerActionServicesBetweenBuilds,\
|
||||
android.experimental.enableNewResourceShrinker,android.experimental.enableNewResourceShrinker.preciseShrinking,\
|
||||
android.enableAppCompileTimeRClass,android.experimental.enableSourceSetPathsMap,android.experimental.cacheCompileLibResources,\
|
||||
android.experimental.enableNewResourceShrinker.preciseShrinking,\
|
||||
android.enableAppCompileTimeRClass,\
|
||||
android.suppressUnsupportedOptionWarnings
|
||||
|
||||
# OSSRH sometimes struggles with slow deployments, so this makes Gradle
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
pluginManagement {
|
||||
def agpVersion = '7.4.2'
|
||||
def agpVersion = '8.0.0'
|
||||
def kotlinVersion = "1.8.0"
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.wireguard.crypto.KeyFormatException;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
@@ -125,12 +126,14 @@ public final class GoBackend implements Backend {
|
||||
Key key = null;
|
||||
long rx = 0;
|
||||
long tx = 0;
|
||||
long latestHandshakeMSec = 0;
|
||||
for (final String line : config.split("\\n")) {
|
||||
if (line.startsWith("public_key=")) {
|
||||
if (key != null)
|
||||
stats.add(key, rx, tx);
|
||||
stats.add(key, rx, tx, latestHandshakeMSec);
|
||||
rx = 0;
|
||||
tx = 0;
|
||||
latestHandshakeMSec = 0;
|
||||
try {
|
||||
key = Key.fromHex(line.substring(11));
|
||||
} catch (final KeyFormatException ignored) {
|
||||
@@ -152,10 +155,26 @@ 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 {
|
||||
latestHandshakeMSec += Long.parseLong(line.substring(24)) * 1000;
|
||||
} catch (final NumberFormatException ignored) {
|
||||
latestHandshakeMSec = 0;
|
||||
}
|
||||
} else if (line.startsWith("last_handshake_time_nsec=")) {
|
||||
if (key == null)
|
||||
continue;
|
||||
try {
|
||||
latestHandshakeMSec += Long.parseLong(line.substring(25)) / 1000000;
|
||||
} catch (final NumberFormatException ignored) {
|
||||
latestHandshakeMSec = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (key != null)
|
||||
stats.add(key, rx, tx);
|
||||
stats.add(key, rx, tx, latestHandshakeMSec);
|
||||
return stats;
|
||||
}
|
||||
|
||||
@@ -324,6 +343,9 @@ public final class GoBackend implements Backend {
|
||||
currentTunnelHandle = -1;
|
||||
currentConfig = null;
|
||||
wgTurnOff(handleToClose);
|
||||
try {
|
||||
vpnService.get(0, TimeUnit.NANOSECONDS).stopSelf();
|
||||
} catch (final TimeoutException ignored) { }
|
||||
}
|
||||
|
||||
tunnel.onStateChange(state);
|
||||
|
||||
@@ -6,36 +6,65 @@
|
||||
package com.wireguard.android.backend;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.wireguard.crypto.Key;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Class representing transfer statistics for a {@link Tunnel} instance.
|
||||
*/
|
||||
@NonNullForAll
|
||||
public class Statistics {
|
||||
private final Map<Key, Pair<Long, Long>> peerBytes = new HashMap<>();
|
||||
|
||||
// TODO: switch to Java Record class once R8 supports desugaring those.
|
||||
public final class PeerStats {
|
||||
public final long rxBytes, txBytes, latestHandshakeEpochMillis;
|
||||
|
||||
PeerStats(final long rxBytes, final long txBytes, final long latestHandshakeEpochMillis) {
|
||||
this.rxBytes = rxBytes;
|
||||
this.txBytes = txBytes;
|
||||
this.latestHandshakeEpochMillis = latestHandshakeEpochMillis;
|
||||
}
|
||||
|
||||
@Override public boolean equals(final Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
final PeerStats stats = (PeerStats) o;
|
||||
return rxBytes == stats.rxBytes && txBytes == stats.txBytes && latestHandshakeEpochMillis == stats.latestHandshakeEpochMillis;
|
||||
}
|
||||
|
||||
@Override public int hashCode() {
|
||||
return Objects.hash(rxBytes, txBytes, latestHandshakeEpochMillis);
|
||||
}
|
||||
}
|
||||
|
||||
private final Map<Key, PeerStats> stats = new HashMap<>();
|
||||
private long lastTouched = SystemClock.elapsedRealtime();
|
||||
|
||||
Statistics() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a peer and its current data usage to the internal map.
|
||||
* Add a peer and its current stats to the internal map.
|
||||
*
|
||||
* @param key A WireGuard public key bound to a particular peer
|
||||
* @param rx The received traffic for the {@link com.wireguard.config.Peer} referenced by
|
||||
* the provided {@link Key}. This value is in bytes
|
||||
* @param tx The transmitted traffic for the {@link com.wireguard.config.Peer} referenced by
|
||||
* the provided {@link Key}. This value is in bytes.
|
||||
* @param key A WireGuard public key bound to a particular peer
|
||||
* @param rxBytes The received traffic for the {@link com.wireguard.config.Peer} referenced by
|
||||
* the provided {@link Key}. This value is in bytes
|
||||
* @param txBytes The transmitted traffic for the {@link com.wireguard.config.Peer} referenced by
|
||||
* the provided {@link Key}. This value is in bytes.
|
||||
* @param latestHandshake The timestamp of the latest handshake for the {@link com.wireguard.config.Peer}
|
||||
* referenced by the provided {@link Key}. The value is in epoch milliseconds.
|
||||
*/
|
||||
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 rxBytes, final long txBytes, final long latestHandshake) {
|
||||
stats.put(key, new PeerStats(rxBytes, txBytes, latestHandshake));
|
||||
lastTouched = SystemClock.elapsedRealtime();
|
||||
}
|
||||
|
||||
@@ -49,31 +78,14 @@ public class Statistics {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the received traffic (in bytes) for the {@link com.wireguard.config.Peer} referenced by
|
||||
* the provided {@link Key}
|
||||
* Get the statistics for the {@link com.wireguard.config.Peer} referenced by the provided {@link Key}
|
||||
*
|
||||
* @param peer A {@link Key} representing a {@link com.wireguard.config.Peer}.
|
||||
* @return a long representing the number of bytes received by this peer.
|
||||
* @return a {@link PeerStats} representing various statistics about this peer.
|
||||
*/
|
||||
public long peerRx(final Key peer) {
|
||||
final Pair<Long, Long> rxTx = peerBytes.get(peer);
|
||||
if (rxTx == null)
|
||||
return 0;
|
||||
return rxTx.first;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the transmitted traffic (in bytes) for the {@link com.wireguard.config.Peer} referenced by
|
||||
* the provided {@link Key}
|
||||
*
|
||||
* @param peer A {@link Key} representing a {@link com.wireguard.config.Peer}.
|
||||
* @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)
|
||||
return 0;
|
||||
return rxTx.second;
|
||||
@Nullable
|
||||
public PeerStats peer(final Key peer) {
|
||||
return this.stats.get(peer);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,7 +95,7 @@ public class Statistics {
|
||||
* {@link com.wireguard.config.Peer}s
|
||||
*/
|
||||
public Key[] peers() {
|
||||
return peerBytes.keySet().toArray(new Key[0]);
|
||||
return stats.keySet().toArray(new Key[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,8 +105,8 @@ public class Statistics {
|
||||
*/
|
||||
public long totalRx() {
|
||||
long rx = 0;
|
||||
for (final Pair<Long, Long> val : peerBytes.values()) {
|
||||
rx += val.first;
|
||||
for (final PeerStats val : stats.values()) {
|
||||
rx += val.rxBytes;
|
||||
}
|
||||
return rx;
|
||||
}
|
||||
@@ -106,8 +118,8 @@ public class Statistics {
|
||||
*/
|
||||
public long totalTx() {
|
||||
long tx = 0;
|
||||
for (final Pair<Long, Long> val : peerBytes.values()) {
|
||||
tx += val.second;
|
||||
for (final PeerStats val : stats.values()) {
|
||||
tx += val.txBytes;
|
||||
}
|
||||
return tx;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.wireguard.util.NonNullForAll;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
@@ -83,17 +84,17 @@ public final class WgQuickBackend implements Backend {
|
||||
final Statistics stats = new Statistics();
|
||||
final Collection<String> output = new ArrayList<>();
|
||||
try {
|
||||
if (rootShell.run(output, String.format("wg show '%s' transfer", tunnel.getName())) != 0)
|
||||
if (rootShell.run(output, String.format("wg show '%s' dump", tunnel.getName())) != 0)
|
||||
return stats;
|
||||
} catch (final Exception ignored) {
|
||||
return stats;
|
||||
}
|
||||
for (final String line : output) {
|
||||
final String[] parts = line.split("\\t");
|
||||
if (parts.length != 3)
|
||||
if (parts.length != 8)
|
||||
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[5]), Long.parseLong(parts[6]), Long.parseLong(parts[4]) * 1000);
|
||||
} catch (final Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,16 @@ cmake_minimum_required(VERSION 3.4.1)
|
||||
project("WireGuard")
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
|
||||
add_link_options(LINKER:--build-id=none)
|
||||
add_compile_options(-Wall -Werror)
|
||||
|
||||
add_executable(libwg-quick.so wireguard-tools/src/wg-quick/android.c ndk-compat/compat.c)
|
||||
target_compile_options(libwg-quick.so PUBLIC -O3 -std=gnu11 -Wall -include ${CMAKE_CURRENT_SOURCE_DIR}/ndk-compat/compat.h -DWG_PACKAGE_NAME=\"${ANDROID_PACKAGE_NAME}\")
|
||||
target_compile_options(libwg-quick.so PUBLIC -std=gnu11 -include ${CMAKE_CURRENT_SOURCE_DIR}/ndk-compat/compat.h -DWG_PACKAGE_NAME=\"${ANDROID_PACKAGE_NAME}\")
|
||||
target_link_libraries(libwg-quick.so -ldl)
|
||||
|
||||
file(GLOB WG_SOURCES wireguard-tools/src/*.c ndk-compat/compat.c)
|
||||
add_executable(libwg.so ${WG_SOURCES})
|
||||
target_include_directories(libwg.so PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/uapi/linux/" "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/")
|
||||
target_compile_options(libwg.so PUBLIC -O3 -std=gnu11 -D_GNU_SOURCE -include ${CMAKE_CURRENT_SOURCE_DIR}/ndk-compat/compat.h -DHAVE_VISIBILITY_HIDDEN -DRUNSTATEDIR=\"/data/data/${ANDROID_PACKAGE_NAME}/cache\")
|
||||
target_compile_options(libwg.so PUBLIC -std=gnu11 -include ${CMAKE_CURRENT_SOURCE_DIR}/ndk-compat/compat.h -DRUNSTATEDIR=\"/data/data/${ANDROID_PACKAGE_NAME}/cache\")
|
||||
|
||||
add_custom_target(libwg-go.so WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/libwg-go" COMMENT "Building wireguard-go" VERBATIM COMMAND "${ANDROID_HOST_PREBUILTS}/bin/make"
|
||||
ANDROID_ARCH_NAME=${ANDROID_ARCH_NAME}
|
||||
@@ -31,7 +32,7 @@ add_custom_target(libwg-go.so WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/lib
|
||||
|
||||
# Strip unwanted ELF sections to prevent DT_FLAGS_1 warnings on old Android versions
|
||||
file(GLOB ELF_CLEANER_SOURCES elf-cleaner/*.c elf-cleaner/*.cpp)
|
||||
add_custom_target(elf-cleaner COMMENT "Building elf-cleaner" VERBATIM COMMAND "${CMAKE_C_COMPILER}"
|
||||
add_custom_target(elf-cleaner COMMENT "Building elf-cleaner" VERBATIM COMMAND cc
|
||||
-O2 -DPACKAGE_NAME="elf-cleaner" -DPACKAGE_VERSION="" -DCOPYRIGHT=""
|
||||
-o "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner" ${ELF_CLEANER_SOURCES}
|
||||
)
|
||||
|
||||
@@ -20,12 +20,12 @@ export GOARCH := $(NDK_GO_ARCH_MAP_$(ANDROID_ARCH_NAME))
|
||||
export GOOS := android
|
||||
export CGO_ENABLED := 1
|
||||
|
||||
GO_VERSION := 1.20.2
|
||||
GO_VERSION := 1.20.3
|
||||
GO_PLATFORM := $(shell uname -s | tr '[:upper:]' '[:lower:]')-$(NDK_GO_ARCH_MAP_$(shell uname -m))
|
||||
GO_TARBALL := go$(GO_VERSION).$(GO_PLATFORM).tar.gz
|
||||
GO_HASH_darwin-amd64 := c93b8ced9517d07e1cd4c362c6e2d5242cb139e29b417a328fbf19aded08764c
|
||||
GO_HASH_darwin-arm64 := 7343c87f19e79c0063532e82e1c4d6f42175a32d99f7a4d15e658e88bf97f885
|
||||
GO_HASH_linux-amd64 := 4eaea32f59cde4dc635fbc42161031d13e1c780b87097f4b4234cfce671f1768
|
||||
GO_HASH_darwin-amd64 := c1e1161d6d859deb576e6cfabeb40e3d042ceb1c6f444f617c3c9d76269c3565
|
||||
GO_HASH_darwin-arm64 := 86b0ed0f2b2df50fa8036eea875d1cf2d76cefdacf247c44639a1464b7e36b95
|
||||
GO_HASH_linux-amd64 := 979694c2c25c735755bf26f4f45e19e64e4811d661dd07b8c010f7a8e18adfca
|
||||
|
||||
default: $(DESTDIR)/libwg-go.so
|
||||
|
||||
@@ -47,6 +47,6 @@ $(BUILDDIR)/go-$(GO_VERSION)/.prepared: $(GRADLE_USER_HOME)/caches/golang/$(GO_T
|
||||
|
||||
$(DESTDIR)/libwg-go.so: export PATH := $(BUILDDIR)/go-$(GO_VERSION)/bin/:$(PATH)
|
||||
$(DESTDIR)/libwg-go.so: $(BUILDDIR)/go-$(GO_VERSION)/.prepared go.mod
|
||||
go build -tags linux -ldflags="-X golang.zx2c4.com/wireguard/ipc.socketDirectory=/data/data/$(ANDROID_PACKAGE_NAME)/cache/wireguard -buildid=" -v -trimpath -o "$@" -buildmode c-shared
|
||||
go build -tags linux -ldflags="-X golang.zx2c4.com/wireguard/ipc.socketDirectory=/data/data/$(ANDROID_PACKAGE_NAME)/cache/wireguard -buildid=" -v -trimpath -buildvcs=false -o "$@" -buildmode c-shared
|
||||
|
||||
.DELETE_ON_ERROR:
|
||||
|
||||
@@ -6,58 +6,6 @@
|
||||
|
||||
#define FILE_IS_EMPTY
|
||||
|
||||
#if defined(__ANDROID_MIN_SDK_VERSION__) && __ANDROID_MIN_SDK_VERSION__ < 18
|
||||
#undef FILE_IS_EMPTY
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
ssize_t getdelim(char **buf, size_t *bufsiz, int delimiter, FILE *fp)
|
||||
{
|
||||
char *ptr, *eptr;
|
||||
|
||||
if (*buf == NULL || *bufsiz == 0) {
|
||||
*bufsiz = BUFSIZ;
|
||||
if ((*buf = malloc(*bufsiz)) == NULL)
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (ptr = *buf, eptr = *buf + *bufsiz;;) {
|
||||
int c = fgetc(fp);
|
||||
if (c == -1) {
|
||||
if (feof(fp)) {
|
||||
ssize_t diff = (ssize_t)(ptr - *buf);
|
||||
if (diff != 0) {
|
||||
*ptr = '\0';
|
||||
return diff;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
*ptr++ = c;
|
||||
if (c == delimiter) {
|
||||
*ptr = '\0';
|
||||
return ptr - *buf;
|
||||
}
|
||||
if (ptr + 2 >= eptr) {
|
||||
char *nbuf;
|
||||
size_t nbufsiz = *bufsiz * 2;
|
||||
ssize_t d = ptr - *buf;
|
||||
if ((nbuf = realloc(*buf, nbufsiz)) == NULL)
|
||||
return -1;
|
||||
*buf = nbuf;
|
||||
*bufsiz = nbufsiz;
|
||||
eptr = nbuf + nbufsiz;
|
||||
ptr = nbuf + d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ssize_t getline(char **buf, size_t *bufsiz, FILE *fp)
|
||||
{
|
||||
return getdelim(buf, bufsiz, '\n', fp);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__ANDROID_MIN_SDK_VERSION__) && __ANDROID_MIN_SDK_VERSION__ < 24
|
||||
#undef FILE_IS_EMPTY
|
||||
#include <string.h>
|
||||
|
||||
@@ -4,12 +4,6 @@
|
||||
*
|
||||
*/
|
||||
|
||||
#if defined(__ANDROID_MIN_SDK_VERSION__) && __ANDROID_MIN_SDK_VERSION__ < 18
|
||||
#include <stdio.h>
|
||||
ssize_t getdelim(char **buf, size_t *bufsiz, int delimiter, FILE *fp);
|
||||
ssize_t getline(char **buf, size_t *bufsiz, FILE *fp);
|
||||
#endif
|
||||
|
||||
#if defined(__ANDROID_MIN_SDK_VERSION__) && __ANDROID_MIN_SDK_VERSION__ < 24
|
||||
char *strchrnul(const char *s, int c);
|
||||
#endif
|
||||
|
||||
+2
-19
@@ -9,10 +9,6 @@ plugins {
|
||||
version wireguardVersionName
|
||||
group groupName
|
||||
|
||||
// Create a variable called keystorePropertiesFile, and initialize it to your
|
||||
// keystore.properties file, in the rootProject folder.
|
||||
final def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
|
||||
android {
|
||||
compileSdk 33
|
||||
buildFeatures {
|
||||
@@ -34,24 +30,11 @@ android {
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
coreLibraryDesugaringEnabled = true
|
||||
}
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
final def keystoreProperties = new Properties()
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
storeFile rootProject.file(keystoreProperties['storeFile'])
|
||||
storePassword keystoreProperties['storePassword']
|
||||
}
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
if (keystorePropertiesFile.exists()) signingConfig signingConfigs.release
|
||||
minifyEnabled true
|
||||
proguardFiles "proguard-android-optimize.txt", "proguard-rules.pro"
|
||||
shrinkResources true
|
||||
proguardFiles "proguard-android-optimize.txt"
|
||||
packagingOptions {
|
||||
exclude "DebugProbesKt.bin"
|
||||
exclude "kotlin-tooling-metadata.json"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
-allowaccessmodification
|
||||
-dontpreverify
|
||||
-dontusemixedcaseclassnames
|
||||
-dontskipnonpubliclibraryclasses
|
||||
-dontobfuscate
|
||||
-verbose
|
||||
|
||||
-keepattributes *Annotation*
|
||||
|
||||
Vendored
-14
@@ -1,14 +0,0 @@
|
||||
# Squelch all warnings, they're harmless but ProGuard
|
||||
# escalates them as errors.
|
||||
-dontwarn sun.misc.Unsafe
|
||||
|
||||
# Fragment 1.2.4 allows Fragment classes to be obfuscated but
|
||||
# databinding references in XML seem to not be rewritten to
|
||||
# match, so we preserve the names as 1.2.3 did.
|
||||
-if public class ** extends androidx.fragment.app.Fragment
|
||||
-keep public class <1> {
|
||||
public <init>();
|
||||
}
|
||||
|
||||
# Don't obfuscate
|
||||
-dontobfuscate
|
||||
@@ -6,6 +6,7 @@
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28"
|
||||
@@ -103,6 +104,7 @@
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.ACTION_SHUTDOWN" />
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.wireguard.android.backend.GoBackend
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.wireguard.android.configStore.FileConfigStore
|
||||
import com.wireguard.android.model.TunnelManager
|
||||
import com.wireguard.android.updater.Updater
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.wireguard.android.util.ToolsInstaller
|
||||
import com.wireguard.android.util.UserKnobs
|
||||
@@ -58,10 +59,6 @@ class Application : android.app.Application() {
|
||||
startActivity(intent)
|
||||
System.exit(0)
|
||||
}
|
||||
if (BuildConfig.DEBUG) {
|
||||
StrictMode.setVmPolicy(VmPolicy.Builder().detectAll().penaltyLog().build())
|
||||
StrictMode.setThreadPolicy(ThreadPolicy.Builder().detectAll().penaltyLog().build())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun determineBackend(): Backend {
|
||||
@@ -119,6 +116,12 @@ class Application : android.app.Application() {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
}
|
||||
Updater.monitorForUpdates()
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
StrictMode.setVmPolicy(VmPolicy.Builder().detectAll().penaltyLog().build())
|
||||
StrictMode.setThreadPolicy(ThreadPolicy.Builder().detectAll().penaltyLog().build())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
|
||||
@@ -8,15 +8,27 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import com.wireguard.android.activity.MainActivity
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.wireguard.android.updater.Updater
|
||||
import com.wireguard.android.util.applicationScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class BootShutdownReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val action = intent.action ?: return
|
||||
|
||||
if (Intent.ACTION_MY_PACKAGE_REPLACED == action && Updater.installer() == context.packageName) {
|
||||
/* TODO: does not work because of restrictions placed on broadcast receivers. */
|
||||
val start = Intent(context, MainActivity::class.java)
|
||||
start.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
start.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(start)
|
||||
return
|
||||
}
|
||||
|
||||
applicationScope.launch {
|
||||
if (Application.getBackend() !is WgQuickBackend) return@launch
|
||||
val action = intent.action ?: return@launch
|
||||
val tunnelManager = Application.getTunnelManager()
|
||||
if (Intent.ACTION_BOOT_COMPLETED == action) {
|
||||
Log.i(TAG, "Broadcast receiver restoring state (boot)")
|
||||
|
||||
@@ -157,9 +157,7 @@ class LogViewerActivity : AppCompatActivity() {
|
||||
builder.append(rawLogLines[i])
|
||||
builder.append('\n')
|
||||
}
|
||||
val ret = builder.toString().toByteArray(Charsets.UTF_8)
|
||||
builder.clear()
|
||||
return ret
|
||||
return builder.toString().toByteArray(Charsets.UTF_8)
|
||||
}
|
||||
|
||||
private suspend fun saveLog() {
|
||||
@@ -203,7 +201,7 @@ class LogViewerActivity : AppCompatActivity() {
|
||||
var priorModified = false
|
||||
val bufferedLogLines = arrayListOf<LogLine>()
|
||||
var timeout = 1000000000L / 2 // The timeout is initially small so that the view gets populated immediately.
|
||||
val MAX_LINES = (1 shl 17) - 1
|
||||
val MAX_LINES = (1 shl 16) - 1
|
||||
val MAX_BUFFERED_LINES = (1 shl 14) - 1
|
||||
|
||||
while (true) {
|
||||
|
||||
@@ -63,8 +63,8 @@ class AppListDialogFragment : DialogFragment() {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
appData.clear()
|
||||
appData.addAll(applicationData)
|
||||
setButtonText()
|
||||
}
|
||||
setButtonText()
|
||||
} catch (e: Throwable) {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
val error = ErrorMessages[e]
|
||||
|
||||
@@ -112,16 +112,25 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider {
|
||||
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i))
|
||||
?: continue
|
||||
val publicKey = peer.item!!.publicKey
|
||||
val rx = statistics.peerRx(publicKey)
|
||||
val tx = statistics.peerTx(publicKey)
|
||||
if (rx == 0L && tx == 0L) {
|
||||
val peerStats = statistics.peer(publicKey)
|
||||
if (peerStats == null || (peerStats.rxBytes == 0L && peerStats.txBytes == 0L)) {
|
||||
peer.transferLabel.visibility = View.GONE
|
||||
peer.transferText.visibility = View.GONE
|
||||
continue
|
||||
} else {
|
||||
peer.transferText.text = getString(R.string.transfer_rx_tx,
|
||||
QuantityFormatter.formatBytes(peerStats.rxBytes),
|
||||
QuantityFormatter.formatBytes(peerStats.txBytes))
|
||||
peer.transferLabel.visibility = View.VISIBLE
|
||||
peer.transferText.visibility = View.VISIBLE
|
||||
}
|
||||
if (peerStats == null || peerStats.latestHandshakeEpochMillis == 0L) {
|
||||
peer.latestHandshakeLabel.visibility = View.GONE
|
||||
peer.latestHandshakeText.visibility = View.GONE
|
||||
} else {
|
||||
peer.latestHandshakeText.text = QuantityFormatter.formatEpochAgo(peerStats.latestHandshakeEpochMillis)
|
||||
peer.latestHandshakeLabel.visibility = View.VISIBLE
|
||||
peer.latestHandshakeText.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 +138,8 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider {
|
||||
?: continue
|
||||
peer.transferLabel.visibility = View.GONE
|
||||
peer.transferText.visibility = View.GONE
|
||||
peer.latestHandshakeLabel.visibility = View.GONE
|
||||
peer.latestHandshakeText.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowC
|
||||
import com.wireguard.android.databinding.TunnelListFragmentBinding
|
||||
import com.wireguard.android.databinding.TunnelListItemBinding
|
||||
import com.wireguard.android.model.ObservableTunnel
|
||||
import com.wireguard.android.updater.SnackbarUpdateShower
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import com.wireguard.android.util.QrCodeFromFileScanner
|
||||
import com.wireguard.android.util.TunnelImporter
|
||||
@@ -122,6 +123,8 @@ class TunnelListFragment : BaseFragment() {
|
||||
backPressedCallback = requireActivity().onBackPressedDispatcher.addCallback(this) { actionMode?.finish() }
|
||||
backPressedCallback?.isEnabled = false
|
||||
|
||||
SnackbarUpdateShower.attachToActivity(requireActivity(), binding?.mainContainer!!, binding?.createFab)
|
||||
|
||||
return binding?.root
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import android.widget.Toast
|
||||
import androidx.preference.Preference
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.updater.Updater
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
|
||||
class DonatePreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
||||
@@ -22,21 +23,8 @@ class DonatePreference(context: Context, attrs: AttributeSet?) : Preference(cont
|
||||
override fun getTitle() = context.getString(R.string.donate_title)
|
||||
|
||||
override fun onClick() {
|
||||
val installer = try {
|
||||
val packageName = context.packageName
|
||||
val pm = context.packageManager
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
pm.getInstallSourceInfo(packageName).installingPackageName ?: ""
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
pm.getInstallerPackageName(packageName) ?: ""
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
""
|
||||
}
|
||||
|
||||
/* Google Play Store forbids links to our donation page. */
|
||||
if (installer == "com.android.vending") {
|
||||
if (Updater.installerIsGooglePlay()) {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.donate_title)
|
||||
.setMessage(R.string.donate_google_play_disappointment)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.updater
|
||||
|
||||
import android.view.View
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.snackbar.BaseTransientBottomBar
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import com.wireguard.android.util.QuantityFormatter
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
object SnackbarUpdateShower {
|
||||
private class SwapableSnackbar(activity: FragmentActivity, view: View, anchor: View?) {
|
||||
val actionSnackbar = makeSnackbar(activity, view, anchor)
|
||||
val statusSnackbar = makeSnackbar(activity, view, anchor)
|
||||
var showingAction: Boolean = false
|
||||
var showingStatus: Boolean = false
|
||||
|
||||
private fun makeSnackbar(activity: FragmentActivity, view: View, anchor: View?): Snackbar {
|
||||
val snackbar = Snackbar.make(activity, view, "", Snackbar.LENGTH_INDEFINITE)
|
||||
if (anchor != null)
|
||||
snackbar.anchorView = anchor
|
||||
snackbar.setTextMaxLines(6)
|
||||
snackbar.behavior = object : BaseTransientBottomBar.Behavior() {
|
||||
override fun canSwipeDismissView(child: View): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
snackbar.addCallback(object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
|
||||
override fun onDismissed(snackbar: Snackbar?, @DismissEvent event: Int) {
|
||||
super.onDismissed(snackbar, event)
|
||||
if (event == DISMISS_EVENT_MANUAL || event == DISMISS_EVENT_ACTION ||
|
||||
(snackbar == actionSnackbar && !showingAction) ||
|
||||
(snackbar == statusSnackbar && !showingStatus)
|
||||
)
|
||||
return
|
||||
activity.lifecycleScope.launch {
|
||||
delay(5.seconds)
|
||||
snackbar?.show()
|
||||
}
|
||||
}
|
||||
})
|
||||
return snackbar
|
||||
}
|
||||
|
||||
fun showAction(text: String, action: String, listener: View.OnClickListener) {
|
||||
if (showingStatus) {
|
||||
showingStatus = false
|
||||
statusSnackbar.dismiss()
|
||||
}
|
||||
actionSnackbar.setText(text)
|
||||
actionSnackbar.setAction(action, listener)
|
||||
if (!showingAction) {
|
||||
actionSnackbar.show()
|
||||
showingAction = true
|
||||
}
|
||||
}
|
||||
|
||||
fun showText(text: String) {
|
||||
if (showingAction) {
|
||||
showingAction = false
|
||||
actionSnackbar.dismiss()
|
||||
}
|
||||
statusSnackbar.setText(text)
|
||||
if (!showingStatus) {
|
||||
statusSnackbar.show()
|
||||
showingStatus = true
|
||||
}
|
||||
}
|
||||
|
||||
fun dismiss() {
|
||||
actionSnackbar.dismiss()
|
||||
statusSnackbar.dismiss()
|
||||
showingAction = false
|
||||
showingStatus = false
|
||||
}
|
||||
}
|
||||
|
||||
fun attachToActivity(activity: FragmentActivity, view: View, anchor: View?) {
|
||||
val snackbar = SwapableSnackbar(activity, view, anchor)
|
||||
val context = activity.applicationContext
|
||||
|
||||
var lastUserIntervention: Updater.Progress.NeedsUserIntervention? = null
|
||||
val intentLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
lastUserIntervention?.markAsDone()
|
||||
}
|
||||
|
||||
Updater.state.onEach { progress ->
|
||||
when (progress) {
|
||||
is Updater.Progress.Complete ->
|
||||
snackbar.dismiss()
|
||||
|
||||
is Updater.Progress.Available ->
|
||||
snackbar.showAction(
|
||||
context.getString(R.string.updater_avalable),
|
||||
context.getString(R.string.updater_action)
|
||||
) {
|
||||
progress.update()
|
||||
}
|
||||
|
||||
is Updater.Progress.NeedsUserIntervention -> {
|
||||
lastUserIntervention = progress
|
||||
intentLauncher.launch(progress.intent)
|
||||
}
|
||||
|
||||
is Updater.Progress.Installing ->
|
||||
snackbar.showText(context.getString(R.string.updater_installing))
|
||||
|
||||
is Updater.Progress.Rechecking ->
|
||||
snackbar.showText(context.getString(R.string.updater_rechecking))
|
||||
|
||||
is Updater.Progress.Downloading -> {
|
||||
if (progress.bytesTotal != 0UL) {
|
||||
snackbar.showText(
|
||||
context.getString(
|
||||
R.string.updater_download_progress,
|
||||
QuantityFormatter.formatBytes(progress.bytesDownloaded.toLong()),
|
||||
QuantityFormatter.formatBytes(progress.bytesTotal.toLong()),
|
||||
progress.bytesDownloaded.toFloat() * 100.0 / progress.bytesTotal.toFloat()
|
||||
)
|
||||
)
|
||||
} else {
|
||||
snackbar.showText(
|
||||
context.getString(
|
||||
R.string.updater_download_progress_nototal,
|
||||
QuantityFormatter.formatBytes(progress.bytesDownloaded.toLong())
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is Updater.Progress.Failure -> {
|
||||
snackbar.showText(
|
||||
context.getString(
|
||||
R.string.updater_failure,
|
||||
ErrorMessages[progress.error]
|
||||
)
|
||||
)
|
||||
delay(5.seconds)
|
||||
progress.retry()
|
||||
}
|
||||
}
|
||||
}.launchIn(activity.lifecycleScope)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
/*
|
||||
* Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.updater
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.IntentCompat
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.BuildConfig
|
||||
import com.wireguard.android.util.UserKnobs
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.InvalidParameterException
|
||||
import java.security.MessageDigest
|
||||
import java.util.UUID
|
||||
import kotlin.math.max
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
object Updater {
|
||||
private const val TAG = "WireGuard/Updater"
|
||||
private const val LATEST_VERSION_URL = "https://download.wireguard.com/android-client/latest.sig"
|
||||
private const val APK_PATH_URL = "https://download.wireguard.com/android-client/%s"
|
||||
private const val APK_NAME_PREFIX = BuildConfig.APPLICATION_ID + "-"
|
||||
private const val APK_NAME_SUFFIX = ".apk"
|
||||
private const val RELEASE_PUBLIC_KEY_BASE64 = "RWTAzwGRYr3EC9px0Ia3fbttz8WcVN6wrOwWp2delz4el6SI8XmkKSMp"
|
||||
private val CURRENT_VERSION = BuildConfig.VERSION_NAME.removeSuffix("-debug")
|
||||
|
||||
sealed class Progress {
|
||||
object Complete : Progress()
|
||||
class Available(val version: String) : Progress() {
|
||||
fun update() {
|
||||
Application.getCoroutineScope().launch {
|
||||
UserKnobs.setUpdaterNewerVersionConsented(version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Rechecking : Progress()
|
||||
class Downloading(val bytesDownloaded: ULong, val bytesTotal: ULong) : Progress()
|
||||
object Installing : Progress()
|
||||
class NeedsUserIntervention(val intent: Intent, private val id: Int) : Progress() {
|
||||
|
||||
private suspend fun installerActive(): Boolean {
|
||||
if (mutableState.firstOrNull() != this@NeedsUserIntervention)
|
||||
return true
|
||||
try {
|
||||
if (Application.get().packageManager.packageInstaller.getSessionInfo(id)?.isActive == true)
|
||||
return true
|
||||
} catch (_: SecurityException) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun markAsDone() {
|
||||
Application.getCoroutineScope().launch {
|
||||
if (installerActive())
|
||||
return@launch
|
||||
delay(7.seconds)
|
||||
if (installerActive())
|
||||
return@launch
|
||||
emitProgress(Failure(Exception("Ignored by user")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Failure(val error: Throwable) : Progress() {
|
||||
fun retry() {
|
||||
Application.getCoroutineScope().launch {
|
||||
downloadAndUpdateWrapErrors()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val mutableState = MutableStateFlow<Progress>(Progress.Complete)
|
||||
val state = mutableState.asStateFlow()
|
||||
|
||||
private suspend fun emitProgress(progress: Progress, force: Boolean = false) {
|
||||
if (force || mutableState.firstOrNull()?.javaClass != progress.javaClass)
|
||||
mutableState.emit(progress)
|
||||
}
|
||||
|
||||
private fun versionIsNewer(lhs: String, rhs: String): Boolean {
|
||||
val lhsParts = lhs.split(".")
|
||||
val rhsParts = rhs.split(".")
|
||||
if (lhsParts.isEmpty() || rhsParts.isEmpty())
|
||||
throw InvalidParameterException("Version is empty")
|
||||
|
||||
for (i in 0 until max(lhsParts.size, rhsParts.size)) {
|
||||
val lhsPart = if (i < lhsParts.size) lhsParts[i].toULong() else 0UL
|
||||
val rhsPart = if (i < rhsParts.size) rhsParts[i].toULong() else 0UL
|
||||
if (lhsPart == rhsPart)
|
||||
continue
|
||||
return lhsPart > rhsPart
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun versionOfFile(name: String): String? {
|
||||
if (!name.startsWith(APK_NAME_PREFIX) || !name.endsWith(APK_NAME_SUFFIX))
|
||||
return null
|
||||
return name.substring(APK_NAME_PREFIX.length, name.length - APK_NAME_SUFFIX.length)
|
||||
}
|
||||
|
||||
private fun verifySignedFileList(signifyDigest: String): Map<String, Sha256Digest> {
|
||||
val publicKeyBytes = Base64.decode(RELEASE_PUBLIC_KEY_BASE64, Base64.DEFAULT)
|
||||
if (publicKeyBytes == null || publicKeyBytes.size != 32 + 10 || publicKeyBytes[0] != 'E'.code.toByte() || publicKeyBytes[1] != 'd'.code.toByte())
|
||||
throw InvalidKeyException("Invalid public key")
|
||||
val lines = signifyDigest.split("\n", limit = 3)
|
||||
if (lines.size != 3)
|
||||
throw InvalidParameterException("Invalid signature format: too few lines")
|
||||
if (!lines[0].startsWith("untrusted comment: "))
|
||||
throw InvalidParameterException("Invalid signature format: missing comment")
|
||||
val signatureBytes = Base64.decode(lines[1], Base64.DEFAULT)
|
||||
if (signatureBytes == null || signatureBytes.size != 64 + 10)
|
||||
throw InvalidParameterException("Invalid signature format: wrong sized or missing signature")
|
||||
for (i in 0..9) {
|
||||
if (signatureBytes[i] != publicKeyBytes[i])
|
||||
throw InvalidParameterException("Invalid signature format: wrong signer")
|
||||
}
|
||||
if (!Ed25519.verify(
|
||||
lines[2].toByteArray(StandardCharsets.UTF_8),
|
||||
signatureBytes.sliceArray(10 until 10 + 64),
|
||||
publicKeyBytes.sliceArray(10 until 10 + 32)
|
||||
)
|
||||
)
|
||||
throw SecurityException("Invalid signature")
|
||||
val hashes: MutableMap<String, Sha256Digest> = HashMap()
|
||||
for (line in lines[2].split("\n").dropLastWhile { it.isEmpty() }) {
|
||||
val components = line.split(" ", limit = 2)
|
||||
if (components.size != 2)
|
||||
throw InvalidParameterException("Invalid file list format: too few components")
|
||||
hashes[components[1]] = Sha256Digest(components[0])
|
||||
}
|
||||
return hashes
|
||||
}
|
||||
|
||||
private class Sha256Digest(hex: String) {
|
||||
val bytes: ByteArray
|
||||
|
||||
init {
|
||||
if (hex.length != 64)
|
||||
throw InvalidParameterException("SHA256 hashes must be 32 bytes long")
|
||||
bytes = hex.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkForUpdates(): Pair<String, Sha256Digest> {
|
||||
val connection = URL(LATEST_VERSION_URL).openConnection() as HttpURLConnection
|
||||
connection.setRequestProperty("User-Agent", Application.USER_AGENT)
|
||||
connection.connect()
|
||||
if (connection.responseCode != HttpURLConnection.HTTP_OK)
|
||||
throw IOException("File list could not be fetched: ${connection.responseCode}")
|
||||
var fileListBytes = ByteArray(1024 * 512 /* 512 KiB */)
|
||||
connection.inputStream.use {
|
||||
val len = it.read(fileListBytes)
|
||||
if (len <= 0)
|
||||
throw IOException("File list is empty")
|
||||
fileListBytes = fileListBytes.sliceArray(0 until len)
|
||||
}
|
||||
val fileList = verifySignedFileList(fileListBytes.decodeToString())
|
||||
if (fileList.isEmpty())
|
||||
throw InvalidParameterException("File list is empty")
|
||||
var newestFile: String? = null
|
||||
var newestVersion: String? = null
|
||||
var newestFileHash: Sha256Digest? = null
|
||||
for (file in fileList) {
|
||||
val fileVersion = versionOfFile(file.key)
|
||||
try {
|
||||
if (fileVersion != null && (newestVersion == null || versionIsNewer(fileVersion, newestVersion))) {
|
||||
newestVersion = fileVersion
|
||||
newestFile = file.key
|
||||
newestFileHash = file.value
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
}
|
||||
if (newestFile == null || newestFileHash == null)
|
||||
throw InvalidParameterException("File list is empty")
|
||||
return Pair(newestFile, newestFileHash)
|
||||
}
|
||||
|
||||
private suspend fun downloadAndUpdate() = withContext(Dispatchers.IO) {
|
||||
val receiver = InstallReceiver()
|
||||
val context = Application.get().applicationContext
|
||||
val pendingIntent = withContext(Dispatchers.Main) {
|
||||
ContextCompat.registerReceiver(
|
||||
context,
|
||||
receiver,
|
||||
IntentFilter(receiver.sessionId),
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
)
|
||||
PendingIntent.getBroadcast(
|
||||
context,
|
||||
0,
|
||||
Intent(receiver.sessionId).setPackage(context.packageName),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
||||
)
|
||||
}
|
||||
|
||||
emitProgress(Progress.Rechecking)
|
||||
val update = checkForUpdates()
|
||||
val updateVersion = versionOfFile(checkForUpdates().first) ?: throw Exception("No versions returned")
|
||||
if (!versionIsNewer(updateVersion, CURRENT_VERSION)) {
|
||||
emitProgress(Progress.Complete)
|
||||
return@withContext
|
||||
}
|
||||
|
||||
emitProgress(Progress.Downloading(0UL, 0UL), true)
|
||||
val connection = URL(APK_PATH_URL.format(update.first)).openConnection() as HttpURLConnection
|
||||
connection.setRequestProperty("User-Agent", Application.USER_AGENT)
|
||||
connection.connect()
|
||||
if (connection.responseCode != HttpURLConnection.HTTP_OK)
|
||||
throw IOException("Update could not be fetched: ${connection.responseCode}")
|
||||
|
||||
var downloadedByteLen: ULong = 0UL
|
||||
val totalByteLen =
|
||||
(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) connection.contentLengthLong else connection.contentLength).toLong()
|
||||
.toULong()
|
||||
val fileBytes = ByteArray(1024 * 32 /* 32 KiB */)
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
emitProgress(Progress.Downloading(downloadedByteLen, totalByteLen), true)
|
||||
|
||||
val installer = context.packageManager.packageInstaller
|
||||
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
params.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
|
||||
params.setAppPackageName(context.packageName) /* Enforces updates; disallows new apps. */
|
||||
val session = installer.openSession(installer.createSession(params))
|
||||
var sessionFailure = true
|
||||
try {
|
||||
val installDest = session.openWrite(receiver.sessionId, 0, -1)
|
||||
|
||||
installDest.use { dest ->
|
||||
connection.inputStream.use { src ->
|
||||
while (true) {
|
||||
val readLen = src.read(fileBytes)
|
||||
if (readLen <= 0)
|
||||
break
|
||||
|
||||
digest.update(fileBytes, 0, readLen)
|
||||
dest.write(fileBytes, 0, readLen)
|
||||
|
||||
downloadedByteLen += readLen.toUInt()
|
||||
emitProgress(Progress.Downloading(downloadedByteLen, totalByteLen), true)
|
||||
|
||||
if (downloadedByteLen >= 1024UL * 1024UL * 100UL /* 100 MiB */)
|
||||
throw IOException("File too large")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emitProgress(Progress.Installing)
|
||||
if (!digest.digest().contentEquals(update.second.bytes))
|
||||
throw SecurityException("Update has invalid hash")
|
||||
sessionFailure = false
|
||||
} finally {
|
||||
if (sessionFailure) {
|
||||
session.abandon()
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
session.commit(pendingIntent.intentSender)
|
||||
session.close()
|
||||
}
|
||||
|
||||
private suspend fun downloadAndUpdateWrapErrors() {
|
||||
try {
|
||||
downloadAndUpdate()
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Update failure", e)
|
||||
emitProgress(Progress.Failure(e))
|
||||
}
|
||||
}
|
||||
|
||||
private class InstallReceiver : BroadcastReceiver() {
|
||||
val sessionId = UUID.randomUUID().toString()
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (sessionId != intent.action)
|
||||
return
|
||||
|
||||
when (val status =
|
||||
intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE_INVALID)) {
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
val id = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, 0)
|
||||
val userIntervention = IntentCompat.getParcelableExtra(intent, Intent.EXTRA_INTENT, Intent::class.java)!!
|
||||
Application.getCoroutineScope().launch {
|
||||
emitProgress(Progress.NeedsUserIntervention(userIntervention, id))
|
||||
}
|
||||
}
|
||||
|
||||
PackageInstaller.STATUS_SUCCESS -> {
|
||||
Application.getCoroutineScope().launch {
|
||||
emitProgress(Progress.Complete)
|
||||
}
|
||||
context.applicationContext.unregisterReceiver(this)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val id = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, 0)
|
||||
try {
|
||||
context.applicationContext.packageManager.packageInstaller.abandonSession(id)
|
||||
} catch (_: SecurityException) {
|
||||
}
|
||||
val message =
|
||||
intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) ?: "Installation error $status"
|
||||
Application.getCoroutineScope().launch {
|
||||
val e = Exception(message)
|
||||
Log.e(TAG, "Update failure", e)
|
||||
emitProgress(Progress.Failure(e))
|
||||
}
|
||||
context.applicationContext.unregisterReceiver(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun monitorForUpdates() {
|
||||
if (installerIsGooglePlay())
|
||||
return
|
||||
|
||||
Application.getCoroutineScope().launch(Dispatchers.IO) {
|
||||
if (UserKnobs.updaterNewerVersionSeen.firstOrNull()?.let { versionIsNewer(it, CURRENT_VERSION) } == true)
|
||||
return@launch
|
||||
|
||||
var waitTime = 15
|
||||
while (true) {
|
||||
try {
|
||||
val updateVersion = versionOfFile(checkForUpdates().first) ?: throw IllegalStateException("No versions returned")
|
||||
if (versionIsNewer(updateVersion, CURRENT_VERSION)) {
|
||||
Log.i(TAG, "Update available: $updateVersion")
|
||||
UserKnobs.setUpdaterNewerVersionSeen(updateVersion)
|
||||
return@launch
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Failed to check for updates", e)
|
||||
}
|
||||
delay(waitTime.minutes)
|
||||
waitTime = 45
|
||||
}
|
||||
}
|
||||
|
||||
UserKnobs.updaterNewerVersionSeen.onEach { ver ->
|
||||
if (ver != null && versionIsNewer(
|
||||
ver,
|
||||
CURRENT_VERSION
|
||||
) && UserKnobs.updaterNewerVersionConsented.firstOrNull()
|
||||
?.let { versionIsNewer(it, CURRENT_VERSION) } != true
|
||||
)
|
||||
emitProgress(Progress.Available(ver))
|
||||
}.launchIn(Application.getCoroutineScope())
|
||||
|
||||
UserKnobs.updaterNewerVersionConsented.onEach { ver ->
|
||||
if (ver != null && versionIsNewer(ver, CURRENT_VERSION))
|
||||
downloadAndUpdateWrapErrors()
|
||||
}.launchIn(Application.getCoroutineScope())
|
||||
}
|
||||
|
||||
fun installer(): String {
|
||||
val context = Application.get().applicationContext
|
||||
return try {
|
||||
val packageName = context.packageName
|
||||
val pm = context.packageManager
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
pm.getInstallSourceInfo(packageName).installingPackageName ?: ""
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
pm.getInstallerPackageName(packageName) ?: ""
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
fun installerIsGooglePlay(): Boolean = installer() == "com.android.vending"
|
||||
}
|
||||
@@ -92,8 +92,8 @@ object ErrorMessages {
|
||||
rootCause is ChecksumException -> {
|
||||
resources.getString(R.string.error_qr_checksum)
|
||||
}
|
||||
rootCause.message != null -> {
|
||||
rootCause.message!!
|
||||
rootCause.localizedMessage != null -> {
|
||||
rootCause.localizedMessage!!
|
||||
}
|
||||
else -> {
|
||||
val errorType = rootCause.javaClass.simpleName
|
||||
@@ -109,7 +109,7 @@ object ErrorMessages {
|
||||
if (kfe!!.type == KeyFormatException.Type.LENGTH) return resources.getString(KFE_FORMAT_MAP.getValue(kfe.format))
|
||||
} else if (bce.cause is ParseException) {
|
||||
val pe = bce.cause as ParseException?
|
||||
if (pe!!.message != null) return ": ${pe.message}"
|
||||
if (pe!!.localizedMessage != null) return ": ${pe.localizedMessage}"
|
||||
} else if (bce.location == BadConfigException.Location.LISTEN_PORT) {
|
||||
return resources.getString(R.string.bad_config_explanation_udp_port)
|
||||
} else if (bce.location == BadConfigException.Location.MTU) {
|
||||
|
||||
@@ -5,8 +5,17 @@
|
||||
|
||||
package com.wireguard.android.util
|
||||
|
||||
import android.icu.text.ListFormatter
|
||||
import android.icu.text.MeasureFormat
|
||||
import android.icu.text.RelativeDateTimeFormatter
|
||||
import android.icu.util.Measure
|
||||
import android.icu.util.MeasureUnit
|
||||
import android.os.Build
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import java.util.Locale
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.DurationUnit
|
||||
|
||||
object QuantityFormatter {
|
||||
fun formatBytes(bytes: Long): String {
|
||||
@@ -19,4 +28,40 @@ object QuantityFormatter {
|
||||
else -> context.getString(R.string.transfer_tibibytes, bytes / (1024.0 * 1024.0 * 1024.0) / 1024.0)
|
||||
}
|
||||
}
|
||||
|
||||
fun formatEpochAgo(epochMillis: Long): String {
|
||||
var span = (System.currentTimeMillis() - epochMillis) / 1000
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
|
||||
return Application.get().applicationContext.getString(R.string.latest_handshake_ago, span.seconds.toString())
|
||||
|
||||
if (span <= 0L)
|
||||
return RelativeDateTimeFormatter.getInstance().format(RelativeDateTimeFormatter.Direction.PLAIN, RelativeDateTimeFormatter.AbsoluteUnit.NOW)
|
||||
val measureFormat = MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
|
||||
val parts = ArrayList<CharSequence>(4)
|
||||
if (span >= 24 * 60 * 60L) {
|
||||
val v = span / (24 * 60 * 60L)
|
||||
parts.add(measureFormat.format(Measure(v, MeasureUnit.DAY)))
|
||||
span -= v * (24 * 60 * 60L)
|
||||
}
|
||||
if (span >= 60 * 60L) {
|
||||
val v = span / (60 * 60L)
|
||||
parts.add(measureFormat.format(Measure(v, MeasureUnit.HOUR)))
|
||||
span -= v * (60 * 60L)
|
||||
}
|
||||
if (span >= 60L) {
|
||||
val v = span / 60L
|
||||
parts.add(measureFormat.format(Measure(v, MeasureUnit.MINUTE)))
|
||||
span -= v * 60L
|
||||
}
|
||||
if (span > 0L)
|
||||
parts.add(measureFormat.format(Measure(span, MeasureUnit.SECOND)))
|
||||
|
||||
val joined = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
|
||||
parts.joinToString()
|
||||
else
|
||||
ListFormatter.getInstance(Locale.getDefault(), ListFormatter.Type.UNITS, ListFormatter.Width.SHORT).format(parts)
|
||||
|
||||
return Application.get().applicationContext.getString(R.string.latest_handshake_ago, joined)
|
||||
}
|
||||
}
|
||||
@@ -88,4 +88,34 @@ object UserKnobs {
|
||||
it[RUNNING_TUNNELS] = runningTunnels
|
||||
}
|
||||
}
|
||||
|
||||
private val UPDATER_NEWER_VERSION_SEEN = stringPreferencesKey("updater_newer_version_seen")
|
||||
val updaterNewerVersionSeen: Flow<String?>
|
||||
get() = Application.getPreferencesDataStore().data.map {
|
||||
it[UPDATER_NEWER_VERSION_SEEN]
|
||||
}
|
||||
|
||||
suspend fun setUpdaterNewerVersionSeen(newerVersionSeen: String?) {
|
||||
Application.getPreferencesDataStore().edit {
|
||||
if (newerVersionSeen == null)
|
||||
it.remove(UPDATER_NEWER_VERSION_SEEN)
|
||||
else
|
||||
it[UPDATER_NEWER_VERSION_SEEN] = newerVersionSeen
|
||||
}
|
||||
}
|
||||
|
||||
private val UPDATER_NEWER_VERSION_CONSENTED = stringPreferencesKey("updater_newer_version_consented")
|
||||
val updaterNewerVersionConsented: Flow<String?>
|
||||
get() = Application.getPreferencesDataStore().data.map {
|
||||
it[UPDATER_NEWER_VERSION_CONSENTED]
|
||||
}
|
||||
|
||||
suspend fun setUpdaterNewerVersionConsented(newerVersionConsented: String?) {
|
||||
Application.getPreferencesDataStore().edit {
|
||||
if (newerVersionConsented == null)
|
||||
it.remove(UPDATER_NEWER_VERSION_CONSENTED)
|
||||
else
|
||||
it[UPDATER_NEWER_VERSION_CONSENTED] = newerVersionConsented
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interface_name_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/interface_name_text"
|
||||
@@ -81,7 +81,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interface_name_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/name"
|
||||
android:nextFocusUp="@id/tunnel_switch"
|
||||
@@ -96,7 +96,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/public_key_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/public_key_text"
|
||||
@@ -106,7 +106,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/public_key_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/public_key"
|
||||
android:ellipsize="end"
|
||||
@@ -124,7 +124,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/addresses_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/addresses_text"
|
||||
@@ -135,7 +135,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/addresses_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/addresses"
|
||||
android:nextFocusUp="@id/public_key_text"
|
||||
@@ -151,7 +151,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dns_servers_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/dns_servers_text"
|
||||
@@ -162,7 +162,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dns_servers_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/dns_servers"
|
||||
android:nextFocusUp="@id/addresses_text"
|
||||
@@ -178,7 +178,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dns_search_domains_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/dns_search_domain_text"
|
||||
@@ -189,7 +189,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dns_search_domains_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/dns_search_domains"
|
||||
android:nextFocusUp="@id/dns_servers_text"
|
||||
@@ -277,7 +277,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/applications_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/applications_text"
|
||||
@@ -288,7 +288,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/applications_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/applications"
|
||||
android:nextFocusUp="@id/mtu_text"
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/peer_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/peer"
|
||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/public_key_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/public_key_text"
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/public_key_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/public_key"
|
||||
android:ellipsize="end"
|
||||
@@ -59,7 +59,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pre_shared_key_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/pre_shared_key_text"
|
||||
@@ -70,7 +70,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pre_shared_key_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/pre_shared_key"
|
||||
android:ellipsize="end"
|
||||
@@ -88,7 +88,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/allowed_ips_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/allowed_ips_text"
|
||||
@@ -99,7 +99,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/allowed_ips_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/allowed_ips"
|
||||
android:nextFocusUp="@id/pre_shared_key_text"
|
||||
@@ -115,7 +115,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/endpoint_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/endpoint_text"
|
||||
@@ -126,7 +126,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/endpoint_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/endpoint"
|
||||
android:nextFocusUp="@id/allowed_ips_text"
|
||||
@@ -142,7 +142,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/persistent_keepalive_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/persistent_keepalive_text"
|
||||
@@ -153,7 +153,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/persistent_keepalive_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/persistent_keepalive"
|
||||
android:nextFocusUp="@id/endpoint_text"
|
||||
@@ -169,9 +169,9 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/transfer_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/endpoint_text"
|
||||
android:layout_below="@+id/persistent_keepalive_text"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/transfer_text"
|
||||
android:text="@string/transfer"
|
||||
@@ -182,7 +182,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/transfer_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/transfer_label"
|
||||
android:contentDescription="@string/transfer"
|
||||
@@ -194,6 +194,34 @@
|
||||
app:layout_constraintTop_toBottomOf="@+id/transfer_label"
|
||||
tools:text="1024 MB"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/latest_handshake_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/transfer_text"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/latest_handshake_text"
|
||||
android:text="@string/latest_handshake"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/transfer_text"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/latest_handshake_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/latest_handshake_label"
|
||||
android:contentDescription="@string/latest_handshake"
|
||||
android:nextFocusUp="@id/transfer_text"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/latest_handshake_label"
|
||||
tools:text="4 minutes, 27 seconds ago"
|
||||
tools:visibility="visible" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</layout>
|
||||
|
||||
@@ -111,6 +111,8 @@
|
||||
<string name="tv_select_a_storage_drive">تحديد محرك تخزين</string>
|
||||
<string name="tv_no_file_picker">الرجاء تثبيت مدير ملفات لتتمكن من تصفح الملفات</string>
|
||||
<string name="tv_add_tunnel_get_started">أضف نفقاً لتبدأ</string>
|
||||
<string name="donate_title">♥️ تبرع لمشروع وايرجارد</string>
|
||||
<string name="donate_summary">كل مساهمة تساعد</string>
|
||||
<string name="disable_config_export_title">تعطيل تصدير التكوين</string>
|
||||
<string name="disable_config_export_description">تعطيل تصدير الإعدادات يجعل المفاتيح الخاصة غير متاحة</string>
|
||||
<string name="dns_servers">خوادم DNS</string>
|
||||
@@ -120,6 +122,7 @@
|
||||
<string name="error_down">خطأ في فصل النفق:%s</string>
|
||||
<string name="error_fetching_apps">خطأ في جلب قائمة التطبيقات: %s</string>
|
||||
<string name="error_root">الرجاء الحصول على صلاحيات الروت وحاول مرة أخرى</string>
|
||||
<string name="error_prepare">خطأ في تحضير النفق: %s</string>
|
||||
<string name="error_up">خطأ خلال إنشاء النفق: %s</string>
|
||||
<string name="exclude_private_ips">استبعاد عناوين بروتوكول الإنترنت (IP) الخاصة</string>
|
||||
<string name="generate_new_private_key">توليد مفتاح خاص جديد</string>
|
||||
@@ -215,6 +218,7 @@
|
||||
<string name="tunnel_create_success">تم إنشاء نفق \"%s\" بنجاح</string>
|
||||
<string name="tunnel_error_already_exists">النفق \"%s\" موجود بالفعل</string>
|
||||
<string name="tunnel_error_invalid_name">إسم غير صالح</string>
|
||||
<string name="tunnel_list_placeholder">أضف نفق باستخدام الزر أدناه</string>
|
||||
<string name="tunnel_name">إسم النفق</string>
|
||||
<string name="tunnel_on_error">غير قادر على تشغيل النفق (wgTurnOn أعاد %d)</string>
|
||||
<string name="tunnel_dns_failure">غير قادر على حل اسم مضيف نظام أسماء النطاقات (DNS hostname): \"%s\"</string>
|
||||
|
||||
@@ -72,6 +72,8 @@
|
||||
<string name="config_file_exists_error">Konfigurationsfilen \"%s\" findes allerede</string>
|
||||
<string name="config_not_found_error">Konfigurationsfilen \"%s\" blev ikke fundet</string>
|
||||
<string name="config_rename_error">Kan ikke omdøbe konfigurationsfilen \"%s\"</string>
|
||||
<string name="config_save_error">Kan ikke gemme konfigurationen for \"%1$s\": %2$s</string>
|
||||
<string name="config_save_success">Konfiguration for \"%s\" blev gemt</string>
|
||||
<string name="create_activity_title">Opret WireGuard tunnel</string>
|
||||
<string name="create_empty">Opret fra ny</string>
|
||||
<string name="create_from_file">Importér fra fil eller arkiv</string>
|
||||
@@ -88,6 +90,7 @@
|
||||
<string name="dns_search_domains">DNS-søgedomæner</string>
|
||||
<string name="edit">Redigér</string>
|
||||
<string name="endpoint">Slutpunkt</string>
|
||||
<string name="error_prepare">Fejl ved forberedelse af tunnel: %s</string>
|
||||
<string name="error_up">Fejl under aktivering af tunnel: %s</string>
|
||||
<string name="exclude_private_ips">Eksludér private IP-adresser</string>
|
||||
<string name="generate_new_private_key">Generér ny privat nøgle</string>
|
||||
@@ -152,6 +155,7 @@
|
||||
<string name="tunnel_create_success">Tunnelen blev succesfuldt oprettet \"%s\"</string>
|
||||
<string name="tunnel_error_already_exists">Tunnel \"%s\" eksisterer allerede</string>
|
||||
<string name="tunnel_error_invalid_name">Ugyldigt navn</string>
|
||||
<string name="tunnel_list_placeholder">Tilføj en tunnel ved hjælp af knappen nedenfor</string>
|
||||
<string name="tunnel_name">Tunnel Navn</string>
|
||||
<string name="tunnel_dns_failure">Kunne ikke opslå DNS adresse: \"%s\"</string>
|
||||
<string name="tunnel_rename_error">Kan ikke omdøbe tunnel: %s</string>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<item quantity="other">Importerte %d tunneler</item>
|
||||
</plurals>
|
||||
<plurals name="set_excluded_applications">
|
||||
<item quantity="one">%d ekskludert program</item>
|
||||
<item quantity="one">%d ekskludert app</item>
|
||||
<item quantity="other">%d ekskluderte apper</item>
|
||||
</plurals>
|
||||
<plurals name="set_included_applications">
|
||||
@@ -79,6 +79,8 @@
|
||||
<string name="bad_config_reason_unknown_section">Ukjent seksjon</string>
|
||||
<string name="bad_config_reason_value_out_of_range">Verdien er utenfor gyldig område</string>
|
||||
<string name="bad_extension_error">Filen må være .conf eller .zip</string>
|
||||
<string name="error_no_qr_found">Ingen QR-kode funnet i bildet</string>
|
||||
<string name="error_qr_checksum">Feil ved sjekksumverifisering av QR-kode</string>
|
||||
<string name="cancel">Avbryt</string>
|
||||
<string name="config_delete_error">Kan ikke slette konfigurasjonsfilen %s</string>
|
||||
<string name="config_exists_error">Konfigurasjon for «%s» finnes allerede</string>
|
||||
@@ -105,14 +107,19 @@
|
||||
<string name="tv_select_a_storage_drive">Velg en lagringsenhet</string>
|
||||
<string name="tv_no_file_picker">Vennligst installer et filhåndteringsverktøy for å bla i filer</string>
|
||||
<string name="tv_add_tunnel_get_started">Opprett en ny tunnel for å komme i gang</string>
|
||||
<string name="donate_title">♥ Donér til WireGuard-prosjektet</string>
|
||||
<string name="donate_summary">Hvert bidrag hjelper</string>
|
||||
<string name="donate_google_play_disappointment">Takk for at du støtter WireGuard-prosjektet!\n\nPå grunn av Googles retningslinjer, kan vi dessverre ikke linke til den delen av prosjektets nettside der du kan donere. Forhåpentligvis klarer du å finne denne selv!\n\nVi takker igjen for ditt bidrag.</string>
|
||||
<string name="disable_config_export_title">Deaktiver eksport av konfigurasjon</string>
|
||||
<string name="disable_config_export_description">Deaktivering av konfigurasjonseksport gjør private nøkler mindre tilgjengelig</string>
|
||||
<string name="dns_servers">DNS tjenere</string>
|
||||
<string name="dns_search_domains">Søk gjennom domener</string>
|
||||
<string name="edit">Rediger</string>
|
||||
<string name="endpoint">Endepunkt</string>
|
||||
<string name="error_down">Feil når tunnel skulle tas ned: %s</string>
|
||||
<string name="error_fetching_apps">Feil ved henting av applikasjonsliste: %s</string>
|
||||
<string name="error_root">Vennligst få root-tilgang og prøv igjen</string>
|
||||
<string name="error_prepare">Feil ved klargjøring av tunnel: %s</string>
|
||||
<string name="error_up">Feil når tunnel skulle tas opp: %s</string>
|
||||
<string name="exclude_private_ips">Utelukk private IP-adresser</string>
|
||||
<string name="generate_new_private_key">Lag ny privat nøkkel</string>
|
||||
@@ -210,13 +217,16 @@
|
||||
<string name="tunnel_create_success">Opprettet tunnelen «%s»</string>
|
||||
<string name="tunnel_error_already_exists">Tunnel «%s» finnes allerede</string>
|
||||
<string name="tunnel_error_invalid_name">Ugyldig navn</string>
|
||||
<string name="tunnel_list_placeholder">Legg til en tunnel ved å bruke knappen under</string>
|
||||
<string name="tunnel_name">Tunnelnavn</string>
|
||||
<string name="tunnel_on_error">Kan ikke slå på tunnel (wgTurnOn returnerte %d)</string>
|
||||
<string name="tunnel_dns_failure">Kan ikke slå opp DNS-vertsnavn: “%s\"</string>
|
||||
<string name="tunnel_rename_error">Kan ikke endre navn på tunnel: %s</string>
|
||||
<string name="tunnel_rename_success">Endret navn på tunnelen til «%s»</string>
|
||||
<string name="type_name_go_userspace">Bruk userspace</string>
|
||||
<string name="type_name_kernel_module">Kjernemodul</string>
|
||||
<string name="unknown_error">Ukjent feil</string>
|
||||
<string name="version_summary">%1$s backend %2$s</string>
|
||||
<string name="version_summary_checking">Sjekker %s backend versjon</string>
|
||||
<string name="version_summary_unknown">Ukjent %s versjon</string>
|
||||
<string name="version_title">WireGuard for Android v%s</string>
|
||||
|
||||
@@ -133,6 +133,9 @@
|
||||
<string name="tv_select_a_storage_drive">Выберите накопитель</string>
|
||||
<string name="tv_no_file_picker">Пожалуйста, установите утилиту управления файлами для их просмотра</string>
|
||||
<string name="tv_add_tunnel_get_started">Добавьте туннель, чтобы начать</string>
|
||||
<string name="donate_title">♥ Пожертвовать проекту WireGuard</string>
|
||||
<string name="donate_summary">Каждое пожертвование помогает</string>
|
||||
<string name="donate_google_play_disappointment">Спасибо за поддержку проекта WireGuard!\n\nК сожалению, из-за политики Google, нельзя размещать ссылку на тот раздел сайта проекта, где можно сделать пожертвование. Надеемся, вы сможете разобраться самостоятельно!\n\nЕщё раз спасибо за ваш вклад.</string>
|
||||
<string name="disable_config_export_title">Отключить экспорт конфигурации</string>
|
||||
<string name="disable_config_export_description">Отключение экспорта конфигурации делает приватные ключи менее доступными</string>
|
||||
<string name="dns_servers">DNS-серверы</string>
|
||||
@@ -142,6 +145,7 @@
|
||||
<string name="error_down">Ошибка при выходе из туннеля: %s</string>
|
||||
<string name="error_fetching_apps">Ошибка при получении списка приложений: %s</string>
|
||||
<string name="error_root">Пожалуйста, получите root-доступ и попробуйте снова</string>
|
||||
<string name="error_prepare">Ошибка при подготовке туннеля: %s</string>
|
||||
<string name="error_up">Ошибка при запуске туннеля: %s</string>
|
||||
<string name="exclude_private_ips">Исключить частные IP-адреса</string>
|
||||
<string name="generate_new_private_key">Сгенерировать новый приватный ключ</string>
|
||||
@@ -161,6 +165,8 @@
|
||||
<string name="key_length_explanation_base64">: ключи WireGuard base64 должны содержать 44 символа (32 байта)</string>
|
||||
<string name="key_length_explanation_binary">: ключи WireGuard должны быть 32 байта</string>
|
||||
<string name="key_length_explanation_hex">: HEX-ключи WireGuard должны содержать 64 символа (32 байта)</string>
|
||||
<string name="latest_handshake">Последнее рукопожатие</string>
|
||||
<string name="latest_handshake_ago">%s назад</string>
|
||||
<string name="listen_port">Порт</string>
|
||||
<string name="log_export_error">Не удалось экспортировать журнал: %s</string>
|
||||
<string name="log_export_subject">Файл журнала WireGuard Android</string>
|
||||
@@ -239,6 +245,7 @@
|
||||
<string name="tunnel_create_success">Успешно создан туннель “%s”</string>
|
||||
<string name="tunnel_error_already_exists">Туннель “%s” уже существует</string>
|
||||
<string name="tunnel_error_invalid_name">Неправильное имя</string>
|
||||
<string name="tunnel_list_placeholder">Добавьте туннель с помощью кнопки ниже</string>
|
||||
<string name="tunnel_name">Название туннеля</string>
|
||||
<string name="tunnel_on_error">Не удалось включить туннель (wgTurnOn вернул %d)</string>
|
||||
<string name="tunnel_dns_failure">Не удалось определить DNS имя: “%s”</string>
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
<string name="error_down">断开连接时出错:%s</string>
|
||||
<string name="error_fetching_apps">获取应用列表时出错:%s</string>
|
||||
<string name="error_root">请获取 root 权限并重试</string>
|
||||
<string name="error_prepare">准备连接时出错:%s</string>
|
||||
<string name="error_up">建立连接时出错:%s</string>
|
||||
<string name="exclude_private_ips">排除局域网</string>
|
||||
<string name="generate_new_private_key">生成新的私钥</string>
|
||||
@@ -125,6 +126,8 @@
|
||||
<string name="key_length_explanation_base64">:WireGuard 的 Base64 密钥长度必须为 44 个字符(32 字节)</string>
|
||||
<string name="key_length_explanation_binary">:WireGuard 密钥大小必须为 32 字节</string>
|
||||
<string name="key_length_explanation_hex">:WireGuard 的十六进制密钥长度必须为 64 个字符(32 字节)</string>
|
||||
<string name="latest_handshake">上次握手时间</string>
|
||||
<string name="latest_handshake_ago">%s之前</string>
|
||||
<string name="listen_port">监听端口</string>
|
||||
<string name="log_export_error">无法导出日志:%s</string>
|
||||
<string name="log_export_subject">WireGuard 日志文件</string>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="fab_margin">16dp</dimen>
|
||||
<dimen name="extra_margin">12dp</dimen>
|
||||
<dimen name="bottom_sheet_item_height">56dp</dimen>
|
||||
<dimen name="normal_margin">8dp</dimen>
|
||||
<dimen name="bottom_sheet_top_padding">8dp</dimen>
|
||||
|
||||
@@ -140,6 +140,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="latest_handshake">Latest handshake</string>
|
||||
<string name="latest_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>
|
||||
@@ -227,6 +229,13 @@
|
||||
<string name="type_name_go_userspace">Go userspace</string>
|
||||
<string name="type_name_kernel_module">Kernel module</string>
|
||||
<string name="unknown_error">Unknown error</string>
|
||||
<string name="updater_avalable">An application update is available. Please update now.</string>
|
||||
<string name="updater_action">Download & Update</string>
|
||||
<string name="updater_rechecking">Fetching update metadata…</string>
|
||||
<string name="updater_download_progress">Downloading update: %1$s / %2$s (%3$.2f%%)</string>
|
||||
<string name="updater_download_progress_nototal">Downloading update: %s</string>
|
||||
<string name="updater_installing">Installing update…</string>
|
||||
<string name="updater_failure">Update failure: %s. Will retry momentarily…</string>
|
||||
<string name="version_summary">%1$s backend %2$s</string>
|
||||
<string name="version_summary_checking">Checking %s backend version</string>
|
||||
<string name="version_summary_unknown">Unknown %s version</string>
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
buildscript {
|
||||
ext {
|
||||
wireguardVersionCode = 498
|
||||
wireguardVersionName = '1.0.20230405'
|
||||
wireguardVersionCode = 501
|
||||
wireguardVersionName = '1.0.20230502'
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user