parse(final CharSequence line) {
43 | final Matcher matcher = LINE_PATTERN.matcher(line);
44 | if (!matcher.matches())
45 | return Optional.empty();
46 | return Optional.of(new Attribute(matcher.group(1), matcher.group(2)));
47 | }
48 |
49 | public static String[] split(final CharSequence value) {
50 | return LIST_SEPARATOR.split(value);
51 | }
52 |
53 | public String getKey() {
54 | return key;
55 | }
56 |
57 | public String getValue() {
58 | return value;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/tunnel/src/main/java/com/wireguard/config/BadConfigException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.config;
7 |
8 | import com.wireguard.crypto.KeyFormatException;
9 | import com.wireguard.util.NonNullForAll;
10 |
11 | import androidx.annotation.Nullable;
12 |
13 | @NonNullForAll
14 | public class BadConfigException extends Exception {
15 | private final Location location;
16 | private final Reason reason;
17 | private final Section section;
18 | @Nullable private final CharSequence text;
19 |
20 | private BadConfigException(final Section section, final Location location,
21 | final Reason reason, @Nullable final CharSequence text,
22 | @Nullable final Throwable cause) {
23 | super(cause);
24 | this.section = section;
25 | this.location = location;
26 | this.reason = reason;
27 | this.text = text;
28 | }
29 |
30 | public BadConfigException(final Section section, final Location location,
31 | final Reason reason, @Nullable final CharSequence text) {
32 | this(section, location, reason, text, null);
33 | }
34 |
35 | public BadConfigException(final Section section, final Location location,
36 | final KeyFormatException cause) {
37 | this(section, location, Reason.INVALID_KEY, null, cause);
38 | }
39 |
40 | public BadConfigException(final Section section, final Location location,
41 | @Nullable final CharSequence text,
42 | final NumberFormatException cause) {
43 | this(section, location, Reason.INVALID_NUMBER, text, cause);
44 | }
45 |
46 | public BadConfigException(final Section section, final Location location,
47 | final ParseException cause) {
48 | this(section, location, Reason.INVALID_VALUE, cause.getText(), cause);
49 | }
50 |
51 | public Location getLocation() {
52 | return location;
53 | }
54 |
55 | public Reason getReason() {
56 | return reason;
57 | }
58 |
59 | public Section getSection() {
60 | return section;
61 | }
62 |
63 | @Nullable
64 | public CharSequence getText() {
65 | return text;
66 | }
67 |
68 | public enum Location {
69 | TOP_LEVEL(""),
70 | ADDRESS("Address"),
71 | ALLOWED_IPS("AllowedIPs"),
72 | DNS("DNS"),
73 | ENDPOINT("Endpoint"),
74 | EXCLUDED_APPLICATIONS("ExcludedApplications"),
75 | INCLUDED_APPLICATIONS("IncludedApplications"),
76 | LISTEN_PORT("ListenPort"),
77 | MTU("MTU"),
78 | PERSISTENT_KEEPALIVE("PersistentKeepalive"),
79 | PRE_SHARED_KEY("PresharedKey"),
80 | PRIVATE_KEY("PrivateKey"),
81 | PUBLIC_KEY("PublicKey");
82 |
83 | private final String name;
84 |
85 | Location(final String name) {
86 | this.name = name;
87 | }
88 |
89 | public String getName() {
90 | return name;
91 | }
92 | }
93 |
94 | public enum Reason {
95 | INVALID_KEY,
96 | INVALID_NUMBER,
97 | INVALID_VALUE,
98 | MISSING_ATTRIBUTE,
99 | MISSING_SECTION,
100 | SYNTAX_ERROR,
101 | UNKNOWN_ATTRIBUTE,
102 | UNKNOWN_SECTION
103 | }
104 |
105 | public enum Section {
106 | CONFIG("Config"),
107 | INTERFACE("Interface"),
108 | PEER("Peer");
109 |
110 | private final String name;
111 |
112 | Section(final String name) {
113 | this.name = name;
114 | }
115 |
116 | public String getName() {
117 | return name;
118 | }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/tunnel/src/main/java/com/wireguard/config/InetNetwork.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.config;
7 |
8 | import com.wireguard.util.NonNullForAll;
9 |
10 | import java.net.Inet4Address;
11 | import java.net.InetAddress;
12 |
13 | /**
14 | * An Internet network, denoted by its address and netmask
15 | *
16 | * Instances of this class are immutable.
17 | */
18 | @NonNullForAll
19 | public final class InetNetwork {
20 | private final InetAddress address;
21 | private final int mask;
22 |
23 | private InetNetwork(final InetAddress address, final int mask) {
24 | this.address = address;
25 | this.mask = mask;
26 | }
27 |
28 | public static InetNetwork parse(final String network) throws ParseException {
29 | final int slash = network.lastIndexOf('/');
30 | final String maskString;
31 | final int rawMask;
32 | final String rawAddress;
33 | if (slash >= 0) {
34 | maskString = network.substring(slash + 1);
35 | try {
36 | rawMask = Integer.parseInt(maskString, 10);
37 | } catch (final NumberFormatException ignored) {
38 | throw new ParseException(Integer.class, maskString);
39 | }
40 | rawAddress = network.substring(0, slash);
41 | } else {
42 | maskString = "";
43 | rawMask = -1;
44 | rawAddress = network;
45 | }
46 | final InetAddress address = InetAddresses.parse(rawAddress);
47 | final int maxMask = (address instanceof Inet4Address) ? 32 : 128;
48 | if (rawMask > maxMask)
49 | throw new ParseException(InetNetwork.class, maskString, "Invalid network mask");
50 | final int mask = rawMask >= 0 ? rawMask : maxMask;
51 | return new InetNetwork(address, mask);
52 | }
53 |
54 | @Override
55 | public boolean equals(final Object obj) {
56 | if (!(obj instanceof InetNetwork))
57 | return false;
58 | final InetNetwork other = (InetNetwork) obj;
59 | return address.equals(other.address) && mask == other.mask;
60 | }
61 |
62 | public InetAddress getAddress() {
63 | return address;
64 | }
65 |
66 | public int getMask() {
67 | return mask;
68 | }
69 |
70 | @Override
71 | public int hashCode() {
72 | return address.hashCode() ^ mask;
73 | }
74 |
75 | @Override
76 | public String toString() {
77 | return address.getHostAddress() + '/' + mask;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/tunnel/src/main/java/com/wireguard/config/ParseException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.config;
7 |
8 | import com.wireguard.util.NonNullForAll;
9 |
10 | import androidx.annotation.Nullable;
11 |
12 | /**
13 | *
14 | */
15 | @NonNullForAll
16 | public class ParseException extends Exception {
17 | private final Class> parsingClass;
18 | private final CharSequence text;
19 |
20 | public ParseException(final Class> parsingClass, final CharSequence text,
21 | @Nullable final String message, @Nullable final Throwable cause) {
22 | super(message, cause);
23 | this.parsingClass = parsingClass;
24 | this.text = text;
25 | }
26 |
27 | public ParseException(final Class> parsingClass, final CharSequence text,
28 | @Nullable final String message) {
29 | this(parsingClass, text, message, null);
30 | }
31 |
32 | public ParseException(final Class> parsingClass, final CharSequence text,
33 | @Nullable final Throwable cause) {
34 | this(parsingClass, text, null, cause);
35 | }
36 |
37 | public ParseException(final Class> parsingClass, final CharSequence text) {
38 | this(parsingClass, text, null, null);
39 | }
40 |
41 | public Class> getParsingClass() {
42 | return parsingClass;
43 | }
44 |
45 | public CharSequence getText() {
46 | return text;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/tunnel/src/main/java/com/wireguard/crypto/KeyFormatException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.crypto;
7 |
8 | import com.wireguard.util.NonNullForAll;
9 |
10 | /**
11 | * An exception thrown when attempting to parse an invalid key (too short, too long, or byte
12 | * data inappropriate for the format). The format being parsed can be accessed with the
13 | * {@link #getFormat} method.
14 | */
15 | @NonNullForAll
16 | public final class KeyFormatException extends Exception {
17 | private final Key.Format format;
18 | private final Type type;
19 |
20 | KeyFormatException(final Key.Format format, final Type type) {
21 | this.format = format;
22 | this.type = type;
23 | }
24 |
25 | public Key.Format getFormat() {
26 | return format;
27 | }
28 |
29 | public Type getType() {
30 | return type;
31 | }
32 |
33 | public enum Type {
34 | CONTENTS,
35 | LENGTH
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tunnel/src/main/java/com/wireguard/crypto/KeyPair.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.crypto;
7 |
8 | import com.wireguard.util.NonNullForAll;
9 |
10 | /**
11 | * Represents a Curve25519 key pair as used by WireGuard.
12 | *
13 | * Instances of this class are immutable.
14 | */
15 | @NonNullForAll
16 | public class KeyPair {
17 | private final Key privateKey;
18 | private final Key publicKey;
19 |
20 | /**
21 | * Creates a key pair using a newly-generated private key.
22 | */
23 | public KeyPair() {
24 | this(Key.generatePrivateKey());
25 | }
26 |
27 | /**
28 | * Creates a key pair using an existing private key.
29 | *
30 | * @param privateKey a private key, used to derive the public key
31 | */
32 | public KeyPair(final Key privateKey) {
33 | this.privateKey = privateKey;
34 | publicKey = Key.generatePublicKey(privateKey);
35 | }
36 |
37 | /**
38 | * Returns the private key from the key pair.
39 | *
40 | * @return the private key
41 | */
42 | public Key getPrivateKey() {
43 | return privateKey;
44 | }
45 |
46 | /**
47 | * Returns the public key from the key pair.
48 | *
49 | * @return the public key
50 | */
51 | public Key getPublicKey() {
52 | return publicKey;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/tunnel/src/main/java/com/wireguard/util/NonNullForAll.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.util;
7 |
8 | import java.lang.annotation.ElementType;
9 | import java.lang.annotation.Retention;
10 | import java.lang.annotation.RetentionPolicy;
11 |
12 | import javax.annotation.Nonnull;
13 | import javax.annotation.meta.TypeQualifierDefault;
14 |
15 | import androidx.annotation.RestrictTo;
16 | import androidx.annotation.RestrictTo.Scope;
17 |
18 | /**
19 | * This annotation can be applied to a package, class or method to indicate that all
20 | * class fields and method parameters and return values in that element are nonnull
21 | * by default unless overridden.
22 | */
23 | @RestrictTo(Scope.LIBRARY_GROUP)
24 | @Nonnull
25 | @TypeQualifierDefault({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
26 | @Retention(RetentionPolicy.RUNTIME)
27 |
28 | public @interface NonNullForAll {
29 | }
30 |
--------------------------------------------------------------------------------
/tunnel/src/test/java/com/wireguard/config/ConfigTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.config;
7 |
8 | import org.junit.Test;
9 |
10 | import java.io.IOException;
11 | import java.io.InputStream;
12 | import java.util.Arrays;
13 | import java.util.Collection;
14 | import java.util.HashSet;
15 | import java.util.Objects;
16 |
17 | import static org.junit.Assert.assertEquals;
18 | import static org.junit.Assert.assertNotNull;
19 | import static org.junit.Assert.assertTrue;
20 | import static org.junit.Assert.fail;
21 |
22 | public class ConfigTest {
23 |
24 | @Test(expected = BadConfigException.class)
25 | public void invalid_config_throws() throws IOException, BadConfigException {
26 | try (final InputStream is = Objects.requireNonNull(getClass().getClassLoader()).getResourceAsStream("broken.conf")) {
27 | Config.parse(is);
28 | }
29 | }
30 |
31 | @Test
32 | public void valid_config_parses_correctly() throws IOException, ParseException {
33 | Config config = null;
34 | final Collection expectedAllowedIps = new HashSet<>(Arrays.asList(InetNetwork.parse("0.0.0.0/0"), InetNetwork.parse("::0/0")));
35 | try (final InputStream is = Objects.requireNonNull(getClass().getClassLoader()).getResourceAsStream("working.conf")) {
36 | config = Config.parse(is);
37 | } catch (final BadConfigException e) {
38 | fail("'working.conf' should never fail to parse");
39 | }
40 | assertNotNull("config cannot be null after parsing", config);
41 | assertTrue(
42 | "No applications should be excluded by default",
43 | config.getInterface().getExcludedApplications().isEmpty()
44 | );
45 | assertEquals("Test config has exactly one peer", 1, config.getPeers().size());
46 | assertEquals("Test config's allowed IPs are 0.0.0.0/0 and ::0/0", config.getPeers().get(0).getAllowedIps(), expectedAllowedIps);
47 | assertEquals("Test config has one DNS server", 1, config.getInterface().getDnsServers().size());
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/tunnel/src/test/resources/broken.conf:
--------------------------------------------------------------------------------
1 | [Interface]
2 | PrivateKey = l0lth1s1sd3f1n1t3lybr0k3n=
3 | Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
4 | DNS = 192.0.2.0
5 |
6 | [Peer]
7 | PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
8 | AllowedIPs = 0.0.0.0/0,::0/0
9 | Endpoint = 192.0.2.1:51820
10 |
--------------------------------------------------------------------------------
/tunnel/src/test/resources/invalid-key.conf:
--------------------------------------------------------------------------------
1 | [Interface]
2 | Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
3 | DNS = 192.0.2.0
4 | PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
5 | [Peer]
6 | AllowedIPs = 0.0.0.0/0, ::0/0
7 | Endpoint = 192.0.2.1:51820
8 | PersistentKeepalive = 0
9 | PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6Og=
10 |
--------------------------------------------------------------------------------
/tunnel/src/test/resources/invalid-number.conf:
--------------------------------------------------------------------------------
1 | [Interface]
2 | Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
3 | DNS = 192.0.2.0
4 | PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
5 | [Peer]
6 | AllowedIPs = 0.0.0.0/0, ::0/0
7 | Endpoint = 192.0.2.1:51820
8 | PersistentKeepalive = 0L
9 | PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
10 |
--------------------------------------------------------------------------------
/tunnel/src/test/resources/invalid-value.conf:
--------------------------------------------------------------------------------
1 | [Interface]
2 | Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
3 | DNS = 192.0.2.0,invalid_value
4 | PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
5 | [Peer]
6 | AllowedIPs = 0.0.0.0/0, ::0/0
7 | Endpoint = 192.0.2.1:51820
8 | PersistentKeepalive = 0
9 | PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
10 |
--------------------------------------------------------------------------------
/tunnel/src/test/resources/missing-attribute.conf:
--------------------------------------------------------------------------------
1 | [Interface]
2 | Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
3 | DNS = 192.0.2.0
4 | PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
5 | [Peer]
6 | AllowedIPs = 0.0.0.0/0, ::0/0
7 | Endpoint = 192.0.2.1:51820
8 | PersistentKeepalive = 0
9 |
--------------------------------------------------------------------------------
/tunnel/src/test/resources/missing-section.conf:
--------------------------------------------------------------------------------
1 | [Peer]
2 | AllowedIPs = 0.0.0.0/0, ::0/0
3 | Endpoint = 192.0.2.1:51820
4 | PersistentKeepalive = 0
5 | PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
6 |
--------------------------------------------------------------------------------
/tunnel/src/test/resources/syntax-error.conf:
--------------------------------------------------------------------------------
1 | [Interface]
2 | Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
3 | DNS = 192.0.2.0
4 | PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
5 | [Peer]
6 | AllowedIPs = 0.0.0.0/0, ::0/0
7 | Endpoint =
8 | PersistentKeepalive = 0
9 | PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
10 |
--------------------------------------------------------------------------------
/tunnel/src/test/resources/unknown-attribute.conf:
--------------------------------------------------------------------------------
1 | [Interface]
2 | Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
3 | DNS = 192.0.2.0
4 | PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
5 | [Peer]
6 | AllowedIPs = 0.0.0.0/0, ::0/0
7 | Endpoint = 192.0.2.1:51820
8 | DontLetTheFeelingFade = 1
9 | PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
10 |
--------------------------------------------------------------------------------
/tunnel/src/test/resources/unknown-section.conf:
--------------------------------------------------------------------------------
1 | [Interface]
2 | Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
3 | DNS = 192.0.2.0
4 | PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
5 | [Peers]
6 | AllowedIPs = 0.0.0.0/0, ::0/0
7 | Endpoint = 192.0.2.1:51820
8 | PersistentKeepalive = 0
9 | PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
10 |
--------------------------------------------------------------------------------
/tunnel/src/test/resources/working.conf:
--------------------------------------------------------------------------------
1 | [Interface]
2 | Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
3 | DNS = 192.0.2.0
4 | PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
5 | [Peer]
6 | AllowedIPs = 0.0.0.0/0, ::0/0
7 | Endpoint = 192.0.2.1:51820
8 | PersistentKeepalive = 0
9 | PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
10 |
--------------------------------------------------------------------------------
/tunnel/tools/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: Apache-2.0
2 | #
3 | # Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
4 |
5 | cmake_minimum_required(VERSION 3.4.1)
6 | project("WireGuard")
7 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
8 | add_link_options(LINKER:--build-id=none)
9 | add_compile_options(-Wall -Werror)
10 |
11 | add_executable(libwg-quick.so wireguard-tools/src/wg-quick/android.c ndk-compat/compat.c)
12 | target_compile_options(libwg-quick.so PUBLIC -std=gnu11 -include ${CMAKE_CURRENT_SOURCE_DIR}/ndk-compat/compat.h -DWG_PACKAGE_NAME=\"${ANDROID_PACKAGE_NAME}\")
13 | target_link_libraries(libwg-quick.so -ldl)
14 |
15 | file(GLOB WG_SOURCES wireguard-tools/src/*.c ndk-compat/compat.c)
16 | add_executable(libwg.so ${WG_SOURCES})
17 | target_include_directories(libwg.so PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/uapi/linux/" "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/")
18 | target_compile_options(libwg.so PUBLIC -std=gnu11 -include ${CMAKE_CURRENT_SOURCE_DIR}/ndk-compat/compat.h -DRUNSTATEDIR=\"/data/data/${ANDROID_PACKAGE_NAME}/cache\")
19 |
20 | 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"
21 | ANDROID_ARCH_NAME=${ANDROID_ARCH_NAME}
22 | ANDROID_PACKAGE_NAME=${ANDROID_PACKAGE_NAME}
23 | GRADLE_USER_HOME=${GRADLE_USER_HOME}
24 | CC=${CMAKE_C_COMPILER}
25 | CFLAGS=${CMAKE_C_FLAGS}
26 | LDFLAGS=${CMAKE_SHARED_LINKER_FLAGS}
27 | SYSROOT=${CMAKE_SYSROOT}
28 | TARGET=${CMAKE_C_COMPILER_TARGET}
29 | DESTDIR=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
30 | BUILDDIR=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/../generated-src
31 | )
32 |
33 | # Strip unwanted ELF sections to prevent DT_FLAGS_1 warnings on old Android versions
34 | file(GLOB ELF_CLEANER_SOURCES elf-cleaner/*.c elf-cleaner/*.cpp)
35 | add_custom_target(elf-cleaner COMMENT "Building elf-cleaner" VERBATIM COMMAND cc
36 | -O2 -DPACKAGE_NAME="elf-cleaner" -DPACKAGE_VERSION="" -DCOPYRIGHT=""
37 | -o "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner" ${ELF_CLEANER_SOURCES}
38 | )
39 | add_custom_command(TARGET libwg.so POST_BUILD VERBATIM COMMAND "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner"
40 | --api-level "${ANDROID_NATIVE_API_LEVEL}" "$")
41 | add_dependencies(libwg.so elf-cleaner)
42 | add_custom_command(TARGET libwg-quick.so POST_BUILD VERBATIM COMMAND "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner"
43 | --api-level "${ANDROID_NATIVE_API_LEVEL}" "$")
44 | add_dependencies(libwg-quick.so elf-cleaner)
45 |
--------------------------------------------------------------------------------
/tunnel/tools/libwg-go/.gitignore:
--------------------------------------------------------------------------------
1 | build/
--------------------------------------------------------------------------------
/tunnel/tools/libwg-go/Makefile:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: Apache-2.0
2 | #
3 | # Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
4 |
5 | BUILDDIR ?= $(CURDIR)/build
6 | DESTDIR ?= $(CURDIR)/out
7 |
8 | NDK_GO_ARCH_MAP_x86 := 386
9 | NDK_GO_ARCH_MAP_x86_64 := amd64
10 | NDK_GO_ARCH_MAP_arm := arm
11 | NDK_GO_ARCH_MAP_arm64 := arm64
12 | NDK_GO_ARCH_MAP_mips := mipsx
13 | NDK_GO_ARCH_MAP_mips64 := mips64x
14 |
15 | comma := ,
16 | CLANG_FLAGS := --target=$(TARGET) --sysroot=$(SYSROOT)
17 | export CGO_CFLAGS := $(CLANG_FLAGS) $(subst -mthumb,-marm,$(CFLAGS))
18 | export CGO_LDFLAGS := $(CLANG_FLAGS) $(patsubst -Wl$(comma)--build-id=%,-Wl$(comma)--build-id=none,$(LDFLAGS)) -Wl,-soname=libwg-go.so
19 | export GOARCH := $(NDK_GO_ARCH_MAP_$(ANDROID_ARCH_NAME))
20 | export GOOS := android
21 | export CGO_ENABLED := 1
22 |
23 | GO_VERSION := 1.24.3
24 | GO_PLATFORM := $(shell uname -s | tr '[:upper:]' '[:lower:]')-$(NDK_GO_ARCH_MAP_$(shell uname -m))
25 | GO_TARBALL := go$(GO_VERSION).$(GO_PLATFORM).tar.gz
26 | GO_HASH_darwin-amd64 := 13e6fe3fcf65689d77d40e633de1e31c6febbdbcb846eb05fc2434ed2213e92b
27 | GO_HASH_darwin-arm64 := 64a3fa22142f627e78fac3018ce3d4aeace68b743eff0afda8aae0411df5e4fb
28 | GO_HASH_linux-amd64 := 3333f6ea53afa971e9078895eaa4ac7204a8c6b5c68c10e6bc9a33e8e391bdd8
29 |
30 | default: $(DESTDIR)/libwg-go.so
31 |
32 | $(GRADLE_USER_HOME)/caches/golang/$(GO_TARBALL):
33 | mkdir -p "$(dir $@)"
34 | flock "$@.lock" -c ' \
35 | [ -f "$@" ] && exit 0; \
36 | curl -o "$@.tmp" "https://dl.google.com/go/$(GO_TARBALL)" && \
37 | echo "$(GO_HASH_$(GO_PLATFORM)) $@.tmp" | sha256sum -c && \
38 | mv "$@.tmp" "$@"'
39 |
40 | $(BUILDDIR)/go-$(GO_VERSION)/.prepared: $(GRADLE_USER_HOME)/caches/golang/$(GO_TARBALL)
41 | mkdir -p "$(dir $@)"
42 | flock "$@.lock" -c ' \
43 | [ -f "$@" ] && exit 0; \
44 | tar -C "$(dir $@)" --strip-components=1 -xzf "$^" && \
45 | patch -p1 -f -N -r- -d "$(dir $@)" < goruntime-boottime-over-monotonic.diff && \
46 | touch "$@"'
47 |
48 | $(DESTDIR)/libwg-go.so: export PATH := $(BUILDDIR)/go-$(GO_VERSION)/bin/:$(PATH)
49 | $(DESTDIR)/libwg-go.so: $(BUILDDIR)/go-$(GO_VERSION)/.prepared go.mod
50 | 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
51 |
52 | .DELETE_ON_ERROR:
53 |
--------------------------------------------------------------------------------
/tunnel/tools/libwg-go/go.mod:
--------------------------------------------------------------------------------
1 | module golang.zx2c4.com/wireguard/android
2 |
3 | go 1.23.1
4 |
5 | require (
6 | golang.org/x/sys v0.33.0
7 | golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
8 | )
9 |
10 | require (
11 | golang.org/x/crypto v0.38.0 // indirect
12 | golang.org/x/net v0.40.0 // indirect
13 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
14 | )
15 |
--------------------------------------------------------------------------------
/tunnel/tools/libwg-go/go.sum:
--------------------------------------------------------------------------------
1 | github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
2 | github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
3 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
4 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
5 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
6 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
7 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
8 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
9 | golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
10 | golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
11 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
12 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
13 | golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
14 | golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
15 | gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
16 | gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
17 |
--------------------------------------------------------------------------------
/tunnel/tools/libwg-go/jni.c:
--------------------------------------------------------------------------------
1 | /* SPDX-License-Identifier: Apache-2.0
2 | *
3 | * Copyright © 2017-2021 Jason A. Donenfeld . All Rights Reserved.
4 | */
5 |
6 | #include
7 | #include
8 | #include
9 |
10 | struct go_string { const char *str; long n; };
11 | extern int wgTurnOn(struct go_string ifname, int tun_fd, struct go_string settings);
12 | extern void wgTurnOff(int handle);
13 | extern int wgGetSocketV4(int handle);
14 | extern int wgGetSocketV6(int handle);
15 | extern char *wgGetConfig(int handle);
16 | extern char *wgVersion();
17 |
18 | JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgTurnOn(JNIEnv *env, jclass c, jstring ifname, jint tun_fd, jstring settings)
19 | {
20 | const char *ifname_str = (*env)->GetStringUTFChars(env, ifname, 0);
21 | size_t ifname_len = (*env)->GetStringUTFLength(env, ifname);
22 | const char *settings_str = (*env)->GetStringUTFChars(env, settings, 0);
23 | size_t settings_len = (*env)->GetStringUTFLength(env, settings);
24 | int ret = wgTurnOn((struct go_string){
25 | .str = ifname_str,
26 | .n = ifname_len
27 | }, tun_fd, (struct go_string){
28 | .str = settings_str,
29 | .n = settings_len
30 | });
31 | (*env)->ReleaseStringUTFChars(env, ifname, ifname_str);
32 | (*env)->ReleaseStringUTFChars(env, settings, settings_str);
33 | return ret;
34 | }
35 |
36 | JNIEXPORT void JNICALL Java_com_wireguard_android_backend_GoBackend_wgTurnOff(JNIEnv *env, jclass c, jint handle)
37 | {
38 | wgTurnOff(handle);
39 | }
40 |
41 | JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgGetSocketV4(JNIEnv *env, jclass c, jint handle)
42 | {
43 | return wgGetSocketV4(handle);
44 | }
45 |
46 | JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgGetSocketV6(JNIEnv *env, jclass c, jint handle)
47 | {
48 | return wgGetSocketV6(handle);
49 | }
50 |
51 | JNIEXPORT jstring JNICALL Java_com_wireguard_android_backend_GoBackend_wgGetConfig(JNIEnv *env, jclass c, jint handle)
52 | {
53 | jstring ret;
54 | char *config = wgGetConfig(handle);
55 | if (!config)
56 | return NULL;
57 | ret = (*env)->NewStringUTF(env, config);
58 | free(config);
59 | return ret;
60 | }
61 |
62 | JNIEXPORT jstring JNICALL Java_com_wireguard_android_backend_GoBackend_wgVersion(JNIEnv *env, jclass c)
63 | {
64 | jstring ret;
65 | char *version = wgVersion();
66 | if (!version)
67 | return NULL;
68 | ret = (*env)->NewStringUTF(env, version);
69 | free(version);
70 | return ret;
71 | }
72 |
--------------------------------------------------------------------------------
/tunnel/tools/ndk-compat/compat.c:
--------------------------------------------------------------------------------
1 | /* SPDX-License-Identifier: BSD
2 | *
3 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
4 | *
5 | */
6 |
7 | #define FILE_IS_EMPTY
8 |
9 | #if defined(__ANDROID_MIN_SDK_VERSION__) && __ANDROID_MIN_SDK_VERSION__ < 24
10 | #undef FILE_IS_EMPTY
11 | #include
12 |
13 | char *strchrnul(const char *s, int c)
14 | {
15 | char *x = strchr(s, c);
16 | if (!x)
17 | return (char *)s + strlen(s);
18 | return x;
19 | }
20 | #endif
21 |
22 | #ifdef FILE_IS_EMPTY
23 | #undef FILE_IS_EMPTY
24 | static char ____x __attribute__((unused));
25 | #endif
26 |
--------------------------------------------------------------------------------
/tunnel/tools/ndk-compat/compat.h:
--------------------------------------------------------------------------------
1 | /* SPDX-License-Identifier: BSD
2 | *
3 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
4 | *
5 | */
6 |
7 | #if defined(__ANDROID_MIN_SDK_VERSION__) && __ANDROID_MIN_SDK_VERSION__ < 24
8 | char *strchrnul(const char *s, int c);
9 | #endif
10 |
11 |
--------------------------------------------------------------------------------
/ui/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage")
2 |
3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
4 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
5 |
6 | val pkg: String = providers.gradleProperty("wireguardPackageName").get()
7 |
8 | plugins {
9 | alias(libs.plugins.android.application)
10 | alias(libs.plugins.kotlin.android)
11 | alias(libs.plugins.kotlin.kapt)
12 | }
13 |
14 | android {
15 | compileSdk = 36
16 | buildFeatures {
17 | buildConfig = true
18 | dataBinding = true
19 | viewBinding = true
20 | }
21 | namespace = pkg
22 | defaultConfig {
23 | applicationId = pkg
24 | minSdk = 21
25 | targetSdk = 36
26 | versionCode = providers.gradleProperty("wireguardVersionCode").get().toInt()
27 | versionName = providers.gradleProperty("wireguardVersionName").get()
28 | buildConfigField("int", "MIN_SDK_VERSION", minSdk.toString())
29 |
30 | ndk {
31 | abiFilters.add("arm64-v8a")
32 | // abiFilters.add("armeabi-v7a")
33 | }
34 | }
35 | compileOptions {
36 | sourceCompatibility = JavaVersion.VERSION_17
37 | targetCompatibility = JavaVersion.VERSION_17
38 | isCoreLibraryDesugaringEnabled = true
39 | }
40 |
41 | /* signingConfigs {
42 | create("release") {
43 | storeFile = file("buildsign.jks")
44 | storePassword = ""
45 | keyAlias = "wireguard"
46 | keyPassword = ""
47 | }
48 | } */
49 |
50 | buildTypes {
51 | release {
52 | isMinifyEnabled = true
53 | isShrinkResources = true
54 | proguardFiles("proguard-android-optimize.txt")
55 | // signingConfig = signingConfigs.getByName("release")
56 | packaging {
57 | resources {
58 | excludes += "DebugProbesKt.bin"
59 | excludes += "kotlin-tooling-metadata.json"
60 | excludes += "META-INF/*.version"
61 | }
62 | }
63 | }
64 | debug {
65 | applicationIdSuffix = ".debug"
66 | versionNameSuffix = "-debug"
67 | }
68 | create("googleplay") {
69 | initWith(getByName("release"))
70 | matchingFallbacks += "release"
71 | }
72 | }
73 | androidResources {
74 | generateLocaleConfig = true
75 | }
76 | lint {
77 | disable += "LongLogTag"
78 | warning += "MissingTranslation"
79 | warning += "ImpliedQuantity"
80 | }
81 | }
82 |
83 | dependencies {
84 | implementation(project(":tunnel"))
85 | implementation(libs.androidx.activity.ktx)
86 | implementation(libs.androidx.annotation)
87 | implementation(libs.androidx.appcompat)
88 | implementation(libs.androidx.constraintlayout)
89 | implementation(libs.androidx.coordinatorlayout)
90 | implementation(libs.androidx.biometric)
91 | implementation(libs.androidx.core.ktx)
92 | implementation(libs.androidx.fragment.ktx)
93 | implementation(libs.androidx.preference.ktx)
94 | implementation(libs.androidx.lifecycle.runtime.ktx)
95 | implementation(libs.androidx.datastore.preferences)
96 | implementation(libs.google.material)
97 | implementation(libs.zxing.android.embedded)
98 | implementation(libs.kotlinx.coroutines.android)
99 | coreLibraryDesugaring(libs.desugarJdkLibs)
100 | }
101 |
102 | tasks.withType().configureEach {
103 | options.compilerArgs.add("-Xlint:unchecked")
104 | options.isDeprecation = true
105 | }
106 |
107 | tasks.withType().configureEach {
108 | compilerOptions.jvmTarget = JvmTarget.JVM_17
109 | }
110 |
--------------------------------------------------------------------------------
/ui/proguard-android-optimize.txt:
--------------------------------------------------------------------------------
1 | -dontwarn android.os.SystemProperties
2 | -dontwarn com.sun.jna.Library
3 | -dontwarn com.sun.jna.Memory
4 | -dontwarn com.sun.jna.Native
5 | -dontwarn com.sun.jna.Pointer
6 | -dontwarn com.sun.jna.Structure$ByReference
7 | -dontwarn com.sun.jna.Structure$FieldOrder
8 | -dontwarn com.sun.jna.Structure
9 | -dontwarn com.sun.jna.WString
10 | -dontwarn com.sun.jna.platform.win32.Guid$GUID
11 | -dontwarn com.sun.jna.platform.win32.Win32Exception
12 | -dontwarn com.sun.jna.ptr.IntByReference
13 | -dontwarn com.sun.jna.win32.W32APIOptions
14 | -dontwarn javax.naming.NamingException
15 | -dontwarn javax.naming.directory.DirContext
16 | -dontwarn javax.naming.directory.InitialDirContext
17 | -dontwarn org.bouncycastle.asn1.ASN1ObjectIdentifier
18 | -dontwarn org.bouncycastle.asn1.edec.EdECObjectIdentifiers
19 | -dontwarn org.bouncycastle.asn1.x509.AlgorithmIdentifier
20 | -dontwarn org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
21 | -dontwarn org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPublicKey
22 | -dontwarn org.slf4j.impl.StaticLoggerBinder
23 | -dontwarn sun.net.spi.nameservice.NameService
24 | -dontwarn sun.net.spi.nameservice.NameServiceDescriptor
25 |
26 | -dontwarn lombok.Generated
27 | -dontwarn org.xbill.DNS.spi.DnsjavaInetAddressResolverProvider
28 |
29 | -allowaccessmodification
30 | -dontusemixedcaseclassnames
31 | -dontobfuscate
32 | -verbose
33 |
34 | -keepattributes *Annotation*
35 |
36 | -keepclasseswithmembernames class * {
37 | native ;
38 | }
39 |
40 | -keepclassmembers enum * {
41 | public static **[] values();
42 | public static ** valueOf(java.lang.String);
43 | }
44 |
45 | -keepclassmembers class * implements android.os.Parcelable {
46 | public static final ** CREATOR;
47 | }
48 |
49 | -keep class androidx.annotation.Keep
50 |
51 | -keep @androidx.annotation.Keep class * {*;}
52 |
53 | -keepclasseswithmembers class * {
54 | @androidx.annotation.Keep ;
55 | }
56 |
57 | -keepclasseswithmembers class * {
58 | @androidx.annotation.Keep ;
59 | }
60 |
61 | -keepclasseswithmembers class * {
62 | @androidx.annotation.Keep (...);
63 | }
64 |
65 |
--------------------------------------------------------------------------------
/ui/sampledata/interface_names.json:
--------------------------------------------------------------------------------
1 | {
2 | "comment": "Interface names",
3 | "names": [
4 | {
5 | "names": [
6 | { "name": "wg0" },
7 | { "name": "wg1" },
8 | { "name": "wg2" },
9 | { "name": "wg3" },
10 | { "name": "wg4" },
11 | { "name": "wg5" },
12 | { "name": "wg6" },
13 | { "name": "wg7" },
14 | { "name": "wg8" },
15 | { "name": "wg9" },
16 | { "name": "wg10" },
17 | { "name": "wg11" }
18 | ],
19 | "checked": [
20 | { "checked": true },
21 | { "checked": false },
22 | { "checked": true },
23 | { "checked": false },
24 | { "checked": true },
25 | { "checked": false },
26 | { "checked": true },
27 | { "checked": false },
28 | { "checked": true },
29 | { "checked": false },
30 | { "checked": true }
31 | ]
32 | }
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/ui/src/debug/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | WireGuard β
4 |
5 |
--------------------------------------------------------------------------------
/ui/src/googleplay/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
5 |
7 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/BootShutdownReceiver.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android
6 |
7 | import android.content.BroadcastReceiver
8 | import android.content.Context
9 | import android.content.Intent
10 | import android.util.Log
11 | import com.wireguard.android.backend.WgQuickBackend
12 | import com.wireguard.android.util.applicationScope
13 | import kotlinx.coroutines.launch
14 |
15 | class BootShutdownReceiver : BroadcastReceiver() {
16 | override fun onReceive(context: Context, intent: Intent) {
17 | val action = intent.action ?: return
18 | applicationScope.launch {
19 | if (Application.getBackend() !is WgQuickBackend) return@launch
20 | val tunnelManager = Application.getTunnelManager()
21 | if (Intent.ACTION_BOOT_COMPLETED == action) {
22 | Log.i(TAG, "Broadcast receiver restoring state (boot)")
23 | tunnelManager.restoreState(false)
24 | } else if (Intent.ACTION_SHUTDOWN == action) {
25 | Log.i(TAG, "Broadcast receiver saving state (shutdown)")
26 | tunnelManager.saveState()
27 | }
28 | }
29 | }
30 |
31 | companion object {
32 | private const val TAG = "WireGuard/BootShutdownReceiver"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.activity
6 |
7 | import android.os.Bundle
8 | import androidx.appcompat.app.AppCompatActivity
9 | import androidx.databinding.CallbackRegistry
10 | import androidx.databinding.CallbackRegistry.NotifierCallback
11 | import androidx.lifecycle.lifecycleScope
12 | import com.wireguard.android.Application
13 | import com.wireguard.android.model.ObservableTunnel
14 | import kotlinx.coroutines.launch
15 |
16 | /**
17 | * Base class for activities that need to remember the currently-selected tunnel.
18 | */
19 | abstract class BaseActivity : AppCompatActivity() {
20 | private val selectionChangeRegistry = SelectionChangeRegistry()
21 | private var created = false
22 | var selectedTunnel: ObservableTunnel? = null
23 | set(value) {
24 | val oldTunnel = field
25 | if (oldTunnel == value) return
26 | field = value
27 | if (created) {
28 | if (!onSelectedTunnelChanged(oldTunnel, value)) {
29 | field = oldTunnel
30 | } else {
31 | selectionChangeRegistry.notifyCallbacks(oldTunnel, 0, value)
32 | }
33 | }
34 | }
35 |
36 | fun addOnSelectedTunnelChangedListener(listener: OnSelectedTunnelChangedListener) {
37 | selectionChangeRegistry.add(listener)
38 | }
39 |
40 | override fun onCreate(savedInstanceState: Bundle?) {
41 | super.onCreate(savedInstanceState)
42 |
43 | // Restore the saved tunnel if there is one; otherwise grab it from the arguments.
44 | val savedTunnelName = when {
45 | savedInstanceState != null -> savedInstanceState.getString(KEY_SELECTED_TUNNEL)
46 | intent != null -> intent.getStringExtra(KEY_SELECTED_TUNNEL)
47 | else -> null
48 | }
49 | if (savedTunnelName != null) {
50 | lifecycleScope.launch {
51 | val tunnel = Application.getTunnelManager().getTunnels()[savedTunnelName]
52 | if (tunnel == null)
53 | created = true
54 | selectedTunnel = tunnel
55 | created = true
56 | }
57 | } else {
58 | created = true
59 | }
60 | }
61 |
62 | override fun onSaveInstanceState(outState: Bundle) {
63 | if (selectedTunnel != null) outState.putString(KEY_SELECTED_TUNNEL, selectedTunnel!!.name)
64 | super.onSaveInstanceState(outState)
65 | }
66 |
67 | protected abstract fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?): Boolean
68 |
69 | fun removeOnSelectedTunnelChangedListener(
70 | listener: OnSelectedTunnelChangedListener
71 | ) {
72 | selectionChangeRegistry.remove(listener)
73 | }
74 |
75 | interface OnSelectedTunnelChangedListener {
76 | fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?)
77 | }
78 |
79 | private class SelectionChangeNotifier : NotifierCallback() {
80 | override fun onNotifyCallback(
81 | listener: OnSelectedTunnelChangedListener,
82 | oldTunnel: ObservableTunnel?,
83 | ignored: Int,
84 | newTunnel: ObservableTunnel?
85 | ) {
86 | listener.onSelectedTunnelChanged(oldTunnel, newTunnel)
87 | }
88 | }
89 |
90 | private class SelectionChangeRegistry :
91 | CallbackRegistry(SelectionChangeNotifier())
92 |
93 | companion object {
94 | private const val KEY_SELECTED_TUNNEL = "selected_tunnel"
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.activity
6 |
7 | import android.os.Bundle
8 | import com.wireguard.android.R
9 | import com.wireguard.android.model.ObservableTunnel
10 |
11 | /**
12 | * Standalone activity for creating tunnels.
13 | */
14 | class TunnelCreatorActivity : BaseActivity() {
15 | override fun onCreate(savedInstanceState: Bundle?) {
16 | super.onCreate(savedInstanceState)
17 | setContentView(R.layout.tunnel_creator_activity)
18 | }
19 |
20 | override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?): Boolean {
21 | finish()
22 | return true
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.activity
6 |
7 | import android.content.ComponentName
8 | import android.os.Build
9 | import android.os.Bundle
10 | import android.service.quicksettings.TileService
11 | import android.util.Log
12 | import android.widget.Toast
13 | import androidx.activity.result.contract.ActivityResultContracts
14 | import androidx.annotation.RequiresApi
15 | import androidx.appcompat.app.AppCompatActivity
16 | import androidx.lifecycle.lifecycleScope
17 | import com.wireguard.android.Application
18 | import com.wireguard.android.QuickTileService
19 | import com.wireguard.android.R
20 | import com.wireguard.android.backend.GoBackend
21 | import com.wireguard.android.backend.Tunnel
22 | import com.wireguard.android.util.ErrorMessages
23 | import kotlinx.coroutines.launch
24 |
25 | @RequiresApi(Build.VERSION_CODES.N)
26 | class TunnelToggleActivity : AppCompatActivity() {
27 | private val permissionActivityResultLauncher =
28 | registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { toggleTunnelWithPermissionsResult() }
29 |
30 | private fun toggleTunnelWithPermissionsResult() {
31 | val tunnel = Application.getTunnelManager().lastUsedTunnel ?: return
32 | lifecycleScope.launch {
33 | try {
34 | tunnel.setStateAsync(Tunnel.State.TOGGLE)
35 | } catch (e: Throwable) {
36 | TileService.requestListeningState(this@TunnelToggleActivity, ComponentName(this@TunnelToggleActivity, QuickTileService::class.java))
37 | val error = ErrorMessages[e]
38 | val message = getString(R.string.toggle_error, error)
39 | Log.e(TAG, message, e)
40 | Toast.makeText(this@TunnelToggleActivity, message, Toast.LENGTH_LONG).show()
41 | finishAffinity()
42 | return@launch
43 | }
44 | TileService.requestListeningState(this@TunnelToggleActivity, ComponentName(this@TunnelToggleActivity, QuickTileService::class.java))
45 | finishAffinity()
46 | }
47 | }
48 |
49 | override fun onCreate(savedInstanceState: Bundle?) {
50 | super.onCreate(savedInstanceState)
51 | lifecycleScope.launch {
52 | if (Application.getBackend() is GoBackend) {
53 | try {
54 | val intent = GoBackend.VpnService.prepare(this@TunnelToggleActivity)
55 | if (intent != null) {
56 | permissionActivityResultLauncher.launch(intent)
57 | return@launch
58 | }
59 | } catch (e: Exception) {
60 | Toast.makeText(this@TunnelToggleActivity, ErrorMessages[e], Toast.LENGTH_LONG).show()
61 | }
62 | }
63 | toggleTunnelWithPermissionsResult()
64 | }
65 | }
66 |
67 | companion object {
68 | private const val TAG = "WireGuard/TunnelToggleActivity"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/configStore/ConfigStore.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.configStore
6 |
7 | import com.wireguard.config.Config
8 |
9 | /**
10 | * Interface for persistent storage providers for WireGuard configurations.
11 | */
12 | interface ConfigStore {
13 | /**
14 | * Create a persistent tunnel, which must have a unique name within the persistent storage
15 | * medium.
16 | *
17 | * @param name The name of the tunnel to create.
18 | * @param config Configuration for the new tunnel.
19 | * @return The configuration that was actually saved to persistent storage.
20 | */
21 | @Throws(Exception::class)
22 | fun create(name: String, config: Config): Config
23 |
24 | /**
25 | * Delete a persistent tunnel.
26 | *
27 | * @param name The name of the tunnel to delete.
28 | */
29 | @Throws(Exception::class)
30 | fun delete(name: String)
31 |
32 | /**
33 | * Enumerate the names of tunnels present in persistent storage.
34 | *
35 | * @return The set of present tunnel names.
36 | */
37 | fun enumerate(): Set
38 |
39 | /**
40 | * Load the configuration for the tunnel given by `name`.
41 | *
42 | * @param name The identifier for the configuration in persistent storage (i.e. the name of the
43 | * tunnel).
44 | * @return An in-memory representation of the configuration loaded from persistent storage.
45 | */
46 | @Throws(Exception::class)
47 | fun load(name: String): Config
48 |
49 | /**
50 | * Rename the configuration for the tunnel given by `name`.
51 | *
52 | * @param name The identifier for the existing configuration in persistent storage.
53 | * @param replacement The new identifier for the configuration in persistent storage.
54 | */
55 | @Throws(Exception::class)
56 | fun rename(name: String, replacement: String)
57 |
58 | /**
59 | * Save the configuration for an existing tunnel given by `name`.
60 | *
61 | * @param name The identifier for the configuration in persistent storage (i.e. the name of
62 | * the tunnel).
63 | * @param config An updated configuration object for the tunnel.
64 | * @return The configuration that was actually saved to persistent storage.
65 | */
66 | @Throws(Exception::class)
67 | fun save(name: String, config: Config): Config
68 | }
69 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/configStore/FileConfigStore.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.configStore
6 |
7 | import android.content.Context
8 | import android.util.Log
9 | import com.wireguard.android.R
10 | import com.wireguard.config.BadConfigException
11 | import com.wireguard.config.Config
12 | import java.io.File
13 | import java.io.FileInputStream
14 | import java.io.FileNotFoundException
15 | import java.io.FileOutputStream
16 | import java.io.IOException
17 | import java.nio.charset.StandardCharsets
18 |
19 | /**
20 | * Configuration store that uses a `wg-quick`-style file for each configured tunnel.
21 | */
22 | class FileConfigStore(private val context: Context) : ConfigStore {
23 | @Throws(IOException::class)
24 | override fun create(name: String, config: Config): Config {
25 | Log.d(TAG, "Creating configuration for tunnel $name")
26 | val file = fileFor(name)
27 | if (!file.createNewFile())
28 | throw IOException(context.getString(R.string.config_file_exists_error, file.name))
29 | FileOutputStream(file, false).use { it.write(config.toWgQuickString().toByteArray(StandardCharsets.UTF_8)) }
30 | return config
31 | }
32 |
33 | @Throws(IOException::class)
34 | override fun delete(name: String) {
35 | Log.d(TAG, "Deleting configuration for tunnel $name")
36 | val file = fileFor(name)
37 | if (!file.delete())
38 | throw IOException(context.getString(R.string.config_delete_error, file.name))
39 | }
40 |
41 | override fun enumerate(): Set {
42 | return context.fileList()
43 | .filter { it.endsWith(".conf") }
44 | .map { it.substring(0, it.length - ".conf".length) }
45 | .toSet()
46 | }
47 |
48 | private fun fileFor(name: String): File {
49 | return File(context.filesDir, "$name.conf")
50 | }
51 |
52 | @Throws(BadConfigException::class, IOException::class)
53 | override fun load(name: String): Config {
54 | FileInputStream(fileFor(name)).use { stream -> return Config.parse(stream) }
55 | }
56 |
57 | @Throws(IOException::class)
58 | override fun rename(name: String, replacement: String) {
59 | Log.d(TAG, "Renaming configuration for tunnel $name to $replacement")
60 | val file = fileFor(name)
61 | val replacementFile = fileFor(replacement)
62 | if (!replacementFile.createNewFile()) throw IOException(context.getString(R.string.config_exists_error, replacement))
63 | if (!file.renameTo(replacementFile)) {
64 | if (!replacementFile.delete()) Log.w(TAG, "Couldn't delete marker file for new name $replacement")
65 | throw IOException(context.getString(R.string.config_rename_error, file.name))
66 | }
67 | }
68 |
69 | @Throws(IOException::class)
70 | override fun save(name: String, config: Config): Config {
71 | Log.d(TAG, "Saving configuration for tunnel $name")
72 | val file = fileFor(name)
73 | if (!file.isFile)
74 | throw FileNotFoundException(context.getString(R.string.config_not_found_error, file.name))
75 | FileOutputStream(file, false).use { stream -> stream.write(config.toWgQuickString().toByteArray(StandardCharsets.UTF_8)) }
76 | return config
77 | }
78 |
79 | companion object {
80 | private const val TAG = "WireGuard/FileConfigStore"
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/databinding/Keyed.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.databinding
6 |
7 | /**
8 | * Interface for objects that have a identifying key of the given type.
9 | */
10 | interface Keyed {
11 | val key: K
12 | }
13 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/databinding/ObservableKeyedArrayList.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.databinding
6 |
7 | import androidx.databinding.ObservableArrayList
8 |
9 | /**
10 | * ArrayList that allows looking up elements by some key property. As the key property must always
11 | * be retrievable, this list cannot hold `null` elements. Because this class places no
12 | * restrictions on the order or duplication of keys, lookup by key, as well as all list modification
13 | * operations, require O(n) time.
14 | */
15 | open class ObservableKeyedArrayList> : ObservableArrayList() {
16 | fun containsKey(key: K) = indexOfKey(key) >= 0
17 |
18 | operator fun get(key: K): E? {
19 | val index = indexOfKey(key)
20 | return if (index >= 0) get(index) else null
21 | }
22 |
23 | open fun indexOfKey(key: K): Int {
24 | val iterator = listIterator()
25 | while (iterator.hasNext()) {
26 | val index = iterator.nextIndex()
27 | if (iterator.next()!!.key == key)
28 | return index
29 | }
30 | return -1
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/databinding/ObservableSortedKeyedArrayList.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.databinding
6 |
7 | import java.util.AbstractList
8 | import java.util.Collections
9 | import java.util.Comparator
10 | import java.util.Spliterator
11 |
12 | /**
13 | * KeyedArrayList that enforces uniqueness and sorted order across the set of keys. This class uses
14 | * binary search to improve lookup and replacement times to O(log(n)). However, due to the
15 | * array-based nature of this class, insertion and removal of elements with anything but the largest
16 | * key still require O(n) time.
17 | */
18 | class ObservableSortedKeyedArrayList>(private val comparator: Comparator) : ObservableKeyedArrayList() {
19 | @Transient
20 | private val keyList = KeyList(this)
21 |
22 | override fun add(element: E): Boolean {
23 | val insertionPoint = getInsertionPoint(element)
24 | if (insertionPoint < 0) {
25 | // Skipping insertion is non-destructive if the new and existing objects are the same.
26 | if (element === get(-insertionPoint - 1)) return false
27 | throw IllegalArgumentException("Element with same key already exists in list")
28 | }
29 | super.add(insertionPoint, element)
30 | return true
31 | }
32 |
33 | override fun add(index: Int, element: E) {
34 | val insertionPoint = getInsertionPoint(element)
35 | require(insertionPoint >= 0) { "Element with same key already exists in list" }
36 | if (insertionPoint != index) throw IndexOutOfBoundsException("Wrong index given for element")
37 | super.add(index, element)
38 | }
39 |
40 | override fun addAll(elements: Collection): Boolean {
41 | var didChange = false
42 | for (e in elements) {
43 | if (add(e))
44 | didChange = true
45 | }
46 | return didChange
47 | }
48 |
49 | override fun addAll(index: Int, elements: Collection): Boolean {
50 | var i = index
51 | for (e in elements)
52 | add(i++, e)
53 | return true
54 | }
55 |
56 | private fun getInsertionPoint(e: E) = -Collections.binarySearch(keyList, e.key, comparator) - 1
57 |
58 | override fun indexOfKey(key: K): Int {
59 | val index = Collections.binarySearch(keyList, key, comparator)
60 | return if (index >= 0) index else -1
61 | }
62 |
63 | override fun set(index: Int, element: E): E {
64 | val order = comparator.compare(element.key, get(index).key)
65 | if (order != 0) {
66 | // Allow replacement if the new key would be inserted adjacent to the replaced element.
67 | val insertionPoint = getInsertionPoint(element)
68 | if (insertionPoint < index || insertionPoint > index + 1)
69 | throw IndexOutOfBoundsException("Wrong index given for element")
70 | }
71 | return super.set(index, element)
72 | }
73 |
74 | private class KeyList>(private val list: ObservableSortedKeyedArrayList) : AbstractList(), Set {
75 | override fun get(index: Int): K = list[index].key
76 |
77 | override val size
78 | get() = list.size
79 |
80 | override fun spliterator(): Spliterator = super.spliterator()
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/fragment/AddTunnelsSheet.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.fragment
6 |
7 | import android.content.pm.PackageManager
8 | import android.graphics.drawable.GradientDrawable
9 | import android.os.Bundle
10 | import android.view.LayoutInflater
11 | import android.view.View
12 | import android.view.ViewGroup
13 | import android.view.ViewTreeObserver
14 | import android.widget.FrameLayout
15 | import androidx.core.os.bundleOf
16 | import androidx.fragment.app.setFragmentResult
17 | import com.google.android.material.bottomsheet.BottomSheetBehavior
18 | import com.google.android.material.bottomsheet.BottomSheetDialog
19 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment
20 | import com.wireguard.android.R
21 | import com.wireguard.android.util.resolveAttribute
22 |
23 | class AddTunnelsSheet : BottomSheetDialogFragment() {
24 |
25 | private var behavior: BottomSheetBehavior? = null
26 | private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
27 | override fun onSlide(bottomSheet: View, slideOffset: Float) {
28 | }
29 |
30 | override fun onStateChanged(bottomSheet: View, newState: Int) {
31 | if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
32 | dismiss()
33 | }
34 | }
35 | }
36 |
37 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
38 | if (savedInstanceState != null) dismiss()
39 | val view = inflater.inflate(R.layout.add_tunnels_bottom_sheet, container, false)
40 | if (activity?.packageManager?.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) != true) {
41 | val qrcode = view.findViewById(R.id.create_from_qrcode)
42 | qrcode.isEnabled = false
43 | qrcode.visibility = View.GONE
44 | }
45 | return view
46 | }
47 |
48 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
49 | super.onViewCreated(view, savedInstanceState)
50 | view.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
51 | override fun onGlobalLayout() {
52 | view.viewTreeObserver.removeOnGlobalLayoutListener(this)
53 | val dialog = dialog as BottomSheetDialog? ?: return
54 | behavior = dialog.behavior
55 | behavior?.apply {
56 | state = BottomSheetBehavior.STATE_EXPANDED
57 | peekHeight = 0
58 | addBottomSheetCallback(bottomSheetCallback)
59 | }
60 | dialog.findViewById(R.id.create_empty)?.setOnClickListener {
61 | dismiss()
62 | onRequestCreateConfig()
63 | }
64 | dialog.findViewById(R.id.create_from_file)?.setOnClickListener {
65 | dismiss()
66 | onRequestImportConfig()
67 | }
68 | dialog.findViewById(R.id.create_from_qrcode)?.setOnClickListener {
69 | dismiss()
70 | onRequestScanQRCode()
71 | }
72 | }
73 | })
74 | val gradientDrawable = GradientDrawable().apply {
75 | setColor(requireContext().resolveAttribute(com.google.android.material.R.attr.colorSurface))
76 | }
77 | view.background = gradientDrawable
78 | }
79 |
80 | override fun dismiss() {
81 | super.dismiss()
82 | behavior?.removeBottomSheetCallback(bottomSheetCallback)
83 | }
84 |
85 | private fun onRequestCreateConfig() {
86 | setFragmentResult(REQUEST_KEY_NEW_TUNNEL, bundleOf(REQUEST_METHOD to REQUEST_CREATE))
87 | }
88 |
89 | private fun onRequestImportConfig() {
90 | setFragmentResult(REQUEST_KEY_NEW_TUNNEL, bundleOf(REQUEST_METHOD to REQUEST_IMPORT))
91 | }
92 |
93 | private fun onRequestScanQRCode() {
94 | setFragmentResult(REQUEST_KEY_NEW_TUNNEL, bundleOf(REQUEST_METHOD to REQUEST_SCAN))
95 | }
96 |
97 | companion object {
98 | const val REQUEST_KEY_NEW_TUNNEL = "request_new_tunnel"
99 | const val REQUEST_METHOD = "request_method"
100 | const val REQUEST_CREATE = "request_create"
101 | const val REQUEST_IMPORT = "request_import"
102 | const val REQUEST_SCAN = "request_scan"
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.fragment
6 |
7 | import android.app.Dialog
8 | import android.os.Bundle
9 | import android.view.WindowManager
10 | import androidx.fragment.app.DialogFragment
11 | import androidx.lifecycle.lifecycleScope
12 | import com.google.android.material.dialog.MaterialAlertDialogBuilder
13 | import com.wireguard.android.Application
14 | import com.wireguard.android.R
15 | import com.wireguard.android.databinding.ConfigNamingDialogFragmentBinding
16 | import com.wireguard.config.BadConfigException
17 | import com.wireguard.config.Config
18 | import kotlinx.coroutines.launch
19 | import java.io.ByteArrayInputStream
20 | import java.io.IOException
21 | import java.nio.charset.StandardCharsets
22 |
23 | class ConfigNamingDialogFragment : DialogFragment() {
24 | private var binding: ConfigNamingDialogFragmentBinding? = null
25 | private var config: Config? = null
26 |
27 | private fun createTunnelAndDismiss() {
28 | val binding = binding ?: return
29 | val activity = activity ?: return
30 | val name = binding.tunnelNameText.text.toString()
31 | activity.lifecycleScope.launch {
32 | try {
33 | Application.getTunnelManager().create(name, config)
34 | dismiss()
35 | } catch (e: Throwable) {
36 | binding.tunnelNameTextLayout.error = e.message
37 | }
38 | }
39 | }
40 |
41 | override fun onCreate(savedInstanceState: Bundle?) {
42 | super.onCreate(savedInstanceState)
43 | val configText = requireArguments().getString(KEY_CONFIG_TEXT)
44 | val configBytes = configText!!.toByteArray(StandardCharsets.UTF_8)
45 | config = try {
46 | Config.parse(ByteArrayInputStream(configBytes))
47 | } catch (e: Throwable) {
48 | when (e) {
49 | is BadConfigException, is IOException -> throw IllegalArgumentException("Invalid config passed to ${javaClass.simpleName}", e)
50 | else -> throw e
51 | }
52 | }
53 | }
54 |
55 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
56 | val activity = requireActivity()
57 | val alertDialogBuilder = MaterialAlertDialogBuilder(activity)
58 | alertDialogBuilder.setTitle(R.string.import_from_qr_code)
59 | binding = ConfigNamingDialogFragmentBinding.inflate(activity.layoutInflater, null, false)
60 | binding?.apply {
61 | executePendingBindings()
62 | alertDialogBuilder.setView(root)
63 | }
64 | alertDialogBuilder.setPositiveButton(R.string.create_tunnel) { _, _ -> createTunnelAndDismiss() }
65 | alertDialogBuilder.setNegativeButton(R.string.cancel) { _, _ -> dismiss() }
66 | val dialog = alertDialogBuilder.create()
67 | dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
68 | return dialog
69 | }
70 |
71 | companion object {
72 | private const val KEY_CONFIG_TEXT = "config_text"
73 |
74 | fun newInstance(configText: String?): ConfigNamingDialogFragment {
75 | val extras = Bundle()
76 | extras.putString(KEY_CONFIG_TEXT, configText)
77 | val fragment = ConfigNamingDialogFragment()
78 | fragment.arguments = extras
79 | return fragment
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/model/ApplicationData.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.model
6 |
7 | import android.graphics.drawable.Drawable
8 | import androidx.databinding.BaseObservable
9 | import androidx.databinding.Bindable
10 | import com.wireguard.android.BR
11 | import com.wireguard.android.databinding.Keyed
12 |
13 | class ApplicationData(val icon: Drawable, val name: String, val packageName: String, isSelected: Boolean) : BaseObservable(), Keyed {
14 | override val key = name
15 |
16 | @get:Bindable
17 | var isSelected = isSelected
18 | set(value) {
19 | field = value
20 | notifyPropertyChanged(BR.selected)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/model/TunnelComparator.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.android.model
7 |
8 | object TunnelComparator : Comparator {
9 | private class NaturalSortString(originalString: String) {
10 | class NaturalSortToken(val maybeString: String?, val maybeNumber: Int?) : Comparable {
11 | override fun compareTo(other: NaturalSortToken): Int {
12 | if (maybeString == null) {
13 | if (other.maybeString != null || maybeNumber!! < other.maybeNumber!!) {
14 | return -1
15 | } else if (maybeNumber > other.maybeNumber) {
16 | return 1
17 | }
18 | } else if (other.maybeString == null || maybeString > other.maybeString) {
19 | return 1
20 | } else if (maybeString < other.maybeString) {
21 | return -1
22 | }
23 | return 0
24 | }
25 | }
26 |
27 | val tokens: MutableList = ArrayList()
28 |
29 | init {
30 | for (s in NATURAL_SORT_DIGIT_FINDER.findAll(originalString.split(WHITESPACE_FINDER).joinToString(" ").lowercase())) {
31 | try {
32 | val n = s.value.toInt()
33 | tokens.add(NaturalSortToken(null, n))
34 | } catch (_: NumberFormatException) {
35 | tokens.add(NaturalSortToken(s.value, null))
36 | }
37 | }
38 | }
39 |
40 | private companion object {
41 | private val NATURAL_SORT_DIGIT_FINDER = Regex("""\d+|\D+""")
42 | private val WHITESPACE_FINDER = Regex("""\s""")
43 | }
44 | }
45 |
46 | override fun compare(a: String, b: String): Int {
47 | if (a == b)
48 | return 0
49 | val na = NaturalSortString(a)
50 | val nb = NaturalSortString(b)
51 | for (i in 0 until nb.tokens.size) {
52 | if (i == na.tokens.size) {
53 | return -1
54 | }
55 | val c = na.tokens[i].compareTo(nb.tokens[i])
56 | if (c != 0)
57 | return c
58 | }
59 | return 1
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/preference/DonatePreference.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.android.preference
7 |
8 | import android.content.Context
9 | import android.content.Intent
10 | import android.net.Uri
11 | import android.util.AttributeSet
12 | import android.widget.Toast
13 | import androidx.preference.Preference
14 | import com.google.android.material.dialog.MaterialAlertDialogBuilder
15 | import com.wireguard.android.R
16 | import com.wireguard.android.updater.Updater
17 | import com.wireguard.android.util.ErrorMessages
18 | import androidx.core.net.toUri
19 |
20 | class DonatePreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
21 | override fun getSummary() = context.getString(R.string.donate_summary)
22 |
23 | override fun getTitle() = context.getString(R.string.donate_title)
24 |
25 | override fun onClick() {
26 | /* Google Play Store forbids links to our donation page. */
27 | if (Updater.installerIsGooglePlay(context)) {
28 | MaterialAlertDialogBuilder(context)
29 | .setTitle(R.string.donate_title)
30 | .setMessage(R.string.donate_google_play_disappointment)
31 | .show()
32 | return
33 | }
34 |
35 | val intent = Intent(Intent.ACTION_VIEW)
36 | intent.data = "https://www.wireguard.com/donations/".toUri()
37 | try {
38 | context.startActivity(intent)
39 | } catch (e: Throwable) {
40 | Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_SHORT).show()
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/preference/KernelModuleEnablerPreference.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.preference
6 |
7 | import android.content.Context
8 | import android.content.Intent
9 | import android.util.AttributeSet
10 | import android.util.Log
11 | import androidx.lifecycle.lifecycleScope
12 | import androidx.preference.Preference
13 | import com.wireguard.android.Application
14 | import com.wireguard.android.R
15 | import com.wireguard.android.activity.SettingsActivity
16 | import com.wireguard.android.backend.Tunnel
17 | import com.wireguard.android.backend.WgQuickBackend
18 | import com.wireguard.android.util.UserKnobs
19 | import com.wireguard.android.util.activity
20 | import com.wireguard.android.util.lifecycleScope
21 | import kotlinx.coroutines.Dispatchers
22 | import kotlinx.coroutines.SupervisorJob
23 | import kotlinx.coroutines.async
24 | import kotlinx.coroutines.awaitAll
25 | import kotlinx.coroutines.launch
26 | import kotlinx.coroutines.withContext
27 | import kotlin.system.exitProcess
28 |
29 | class KernelModuleEnablerPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
30 | private var state = State.UNKNOWN
31 |
32 | init {
33 | isVisible = false
34 | lifecycleScope.launch {
35 | setState(if (Application.getBackend() is WgQuickBackend) State.ENABLED else State.DISABLED)
36 | }
37 | }
38 |
39 | override fun getSummary() = if (state == State.UNKNOWN) "" else context.getString(state.summaryResourceId)
40 |
41 | override fun getTitle() = if (state == State.UNKNOWN) "" else context.getString(state.titleResourceId)
42 |
43 | override fun onClick() {
44 | activity.lifecycleScope.launch {
45 | if (state == State.DISABLED) {
46 | setState(State.ENABLING)
47 | UserKnobs.setEnableKernelModule(true)
48 | } else if (state == State.ENABLED) {
49 | setState(State.DISABLING)
50 | UserKnobs.setEnableKernelModule(false)
51 | }
52 | val observableTunnels = Application.getTunnelManager().getTunnels()
53 | val downings = observableTunnels.map { async(SupervisorJob()) { it.setStateAsync(Tunnel.State.DOWN) } }
54 | try {
55 | downings.awaitAll()
56 | withContext(Dispatchers.IO) {
57 | val restartIntent = Intent(context, SettingsActivity::class.java)
58 | restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
59 | restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
60 | Application.get().startActivity(restartIntent)
61 | exitProcess(0)
62 | }
63 | } catch (e: Throwable) {
64 | Log.e(TAG, Log.getStackTraceString(e))
65 | }
66 | }
67 | }
68 |
69 | private fun setState(state: State) {
70 | if (this.state == state) return
71 | this.state = state
72 | if (isEnabled != state.shouldEnableView) isEnabled = state.shouldEnableView
73 | if (isVisible != state.visible) isVisible = state.visible
74 | notifyChanged()
75 | }
76 |
77 | private enum class State(val titleResourceId: Int, val summaryResourceId: Int, val shouldEnableView: Boolean, val visible: Boolean) {
78 | UNKNOWN(0, 0, false, false),
79 | ENABLED(R.string.module_enabler_enabled_title, R.string.module_enabler_enabled_summary, true, true),
80 | DISABLED(R.string.module_enabler_disabled_title, R.string.module_enabler_disabled_summary, true, true),
81 | ENABLING(R.string.module_enabler_disabled_title, R.string.success_application_will_restart, false, true),
82 | DISABLING(R.string.module_enabler_enabled_title, R.string.success_application_will_restart, false, true);
83 | }
84 |
85 | companion object {
86 | private const val TAG = "WireGuard/KernelModuleEnablerPreference"
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/preference/QuickTilePreference.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.android.preference
7 |
8 | import android.app.StatusBarManager
9 | import android.content.ComponentName
10 | import android.content.Context
11 | import android.graphics.drawable.Icon
12 | import android.os.Build
13 | import android.util.AttributeSet
14 | import android.widget.Toast
15 | import androidx.annotation.RequiresApi
16 | import androidx.preference.Preference
17 | import com.wireguard.android.QuickTileService
18 | import com.wireguard.android.R
19 |
20 | @RequiresApi(Build.VERSION_CODES.TIRAMISU)
21 | class QuickTilePreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
22 | override fun getSummary() = context.getString(R.string.quick_settings_tile_add_summary)
23 |
24 | override fun getTitle() = context.getString(R.string.quick_settings_tile_add_title)
25 |
26 | override fun onClick() {
27 | val statusBarManager = context.getSystemService(StatusBarManager::class.java)
28 | statusBarManager.requestAddTileService(
29 | ComponentName(context, QuickTileService::class.java),
30 | context.getString(R.string.quick_settings_tile_action),
31 | Icon.createWithResource(context, R.drawable.ic_tile),
32 | context.mainExecutor
33 | ) {
34 | when (it) {
35 | StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED,
36 | StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED -> {
37 | parent?.removePreference(this)
38 | --preferenceManager.preferenceScreen.initialExpandedChildrenCount
39 | }
40 | StatusBarManager.TILE_ADD_REQUEST_ERROR_MISMATCHED_PACKAGE,
41 | StatusBarManager.TILE_ADD_REQUEST_ERROR_REQUEST_IN_PROGRESS,
42 | StatusBarManager.TILE_ADD_REQUEST_ERROR_BAD_COMPONENT,
43 | StatusBarManager.TILE_ADD_REQUEST_ERROR_NOT_CURRENT_USER,
44 | StatusBarManager.TILE_ADD_REQUEST_ERROR_APP_NOT_IN_FOREGROUND,
45 | StatusBarManager.TILE_ADD_REQUEST_ERROR_NO_STATUS_BAR_SERVICE ->
46 | Toast.makeText(context, context.getString(R.string.quick_settings_tile_add_failure, it), Toast.LENGTH_SHORT).show()
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.preference
6 |
7 | import android.content.Context
8 | import android.util.AttributeSet
9 | import androidx.preference.Preference
10 | import com.wireguard.android.Application
11 | import com.wireguard.android.R
12 | import com.wireguard.android.util.ToolsInstaller
13 | import com.wireguard.android.util.lifecycleScope
14 | import kotlinx.coroutines.Dispatchers
15 | import kotlinx.coroutines.launch
16 | import kotlinx.coroutines.withContext
17 |
18 | /**
19 | * Preference implementing a button that asynchronously runs `ToolsInstaller` and displays the
20 | * result as the preference summary.
21 | */
22 | class ToolsInstallerPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
23 | private var state = State.INITIAL
24 | override fun getSummary() = context.getString(state.messageResourceId)
25 |
26 | override fun getTitle() = context.getString(R.string.tools_installer_title)
27 |
28 | override fun onAttached() {
29 | super.onAttached()
30 | lifecycleScope.launch {
31 | try {
32 | val state = withContext(Dispatchers.IO) { Application.getToolsInstaller().areInstalled() }
33 | when {
34 | state == ToolsInstaller.ERROR -> setState(State.INITIAL)
35 | state and ToolsInstaller.YES == ToolsInstaller.YES -> setState(State.ALREADY)
36 | state and (ToolsInstaller.MAGISK or ToolsInstaller.NO) == ToolsInstaller.MAGISK or ToolsInstaller.NO -> setState(State.INITIAL_MAGISK)
37 | state and (ToolsInstaller.SYSTEM or ToolsInstaller.NO) == ToolsInstaller.SYSTEM or ToolsInstaller.NO -> setState(State.INITIAL_SYSTEM)
38 | else -> setState(State.INITIAL)
39 | }
40 | } catch (_: Throwable) {
41 | setState(State.INITIAL)
42 | }
43 | }
44 | }
45 |
46 | override fun onClick() {
47 | setState(State.WORKING)
48 | lifecycleScope.launch {
49 | try {
50 | val result = withContext(Dispatchers.IO) { Application.getToolsInstaller().install() }
51 | when {
52 | result and (ToolsInstaller.YES or ToolsInstaller.MAGISK) == ToolsInstaller.YES or ToolsInstaller.MAGISK -> setState(State.SUCCESS_MAGISK)
53 | result and (ToolsInstaller.YES or ToolsInstaller.SYSTEM) == ToolsInstaller.YES or ToolsInstaller.SYSTEM -> setState(State.SUCCESS_SYSTEM)
54 | else -> setState(State.FAILURE)
55 | }
56 | } catch (_: Throwable) {
57 | setState(State.FAILURE)
58 | }
59 | }
60 | }
61 |
62 | private fun setState(state: State) {
63 | if (this.state == state) return
64 | this.state = state
65 | if (isEnabled != state.shouldEnableView) isEnabled = state.shouldEnableView
66 | notifyChanged()
67 | }
68 |
69 | private enum class State(val messageResourceId: Int, val shouldEnableView: Boolean) {
70 | INITIAL(R.string.tools_installer_initial, true),
71 | ALREADY(R.string.tools_installer_already, false),
72 | FAILURE(R.string.tools_installer_failure, true),
73 | WORKING(R.string.tools_installer_working, false),
74 | INITIAL_SYSTEM(R.string.tools_installer_initial_system, true),
75 | SUCCESS_SYSTEM(R.string.tools_installer_success_system, false),
76 | INITIAL_MAGISK(R.string.tools_installer_initial_magisk, true),
77 | SUCCESS_MAGISK(R.string.tools_installer_success_magisk, false);
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.preference
6 |
7 | import android.content.Context
8 | import android.content.Intent
9 | import android.net.Uri
10 | import android.util.AttributeSet
11 | import android.widget.Toast
12 | import androidx.preference.Preference
13 | import com.wireguard.android.Application
14 | import com.wireguard.android.BuildConfig
15 | import com.wireguard.android.R
16 | import com.wireguard.android.backend.Backend
17 | import com.wireguard.android.backend.GoBackend
18 | import com.wireguard.android.backend.WgQuickBackend
19 | import com.wireguard.android.util.ErrorMessages
20 | import com.wireguard.android.util.lifecycleScope
21 | import kotlinx.coroutines.Dispatchers
22 | import kotlinx.coroutines.launch
23 | import kotlinx.coroutines.withContext
24 |
25 | class VersionPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
26 | private var versionSummary: String? = null
27 |
28 | override fun getSummary() = versionSummary
29 |
30 | override fun getTitle() = context.getString(R.string.version_title, BuildConfig.VERSION_NAME)
31 |
32 | override fun onClick() {
33 | val intent = Intent(Intent.ACTION_VIEW)
34 | intent.data = Uri.parse("https://www.wireguard.com/")
35 | try {
36 | context.startActivity(intent)
37 | } catch (e: Throwable) {
38 | Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_SHORT).show()
39 | }
40 | }
41 |
42 | companion object {
43 | private fun getBackendPrettyName(context: Context, backend: Backend) = when (backend) {
44 | is WgQuickBackend -> context.getString(R.string.type_name_kernel_module)
45 | is GoBackend -> context.getString(R.string.type_name_go_userspace)
46 | else -> ""
47 | }
48 | }
49 |
50 | init {
51 | lifecycleScope.launch {
52 | val backend = Application.getBackend()
53 | versionSummary = getContext().getString(R.string.version_summary_checking, getBackendPrettyName(context, backend).lowercase())
54 | notifyChanged()
55 | versionSummary = try {
56 | getContext().getString(R.string.version_summary, getBackendPrettyName(context, backend), withContext(Dispatchers.IO) { backend.version })
57 | } catch (_: Throwable) {
58 | getContext().getString(R.string.version_summary_unknown, getBackendPrettyName(context, backend).lowercase())
59 | }
60 | notifyChanged()
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/util/AdminKnobs.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.android.util
7 |
8 | import android.content.RestrictionsManager
9 | import androidx.core.content.getSystemService
10 | import com.wireguard.android.Application
11 |
12 | object AdminKnobs {
13 | private val restrictions: RestrictionsManager? = Application.get().getSystemService()
14 | val disableConfigExport: Boolean
15 | get() = restrictions?.applicationRestrictions?.getBoolean("disable_config_export", false)
16 | ?: false
17 | }
18 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.android.util
7 |
8 | import android.os.Handler
9 | import android.os.Looper
10 | import android.util.Log
11 | import androidx.annotation.StringRes
12 | import androidx.biometric.BiometricManager
13 | import androidx.biometric.BiometricManager.Authenticators
14 | import androidx.biometric.BiometricPrompt
15 | import androidx.fragment.app.Fragment
16 | import com.wireguard.android.R
17 |
18 |
19 | object BiometricAuthenticator {
20 | private const val TAG = "WireGuard/BiometricAuthenticator"
21 |
22 | // Not all devices support strong biometric auth so we're allowing both device credentials as
23 | // well as weak biometrics.
24 | private const val allowedAuthenticators = Authenticators.DEVICE_CREDENTIAL or Authenticators.BIOMETRIC_WEAK
25 |
26 | sealed class Result {
27 | data class Success(val cryptoObject: BiometricPrompt.CryptoObject?) : Result()
28 | data class Failure(val code: Int?, val message: CharSequence) : Result()
29 | object HardwareUnavailableOrDisabled : Result()
30 | object Cancelled : Result()
31 | }
32 |
33 | fun authenticate(
34 | @StringRes dialogTitleRes: Int,
35 | fragment: Fragment,
36 | callback: (Result) -> Unit
37 | ) {
38 | val authCallback = object : BiometricPrompt.AuthenticationCallback() {
39 | override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
40 | super.onAuthenticationError(errorCode, errString)
41 | Log.d(TAG, "BiometricAuthentication error: errorCode=$errorCode, msg=$errString")
42 | callback(
43 | when (errorCode) {
44 | BiometricPrompt.ERROR_CANCELED, BiometricPrompt.ERROR_USER_CANCELED,
45 | BiometricPrompt.ERROR_NEGATIVE_BUTTON -> {
46 | Result.Cancelled
47 | }
48 |
49 | BiometricPrompt.ERROR_HW_NOT_PRESENT, BiometricPrompt.ERROR_HW_UNAVAILABLE,
50 | BiometricPrompt.ERROR_NO_BIOMETRICS, BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> {
51 | Result.HardwareUnavailableOrDisabled
52 | }
53 |
54 | else -> Result.Failure(errorCode, fragment.getString(R.string.biometric_auth_error_reason, errString))
55 | }
56 | )
57 | }
58 |
59 | override fun onAuthenticationFailed() {
60 | super.onAuthenticationFailed()
61 | callback(Result.Failure(null, fragment.getString(R.string.biometric_auth_error)))
62 | }
63 |
64 | override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
65 | super.onAuthenticationSucceeded(result)
66 | callback(Result.Success(result.cryptoObject))
67 | }
68 | }
69 | val biometricPrompt = BiometricPrompt(fragment, { Handler(Looper.getMainLooper()).post(it) }, authCallback)
70 | val promptInfo = BiometricPrompt.PromptInfo.Builder()
71 | .setTitle(fragment.getString(dialogTitleRes))
72 | .setAllowedAuthenticators(allowedAuthenticators)
73 | .build()
74 | if (BiometricManager.from(fragment.requireContext()).canAuthenticate(allowedAuthenticators) == BiometricManager.BIOMETRIC_SUCCESS) {
75 | biometricPrompt.authenticate(promptInfo)
76 | } else {
77 | callback(Result.HardwareUnavailableOrDisabled)
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/util/ClipboardUtils.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.util
6 |
7 | import android.content.ClipData
8 | import android.content.ClipboardManager
9 | import android.os.Build
10 | import android.view.View
11 | import android.widget.TextView
12 | import androidx.core.content.getSystemService
13 | import com.google.android.material.snackbar.Snackbar
14 | import com.google.android.material.textfield.TextInputEditText
15 | import com.wireguard.android.R
16 |
17 | /**
18 | * Standalone utilities for interacting with the system clipboard.
19 | */
20 | object ClipboardUtils {
21 | @JvmStatic
22 | fun copyTextView(view: View) {
23 | val data = when (view) {
24 | is TextInputEditText -> Pair(view.editableText, view.hint)
25 | is TextView -> Pair(view.text, view.contentDescription)
26 | else -> return
27 | }
28 | if (data.first == null || data.first.isEmpty()) {
29 | return
30 | }
31 | val service = view.context.getSystemService() ?: return
32 | service.setPrimaryClip(ClipData.newPlainText(data.second, data.first))
33 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
34 | Snackbar.make(view, view.context.getString(R.string.copied_to_clipboard, data.second), Snackbar.LENGTH_LONG).show()
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/util/Extensions.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.android.util
7 |
8 | import android.content.Context
9 | import android.util.TypedValue
10 | import androidx.annotation.AttrRes
11 | import androidx.lifecycle.lifecycleScope
12 | import androidx.preference.Preference
13 | import com.wireguard.android.Application
14 | import com.wireguard.android.activity.SettingsActivity
15 | import kotlinx.coroutines.CoroutineScope
16 |
17 | fun Context.resolveAttribute(@AttrRes attrRes: Int): Int {
18 | val typedValue = TypedValue()
19 | theme.resolveAttribute(attrRes, typedValue, true)
20 | return typedValue.data
21 | }
22 |
23 | val Any.applicationScope: CoroutineScope
24 | get() = Application.getCoroutineScope()
25 |
26 | val Preference.activity: SettingsActivity
27 | get() = context as? SettingsActivity
28 | ?: throw IllegalStateException("Failed to resolve SettingsActivity")
29 |
30 | val Preference.lifecycleScope: CoroutineScope
31 | get() = activity.lifecycleScope
32 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/util/QrCodeFromFileScanner.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.android.util
7 |
8 | import android.content.ContentResolver
9 | import android.graphics.Bitmap
10 | import android.graphics.BitmapFactory
11 | import android.net.Uri
12 | import android.util.Log
13 | import com.google.zxing.BinaryBitmap
14 | import com.google.zxing.DecodeHintType
15 | import com.google.zxing.NotFoundException
16 | import com.google.zxing.RGBLuminanceSource
17 | import com.google.zxing.Reader
18 | import com.google.zxing.Result
19 | import com.google.zxing.common.HybridBinarizer
20 | import kotlinx.coroutines.Dispatchers
21 | import kotlinx.coroutines.withContext
22 |
23 | /**
24 | * Encapsulates the logic of scanning a barcode from a file,
25 | * @property contentResolver - Resolver to read the incoming data
26 | * @property reader - An instance of zxing's [Reader] class to parse the image
27 | */
28 | class QrCodeFromFileScanner(
29 | private val contentResolver: ContentResolver,
30 | private val reader: Reader,
31 | ) {
32 | private fun scanBitmapForResult(source: Bitmap): Result {
33 | val width = source.width
34 | val height = source.height
35 | val pixels = IntArray(width * height)
36 | source.getPixels(pixels, 0, width, 0, 0, width, height)
37 |
38 | val bBitmap = BinaryBitmap(HybridBinarizer(RGBLuminanceSource(width, height, pixels)))
39 | return reader.decode(bBitmap, mapOf(DecodeHintType.TRY_HARDER to true))
40 | }
41 |
42 | private fun doScan(data: Uri): Result {
43 | Log.d(TAG, "Starting to scan an image: $data")
44 | contentResolver.openInputStream(data).use { inputStream ->
45 | var bitmap: Bitmap? = null
46 | var firstException: Throwable? = null
47 | for (i in arrayOf(1, 2, 4, 8, 16, 32, 64, 128)) {
48 | try {
49 | val options = BitmapFactory.Options()
50 | options.inSampleSize = i
51 | bitmap = BitmapFactory.decodeStream(inputStream, null, options)
52 | ?: throw IllegalArgumentException("Can't decode stream for bitmap")
53 | return scanBitmapForResult(bitmap)
54 | } catch (e: Throwable) {
55 | bitmap?.recycle()
56 | System.gc()
57 | Log.e(TAG, "Original image scan at scale factor $i finished with error: $e")
58 | if (firstException == null)
59 | firstException = e
60 | }
61 | }
62 | throw Exception(firstException)
63 | }
64 | }
65 |
66 | /**
67 | * Attempts to parse incoming data
68 | * @return result of the decoding operation
69 | * @throws NotFoundException when parser didn't find QR code in the image
70 | */
71 | suspend fun scan(data: Uri) = withContext(Dispatchers.Default) { doScan(data) }
72 |
73 | companion object {
74 | private const val TAG = "QrCodeFromFileScanner"
75 |
76 | /**
77 | * Given a reference to a file, check if this file could be parsed by this class
78 | * @return true if the file can be parsed, false if not
79 | */
80 | fun validContentType(contentResolver: ContentResolver, data: Uri): Boolean {
81 | return contentResolver.getType(data)?.startsWith("image/") == true
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.android.util
7 |
8 | import android.icu.text.ListFormatter
9 | import android.icu.text.MeasureFormat
10 | import android.icu.text.RelativeDateTimeFormatter
11 | import android.icu.util.Measure
12 | import android.icu.util.MeasureUnit
13 | import android.os.Build
14 | import com.wireguard.android.Application
15 | import com.wireguard.android.R
16 | import java.util.Locale
17 | import kotlin.time.Duration.Companion.seconds
18 |
19 | object QuantityFormatter {
20 | fun formatBytes(bytes: Long): String {
21 | val context = Application.get().applicationContext
22 | return when {
23 | bytes < 1024 -> context.getString(R.string.transfer_bytes, bytes)
24 | bytes < 1024 * 1024 -> context.getString(R.string.transfer_kibibytes, bytes / 1024.0)
25 | bytes < 1024 * 1024 * 1024 -> context.getString(R.string.transfer_mibibytes, bytes / (1024.0 * 1024.0))
26 | bytes < 1024 * 1024 * 1024 * 1024L -> context.getString(R.string.transfer_gibibytes, bytes / (1024.0 * 1024.0 * 1024.0))
27 | else -> context.getString(R.string.transfer_tibibytes, bytes / (1024.0 * 1024.0 * 1024.0) / 1024.0)
28 | }
29 | }
30 |
31 | fun formatEpochAgo(epochMillis: Long): String {
32 | var span = (System.currentTimeMillis() - epochMillis) / 1000
33 |
34 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
35 | return Application.get().applicationContext.getString(R.string.latest_handshake_ago, span.seconds.toString())
36 |
37 | if (span <= 0L)
38 | return RelativeDateTimeFormatter.getInstance().format(RelativeDateTimeFormatter.Direction.PLAIN, RelativeDateTimeFormatter.AbsoluteUnit.NOW)
39 | val measureFormat = MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
40 | val parts = ArrayList(4)
41 | if (span >= 24 * 60 * 60L) {
42 | val v = span / (24 * 60 * 60L)
43 | parts.add(measureFormat.format(Measure(v, MeasureUnit.DAY)))
44 | span -= v * (24 * 60 * 60L)
45 | }
46 | if (span >= 60 * 60L) {
47 | val v = span / (60 * 60L)
48 | parts.add(measureFormat.format(Measure(v, MeasureUnit.HOUR)))
49 | span -= v * (60 * 60L)
50 | }
51 | if (span >= 60L) {
52 | val v = span / 60L
53 | parts.add(measureFormat.format(Measure(v, MeasureUnit.MINUTE)))
54 | span -= v * 60L
55 | }
56 | if (span > 0L)
57 | parts.add(measureFormat.format(Measure(span, MeasureUnit.SECOND)))
58 |
59 | val joined = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
60 | parts.joinToString()
61 | else
62 | ListFormatter.getInstance(Locale.getDefault(), ListFormatter.Type.UNITS, ListFormatter.Width.SHORT).format(parts)
63 |
64 | return Application.get().applicationContext.getString(R.string.latest_handshake_ago, joined)
65 | }
66 | }
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.viewmodel
6 |
7 | import android.os.Build
8 | import android.os.Parcel
9 | import android.os.Parcelable
10 | import androidx.core.os.ParcelCompat
11 | import androidx.databinding.ObservableArrayList
12 | import androidx.databinding.ObservableList
13 | import com.wireguard.config.BadConfigException
14 | import com.wireguard.config.Config
15 | import com.wireguard.config.Peer
16 |
17 | class ConfigProxy : Parcelable {
18 | val `interface`: InterfaceProxy
19 | val peers: ObservableList = ObservableArrayList()
20 |
21 | private constructor(parcel: Parcel) {
22 | `interface` = ParcelCompat.readParcelable(parcel, InterfaceProxy::class.java.classLoader, InterfaceProxy::class.java) ?: InterfaceProxy()
23 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
24 | ParcelCompat.readParcelableList(parcel, peers, PeerProxy::class.java.classLoader, PeerProxy::class.java)
25 | } else {
26 | parcel.readTypedList(peers, PeerProxy.CREATOR)
27 | }
28 | peers.forEach { it.bind(this) }
29 | }
30 |
31 | constructor(other: Config) {
32 | `interface` = InterfaceProxy(other.getInterface())
33 | other.peers.forEach {
34 | val proxy = PeerProxy(it)
35 | peers.add(proxy)
36 | proxy.bind(this)
37 | }
38 | }
39 |
40 | constructor() {
41 | `interface` = InterfaceProxy()
42 | }
43 |
44 | fun addPeer(): PeerProxy {
45 | val proxy = PeerProxy()
46 | peers.add(proxy)
47 | proxy.bind(this)
48 | return proxy
49 | }
50 |
51 | override fun describeContents() = 0
52 |
53 | @Throws(BadConfigException::class)
54 | fun resolve(): Config {
55 | val resolvedPeers: MutableCollection = ArrayList()
56 | peers.forEach { resolvedPeers.add(it.resolve()) }
57 | return Config.Builder()
58 | .setInterface(`interface`.resolve())
59 | .addPeers(resolvedPeers)
60 | .build()
61 | }
62 |
63 | override fun writeToParcel(dest: Parcel, flags: Int) {
64 | dest.writeParcelable(`interface`, flags)
65 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
66 | dest.writeParcelableList(peers, flags)
67 | } else {
68 | dest.writeTypedList(peers)
69 | }
70 | }
71 |
72 | private class ConfigProxyCreator : Parcelable.Creator {
73 | override fun createFromParcel(parcel: Parcel): ConfigProxy {
74 | return ConfigProxy(parcel)
75 | }
76 |
77 | override fun newArray(size: Int): Array {
78 | return arrayOfNulls(size)
79 | }
80 | }
81 |
82 | companion object {
83 | @JvmField
84 | val CREATOR: Parcelable.Creator = ConfigProxyCreator()
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/widget/KeyInputFilter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.widget
6 |
7 | import android.text.InputFilter
8 | import android.text.SpannableStringBuilder
9 | import android.text.Spanned
10 | import com.wireguard.crypto.Key
11 |
12 | /**
13 | * InputFilter for entering WireGuard private/public keys encoded with base64.
14 | */
15 | class KeyInputFilter : InputFilter {
16 | override fun filter(
17 | source: CharSequence,
18 | sStart: Int, sEnd: Int,
19 | dest: Spanned,
20 | dStart: Int, dEnd: Int
21 | ): CharSequence? {
22 | var replacement: SpannableStringBuilder? = null
23 | var rIndex = 0
24 | val dLength = dest.length
25 | for (sIndex in sStart until sEnd) {
26 | val c = source[sIndex]
27 | val dIndex = dStart + (sIndex - sStart)
28 | // Restrict characters to the base64 character set.
29 | // Ensure adding this character does not push the length over the limit.
30 | if ((dIndex + 1 < Key.Format.BASE64.length && isAllowed(c) ||
31 | dIndex + 1 == Key.Format.BASE64.length && c == '=') &&
32 | dLength + (sIndex - sStart) < Key.Format.BASE64.length
33 | ) {
34 | ++rIndex
35 | } else {
36 | if (replacement == null) replacement = SpannableStringBuilder(source, sStart, sEnd)
37 | replacement.delete(rIndex, rIndex + 1)
38 | }
39 | }
40 | return replacement
41 | }
42 |
43 | companion object {
44 | private fun isAllowed(c: Char) = Character.isLetterOrDigit(c) || c == '+' || c == '/'
45 |
46 | @JvmStatic
47 | fun newInstance() = KeyInputFilter()
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.widget
6 |
7 | import android.content.Context
8 | import android.util.AttributeSet
9 | import android.view.View
10 | import android.widget.RelativeLayout
11 | import com.wireguard.android.R
12 |
13 | class MultiselectableRelativeLayout @JvmOverloads constructor(
14 | context: Context? = null,
15 | attrs: AttributeSet? = null,
16 | defStyleAttr: Int = 0,
17 | defStyleRes: Int = 0
18 | ) : RelativeLayout(context, attrs, defStyleAttr, defStyleRes) {
19 | private var multiselected = false
20 |
21 | override fun onCreateDrawableState(extraSpace: Int): IntArray {
22 | if (multiselected) {
23 | val drawableState = super.onCreateDrawableState(extraSpace + 1)
24 | View.mergeDrawableStates(drawableState, STATE_MULTISELECTED)
25 | return drawableState
26 | }
27 | return super.onCreateDrawableState(extraSpace)
28 | }
29 |
30 | fun setMultiSelected(on: Boolean) {
31 | if (!multiselected) {
32 | multiselected = true
33 | refreshDrawableState()
34 | }
35 | isActivated = on
36 | }
37 |
38 | fun setSingleSelected(on: Boolean) {
39 | if (multiselected) {
40 | multiselected = false
41 | refreshDrawableState()
42 | }
43 | isActivated = on
44 | }
45 |
46 | companion object {
47 | private val STATE_MULTISELECTED = intArrayOf(R.attr.state_multiselected)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/widget/NameInputFilter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.widget
6 |
7 | import android.text.InputFilter
8 | import android.text.SpannableStringBuilder
9 | import android.text.Spanned
10 | import com.wireguard.android.backend.Tunnel
11 |
12 | /**
13 | * InputFilter for entering WireGuard configuration names (Linux interface names).
14 | */
15 | class NameInputFilter : InputFilter {
16 | override fun filter(
17 | source: CharSequence,
18 | sStart: Int, sEnd: Int,
19 | dest: Spanned,
20 | dStart: Int, dEnd: Int
21 | ): CharSequence? {
22 | var replacement: SpannableStringBuilder? = null
23 | var rIndex = 0
24 | val dLength = dest.length
25 | for (sIndex in sStart until sEnd) {
26 | val c = source[sIndex]
27 | val dIndex = dStart + (sIndex - sStart)
28 | // Restrict characters to those valid in interfaces.
29 | // Ensure adding this character does not push the length over the limit.
30 | if (dIndex < Tunnel.NAME_MAX_LENGTH && isAllowed(c) &&
31 | dLength + (sIndex - sStart) < Tunnel.NAME_MAX_LENGTH
32 | ) {
33 | ++rIndex
34 | } else {
35 | if (replacement == null) replacement = SpannableStringBuilder(source, sStart, sEnd)
36 | replacement.delete(rIndex, rIndex + 1)
37 | }
38 | }
39 | return replacement
40 | }
41 |
42 | companion object {
43 | private fun isAllowed(c: Char) = Character.isLetterOrDigit(c) || "_=+.-".indexOf(c) >= 0
44 |
45 | @JvmStatic
46 | fun newInstance() = NameInputFilter()
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/widget/ToggleSwitch.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2013 The Android Open Source Project
3 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
4 | * SPDX-License-Identifier: Apache-2.0
5 | */
6 | package com.wireguard.android.widget
7 |
8 | import android.content.Context
9 | import android.os.Parcelable
10 | import android.util.AttributeSet
11 | import com.google.android.material.materialswitch.MaterialSwitch
12 |
13 | class ToggleSwitch @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : MaterialSwitch(context, attrs) {
14 | private var isRestoringState = false
15 | private var listener: OnBeforeCheckedChangeListener? = null
16 | override fun onRestoreInstanceState(state: Parcelable) {
17 | isRestoringState = true
18 | super.onRestoreInstanceState(state)
19 | isRestoringState = false
20 | }
21 |
22 | override fun setChecked(checked: Boolean) {
23 | if (checked == isChecked) return
24 | if (isRestoringState || listener == null) {
25 | super.setChecked(checked)
26 | return
27 | }
28 | isEnabled = false
29 | listener!!.onBeforeCheckedChanged(this, checked)
30 | }
31 |
32 | fun setCheckedInternal(checked: Boolean) {
33 | super.setChecked(checked)
34 | isEnabled = true
35 | }
36 |
37 | fun setOnBeforeCheckedChangeListener(listener: OnBeforeCheckedChangeListener?) {
38 | this.listener = listener
39 | }
40 |
41 | interface OnBeforeCheckedChangeListener {
42 | fun onBeforeCheckedChanged(toggleSwitch: ToggleSwitch?, checked: Boolean)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/widget/TvCardView.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.android.widget
7 |
8 | import android.content.Context
9 | import android.util.AttributeSet
10 | import android.view.View
11 | import com.google.android.material.card.MaterialCardView
12 | import com.wireguard.android.R
13 |
14 | class TvCardView(context: Context?, attrs: AttributeSet?) : MaterialCardView(context, attrs) {
15 | var isUp: Boolean = false
16 | set(value) {
17 | field = value
18 | refreshDrawableState()
19 | }
20 | var isDeleting: Boolean = false
21 | set(value) {
22 | field = value
23 | refreshDrawableState()
24 | }
25 |
26 | override fun onCreateDrawableState(extraSpace: Int): IntArray {
27 | if (isUp || isDeleting) {
28 | val drawableState = super.onCreateDrawableState(extraSpace + (if (isUp) 1 else 0) + (if (isDeleting) 1 else 0))
29 | if (isUp) {
30 | View.mergeDrawableStates(drawableState, STATE_IS_UP)
31 | }
32 | if (isDeleting) {
33 | View.mergeDrawableStates(drawableState, STATE_IS_DELETING)
34 | }
35 | return drawableState
36 | }
37 | return super.onCreateDrawableState(extraSpace)
38 | }
39 |
40 | companion object {
41 | private val STATE_IS_UP = intArrayOf(R.attr.state_isUp)
42 | private val STATE_IS_DELETING = intArrayOf(R.attr.state_isDeleting)
43 | }
44 | }
--------------------------------------------------------------------------------
/ui/src/main/res/anim/scale_down.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
15 |
16 |
--------------------------------------------------------------------------------
/ui/src/main/res/anim/scale_up.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
15 |
16 |
--------------------------------------------------------------------------------
/ui/src/main/res/color/tv_list_item_tint.xml:
--------------------------------------------------------------------------------
1 |
5 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_action_add_white.xml:
--------------------------------------------------------------------------------
1 |
5 |
11 |
14 |
15 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_action_delete.xml:
--------------------------------------------------------------------------------
1 |
5 |
11 |
14 |
15 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_action_edit.xml:
--------------------------------------------------------------------------------
1 |
5 |
11 |
14 |
15 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_action_generate.xml:
--------------------------------------------------------------------------------
1 |
5 |
11 |
14 |
15 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_action_open.xml:
--------------------------------------------------------------------------------
1 |
5 |
11 |
14 |
15 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_action_save.xml:
--------------------------------------------------------------------------------
1 |
5 |
11 |
14 |
15 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_action_scan_qr_code.xml:
--------------------------------------------------------------------------------
1 |
5 |
11 |
14 |
15 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_action_select_all.xml:
--------------------------------------------------------------------------------
1 |
5 |
11 |
14 |
15 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_action_share_white.xml:
--------------------------------------------------------------------------------
1 |
5 |
11 |
14 |
15 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_arrow_back.xml:
--------------------------------------------------------------------------------
1 |
5 |
11 |
14 |
15 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_settings.xml:
--------------------------------------------------------------------------------
1 |
5 |
11 |
14 |
15 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_tile.xml:
--------------------------------------------------------------------------------
1 |
5 |
10 |
16 |
22 |
28 |
29 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/list_item_background.xml:
--------------------------------------------------------------------------------
1 |
5 |
7 | -
8 |
9 |
-
12 |
13 |
14 | -
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout-sw600dp/main_activity.xml:
--------------------------------------------------------------------------------
1 |
5 |
12 |
13 |
21 |
22 |
29 |
30 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/add_tunnels_bottom_sheet.xml:
--------------------------------------------------------------------------------
1 |
5 |
11 |
12 |
34 |
35 |
58 |
59 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/app_list_dialog_fragment.xml:
--------------------------------------------------------------------------------
1 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
18 |
19 |
22 |
23 |
24 |
28 |
29 |
34 |
35 |
39 |
40 |
44 |
45 |
46 |
50 |
51 |
59 |
60 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/app_list_item.xml:
--------------------------------------------------------------------------------
1 |
5 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
19 |
20 |
23 |
24 |
25 |
34 |
35 |
42 |
43 |
55 |
56 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/config_naming_dialog_fragment.xml:
--------------------------------------------------------------------------------
1 |
5 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
17 |
18 |
22 |
23 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/log_viewer_activity.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
12 |
13 |
19 |
20 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/log_viewer_entry.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
12 |
13 |
22 |
23 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/main_activity.xml:
--------------------------------------------------------------------------------
1 |
5 |
12 |
13 |
19 |
20 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/tunnel_creator_activity.xml:
--------------------------------------------------------------------------------
1 |
5 |
12 |
13 |
18 |
19 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/tunnel_list_fragment.xml:
--------------------------------------------------------------------------------
1 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
20 |
21 |
24 |
25 |
26 |
32 |
33 |
47 |
48 |
55 |
56 |
64 |
65 |
73 |
74 |
75 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/tunnel_list_item.xml:
--------------------------------------------------------------------------------
1 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
18 |
19 |
22 |
23 |
26 |
27 |
30 |
31 |
32 |
42 |
43 |
54 |
55 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/tv_file_list_item.xml:
--------------------------------------------------------------------------------
1 |
5 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
19 |
20 |
21 |
30 |
31 |
34 |
35 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/tv_tunnel_list_item.xml:
--------------------------------------------------------------------------------
1 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
24 |
25 |
28 |
29 |
32 |
33 |
34 |
46 |
47 |
50 |
51 |
60 |
61 |
70 |
71 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/ui/src/main/res/menu/config_editor.xml:
--------------------------------------------------------------------------------
1 |
5 |
14 |
--------------------------------------------------------------------------------
/ui/src/main/res/menu/log_viewer.xml:
--------------------------------------------------------------------------------
1 |
5 |
13 |
--------------------------------------------------------------------------------
/ui/src/main/res/menu/main_activity.xml:
--------------------------------------------------------------------------------
1 |
5 |
15 |
--------------------------------------------------------------------------------
/ui/src/main/res/menu/tunnel_detail.xml:
--------------------------------------------------------------------------------
1 |
5 |
14 |
--------------------------------------------------------------------------------
/ui/src/main/res/menu/tunnel_list_action_mode.xml:
--------------------------------------------------------------------------------
1 |
5 |
20 |
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhcode-fun/wireguard-android/3198e22fab07496108913f6e6949bbd4e4ee3778/ui/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhcode-fun/wireguard-android/3198e22fab07496108913f6e6949bbd4e4ee3778/ui/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhcode-fun/wireguard-android/3198e22fab07496108913f6e6949bbd4e4ee3778/ui/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhcode-fun/wireguard-android/3198e22fab07496108913f6e6949bbd4e4ee3778/ui/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-xhdpi/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhcode-fun/wireguard-android/3198e22fab07496108913f6e6949bbd4e4ee3778/ui/src/main/res/mipmap-xhdpi/banner.png
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhcode-fun/wireguard-android/3198e22fab07496108913f6e6949bbd4e4ee3778/ui/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhcode-fun/wireguard-android/3198e22fab07496108913f6e6949bbd4e4ee3778/ui/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhcode-fun/wireguard-android/3198e22fab07496108913f6e6949bbd4e4ee3778/ui/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhcode-fun/wireguard-android/3198e22fab07496108913f6e6949bbd4e4ee3778/ui/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhcode-fun/wireguard-android/3198e22fab07496108913f6e6949bbd4e4ee3778/ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhcode-fun/wireguard-android/3198e22fab07496108913f6e6949bbd4e4ee3778/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/ui/src/main/res/resources.properties:
--------------------------------------------------------------------------------
1 | unqualifiedResLocale=en-US
2 |
--------------------------------------------------------------------------------
/ui/src/main/res/values-night/bools.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 | false
7 | false
8 |
9 |
--------------------------------------------------------------------------------
/ui/src/main/res/values-night/logviewer_colors.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 | #aaaaaa
7 | #ff0000
8 | #00ff00
9 | #ffff00
10 |
11 |
--------------------------------------------------------------------------------
/ui/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
34 |
35 |
--------------------------------------------------------------------------------
/ui/src/main/res/values-v23/styles.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/ui/src/main/res/values-v27/styles.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
12 |
13 |
--------------------------------------------------------------------------------
/ui/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/ui/src/main/res/values/bools.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 | true
7 | true
8 |
9 |
--------------------------------------------------------------------------------
/ui/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 | #1a73e8
7 | #005BC0
8 | #FFFFFF
9 | #D8E2FF
10 | #001A41
11 | #565E71
12 | #FFFFFF
13 | #DBE2F9
14 | #131B2C
15 | #715574
16 | #FFFFFF
17 | #FBD7FC
18 | #29132D
19 | #BA1A1A
20 | #FFDAD6
21 | #FFFFFF
22 | #410002
23 | #FEFBFF
24 | #1B1B1F
25 | #FEFBFF
26 | #1B1B1F
27 | #E1E2EC
28 | #44474F
29 | #74777F
30 | #F2F0F4
31 | #303033
32 | #ADC7FF
33 | #000000
34 | #005BC0
35 | #C4C6D0
36 | #000000
37 | #ADC7FF
38 | #002E68
39 | #004493
40 | #D8E2FF
41 | #BFC6DC
42 | #283041
43 | #3F4759
44 | #DBE2F9
45 | #DEBCDF
46 | #402843
47 | #583E5B
48 | #FBD7FC
49 | #FFB4AB
50 | #93000A
51 | #690005
52 | #FFDAD6
53 | #1B1B1F
54 | #E3E2E6
55 | #1B1B1F
56 | #E3E2E6
57 | #44474F
58 | #C4C6D0
59 | #8E9099
60 | #1B1B1F
61 | #E3E2E6
62 | #005BC0
63 | #000000
64 | #ADC7FF
65 | #44474F
66 | #000000
67 |
68 |
--------------------------------------------------------------------------------
/ui/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 | 16dp
7 | 56dp
8 | 8dp
9 | 8dp
10 | 16dp
11 | 16dp
12 |
13 |
--------------------------------------------------------------------------------
/ui/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 | #871719
7 |
8 |
--------------------------------------------------------------------------------
/ui/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ui/src/main/res/values/logviewer_colors.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 | #444444
7 | #aa0000
8 | #00aa00
9 | #aaaa00
10 |
11 |
--------------------------------------------------------------------------------
/ui/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
10 |
11 |
17 |
18 |
19 |
20 |
21 |
25 |
26 |
30 |
31 |
43 |
44 |
48 |
49 |
--------------------------------------------------------------------------------
/ui/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
34 |
35 |
--------------------------------------------------------------------------------
/ui/src/main/res/xml/app_restrictions.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
12 |
13 |
--------------------------------------------------------------------------------
/ui/src/main/res/xml/preferences.xml:
--------------------------------------------------------------------------------
1 |
5 |
7 |
10 |
17 |
18 |
19 |
24 |
31 |
38 |
39 |
40 |
47 |
48 |
49 |
--------------------------------------------------------------------------------