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-2023 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/InetAddresses.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2023 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.lang.reflect.Method;
11 | import java.net.Inet4Address;
12 | import java.net.Inet6Address;
13 | import java.net.InetAddress;
14 | import java.net.UnknownHostException;
15 | import java.util.regex.Pattern;
16 |
17 | import androidx.annotation.Nullable;
18 |
19 | /**
20 | * Utility methods for creating instances of {@link InetAddress}.
21 | */
22 | @NonNullForAll
23 | public final class InetAddresses {
24 | @Nullable private static final Method PARSER_METHOD;
25 | private static final Pattern WONT_TOUCH_RESOLVER = Pattern.compile("^(((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?)|((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$");
26 | private static final Pattern VALID_HOSTNAME = Pattern.compile("^(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?(?:\\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?)*\\.?$");
27 |
28 | static {
29 | Method m = null;
30 | try {
31 | if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.Q)
32 | // noinspection JavaReflectionMemberAccess
33 | m = InetAddress.class.getMethod("parseNumericAddress", String.class);
34 | } catch (final Exception ignored) {
35 | }
36 | PARSER_METHOD = m;
37 | }
38 |
39 | private InetAddresses() {
40 | }
41 |
42 | /**
43 | * Determines whether input is a valid DNS hostname.
44 | *
45 | * @param maybeHostname a string that is possibly a DNS hostname
46 | * @return whether or not maybeHostname is a valid DNS hostname
47 | */
48 | public static boolean isHostname(final CharSequence maybeHostname) {
49 | return VALID_HOSTNAME.matcher(maybeHostname).matches();
50 | }
51 |
52 | /**
53 | * Parses a numeric IPv4 or IPv6 address without performing any DNS lookups.
54 | *
55 | * @param address a string representing the IP address
56 | * @return an instance of {@link Inet4Address} or {@link Inet6Address}, as appropriate
57 | */
58 | public static InetAddress parse(final String address) throws ParseException {
59 | if (address.isEmpty())
60 | throw new ParseException(InetAddress.class, address, "Empty address");
61 | try {
62 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q)
63 | return android.net.InetAddresses.parseNumericAddress(address);
64 | else if (PARSER_METHOD != null)
65 | return (InetAddress) PARSER_METHOD.invoke(null, address);
66 | else
67 | throw new NoSuchMethodException("parseNumericAddress");
68 | } catch (final IllegalArgumentException e) {
69 | throw new ParseException(InetAddress.class, address, e);
70 | } catch (final Exception e) {
71 | final Throwable cause = e.getCause();
72 | // Re-throw parsing exceptions with the original type, as callers might try to catch
73 | // them. On the other hand, callers cannot be expected to handle reflection failures.
74 | if (cause instanceof IllegalArgumentException)
75 | throw new ParseException(InetAddress.class, address, cause);
76 | try {
77 | if (WONT_TOUCH_RESOLVER.matcher(address).matches())
78 | return InetAddress.getByName(address);
79 | else
80 | throw new ParseException(InetAddress.class, address, "Not an IP address");
81 | } catch (final UnknownHostException f) {
82 | throw new ParseException(InetAddress.class, address, f);
83 | }
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/tunnel/src/main/java/com/wireguard/config/InetNetwork.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2023 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-2023 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-2023 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-2023 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-2023 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-2023 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-2023 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-2023 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.20.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 := c1e1161d6d859deb576e6cfabeb40e3d042ceb1c6f444f617c3c9d76269c3565
27 | GO_HASH_darwin-arm64 := 86b0ed0f2b2df50fa8036eea875d1cf2d76cefdacf247c44639a1464b7e36b95
28 | GO_HASH_linux-amd64 := 979694c2c25c735755bf26f4f45e19e64e4811d661dd07b8c010f7a8e18adfca
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.20
4 |
5 | require (
6 | golang.org/x/sys v0.6.0
7 | golang.zx2c4.com/wireguard v0.0.0-20230223181233-21636207a675
8 | )
9 |
10 | require (
11 | golang.org/x/crypto v0.7.0 // indirect
12 | golang.org/x/net v0.8.0 // indirect
13 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
14 | )
15 |
--------------------------------------------------------------------------------
/tunnel/tools/libwg-go/go.sum:
--------------------------------------------------------------------------------
1 | golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
2 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
3 | golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
4 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
5 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
6 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
7 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
8 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
9 | golang.zx2c4.com/wireguard v0.0.0-20230223181233-21636207a675 h1:/J/RVnr7ng4fWPRH3xa4WtBJ1Jp+Auu4YNLmGiPv5QU=
10 | golang.zx2c4.com/wireguard v0.0.0-20230223181233-21636207a675/go.mod h1:whfbyDBt09xhCYQWtO2+3UVjlaq6/9hDZrjg2ZE6SyA=
11 |
--------------------------------------------------------------------------------
/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-2023 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-2023 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 = 34
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 = 34
26 | versionCode = providers.gradleProperty("wireguardVersionCode").get().toInt()
27 | versionName = providers.gradleProperty("wireguardVersionName").get()
28 | buildConfigField("int", "MIN_SDK_VERSION", minSdk.toString())
29 | }
30 | compileOptions {
31 | sourceCompatibility = JavaVersion.VERSION_17
32 | targetCompatibility = JavaVersion.VERSION_17
33 | isCoreLibraryDesugaringEnabled = true
34 | }
35 | buildTypes {
36 | release {
37 | isMinifyEnabled = true
38 | isShrinkResources = true
39 | proguardFiles("proguard-android-optimize.txt")
40 | packaging {
41 | resources {
42 | excludes += "DebugProbesKt.bin"
43 | excludes += "kotlin-tooling-metadata.json"
44 | excludes += "META-INF/*.version"
45 | }
46 | }
47 | }
48 | debug {
49 | applicationIdSuffix = ".debug"
50 | versionNameSuffix = "-debug"
51 | }
52 | create("googleplay") {
53 | initWith(getByName("release"))
54 | matchingFallbacks += "release"
55 | }
56 | }
57 | androidResources {
58 | generateLocaleConfig = true
59 | }
60 | lint {
61 | disable += "LongLogTag"
62 | warning += "MissingTranslation"
63 | warning += "ImpliedQuantity"
64 | }
65 | }
66 |
67 | dependencies {
68 | implementation(project(":tunnel"))
69 | implementation(libs.androidx.activity.ktx)
70 | implementation(libs.androidx.annotation)
71 | implementation(libs.androidx.appcompat)
72 | implementation(libs.androidx.constraintlayout)
73 | implementation(libs.androidx.coordinatorlayout)
74 | implementation(libs.androidx.biometric)
75 | implementation(libs.androidx.core.ktx)
76 | implementation(libs.androidx.fragment.ktx)
77 | implementation(libs.androidx.preference.ktx)
78 | implementation(libs.androidx.lifecycle.runtime.ktx)
79 | implementation(libs.androidx.datastore.preferences)
80 | implementation(libs.google.material)
81 | implementation(libs.zxing.android.embedded)
82 | implementation(libs.kotlinx.coroutines.android)
83 | coreLibraryDesugaring(libs.desugarJdkLibs)
84 | }
85 |
86 | tasks.withType().configureEach {
87 | options.compilerArgs.add("-Xlint:unchecked")
88 | options.isDeprecation = true
89 | }
90 |
91 | tasks.withType().configureEach {
92 | compilerOptions.jvmTarget.set(JvmTarget.JVM_17)
93 | }
94 |
--------------------------------------------------------------------------------
/ui/proguard-android-optimize.txt:
--------------------------------------------------------------------------------
1 | -allowaccessmodification
2 | -dontusemixedcaseclassnames
3 | -dontobfuscate
4 | -verbose
5 |
6 | -keepattributes *Annotation*
7 |
8 | -keepclasseswithmembernames class * {
9 | native ;
10 | }
11 |
12 | -keepclassmembers enum * {
13 | public static **[] values();
14 | public static ** valueOf(java.lang.String);
15 | }
16 |
17 | -keepclassmembers class * implements android.os.Parcelable {
18 | public static final ** CREATOR;
19 | }
20 |
21 | -keep class androidx.annotation.Keep
22 |
23 | -keep @androidx.annotation.Keep class * {*;}
24 |
25 | -keepclasseswithmembers class * {
26 | @androidx.annotation.Keep ;
27 | }
28 |
29 | -keepclasseswithmembers class * {
30 | @androidx.annotation.Keep ;
31 | }
32 |
33 | -keepclasseswithmembers class * {
34 | @androidx.annotation.Keep (...);
35 | }
36 |
--------------------------------------------------------------------------------
/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 |
2 |
4 |
5 |
8 |
9 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/BootShutdownReceiver.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2023 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-2023 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-2023 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.fragment.app.commit
9 | import com.wireguard.android.fragment.TunnelEditorFragment
10 | import com.wireguard.android.model.ObservableTunnel
11 |
12 | /**
13 | * Standalone activity for creating tunnels.
14 | */
15 | class TunnelCreatorActivity : BaseActivity() {
16 | override fun onCreate(savedInstanceState: Bundle?) {
17 | super.onCreate(savedInstanceState)
18 | if (supportFragmentManager.findFragmentById(android.R.id.content) == null) {
19 | supportFragmentManager.commit {
20 | add(android.R.id.content, TunnelEditorFragment())
21 | }
22 | }
23 | }
24 |
25 | override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?): Boolean {
26 | finish()
27 | return true
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2023 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 | val intent = GoBackend.VpnService.prepare(this@TunnelToggleActivity)
54 | if (intent != null) {
55 | permissionActivityResultLauncher.launch(intent)
56 | return@launch
57 | }
58 | }
59 | toggleTunnelWithPermissionsResult()
60 | }
61 | }
62 |
63 | companion object {
64 | private const val TAG = "WireGuard/TunnelToggleActivity"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/configStore/ConfigStore.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2023 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-2023 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-2023 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-2023 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/ObservableKeyedRecyclerViewAdapter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.databinding
6 |
7 | import android.content.Context
8 | import android.view.LayoutInflater
9 | import android.view.ViewGroup
10 | import androidx.databinding.DataBindingUtil
11 | import androidx.databinding.ObservableList
12 | import androidx.databinding.ViewDataBinding
13 | import androidx.recyclerview.widget.RecyclerView
14 | import com.wireguard.android.BR
15 | import java.lang.ref.WeakReference
16 |
17 | /**
18 | * A generic `RecyclerView.Adapter` backed by a `ObservableKeyedArrayList`.
19 | */
20 | class ObservableKeyedRecyclerViewAdapter> internal constructor(
21 | context: Context, private val layoutId: Int,
22 | list: ObservableKeyedArrayList?
23 | ) : RecyclerView.Adapter() {
24 | private val callback = OnListChangedCallback(this)
25 | private val layoutInflater: LayoutInflater = LayoutInflater.from(context)
26 | private var list: ObservableKeyedArrayList? = null
27 | private var rowConfigurationHandler: RowConfigurationHandler? = null
28 |
29 | private fun getItem(position: Int): E? = if (list == null || position < 0 || position >= list!!.size) null else list?.get(position)
30 |
31 | override fun getItemCount() = list?.size ?: 0
32 |
33 | override fun getItemId(position: Int) = (getKey(position)?.hashCode() ?: -1).toLong()
34 |
35 | private fun getKey(position: Int): K? = getItem(position)?.key
36 |
37 | override fun onBindViewHolder(holder: ViewHolder, position: Int) {
38 | holder.binding.setVariable(BR.collection, list)
39 | holder.binding.setVariable(BR.key, getKey(position))
40 | holder.binding.setVariable(BR.item, getItem(position))
41 | holder.binding.executePendingBindings()
42 | if (rowConfigurationHandler != null) {
43 | val item = getItem(position)
44 | if (item != null) {
45 | rowConfigurationHandler?.onConfigureRow(holder.binding, item, position)
46 | }
47 | }
48 | }
49 |
50 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(DataBindingUtil.inflate(layoutInflater, layoutId, parent, false))
51 |
52 | fun setList(newList: ObservableKeyedArrayList?) {
53 | list?.removeOnListChangedCallback(callback)
54 | list = newList
55 | list?.addOnListChangedCallback(callback)
56 | notifyDataSetChanged()
57 | }
58 |
59 | fun setRowConfigurationHandler(rowConfigurationHandler: RowConfigurationHandler<*, *>?) {
60 | @Suppress("UNCHECKED_CAST")
61 | this.rowConfigurationHandler = rowConfigurationHandler as? RowConfigurationHandler
62 | }
63 |
64 | interface RowConfigurationHandler {
65 | fun onConfigureRow(binding: B, item: T, position: Int)
66 | }
67 |
68 | private class OnListChangedCallback> constructor(adapter: ObservableKeyedRecyclerViewAdapter<*, E>) : ObservableList.OnListChangedCallback>() {
69 | private val weakAdapter: WeakReference> = WeakReference(adapter)
70 |
71 | override fun onChanged(sender: ObservableList) {
72 | val adapter = weakAdapter.get()
73 | if (adapter != null)
74 | adapter.notifyDataSetChanged()
75 | else
76 | sender.removeOnListChangedCallback(this)
77 | }
78 |
79 | override fun onItemRangeChanged(sender: ObservableList, positionStart: Int,
80 | itemCount: Int) {
81 | onChanged(sender)
82 | }
83 |
84 | override fun onItemRangeInserted(sender: ObservableList, positionStart: Int,
85 | itemCount: Int) {
86 | onChanged(sender)
87 | }
88 |
89 | override fun onItemRangeMoved(sender: ObservableList, fromPosition: Int,
90 | toPosition: Int, itemCount: Int) {
91 | onChanged(sender)
92 | }
93 |
94 | override fun onItemRangeRemoved(sender: ObservableList, positionStart: Int,
95 | itemCount: Int) {
96 | onChanged(sender)
97 | }
98 |
99 | }
100 |
101 | class ViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root)
102 |
103 | init {
104 | setList(list)
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/databinding/ObservableSortedKeyedArrayList.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2023 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-2023 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-2023 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-2023 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-2023 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-2023 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 |
19 | class DonatePreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
20 | override fun getSummary() = context.getString(R.string.donate_summary)
21 |
22 | override fun getTitle() = context.getString(R.string.donate_title)
23 |
24 | override fun onClick() {
25 | /* Google Play Store forbids links to our donation page. */
26 | if (Updater.installerIsGooglePlay(context)) {
27 | MaterialAlertDialogBuilder(context)
28 | .setTitle(R.string.donate_title)
29 | .setMessage(R.string.donate_google_play_disappointment)
30 | .show()
31 | return
32 | }
33 |
34 | val intent = Intent(Intent.ACTION_VIEW)
35 | intent.data = Uri.parse("https://www.wireguard.com/donations/")
36 | try {
37 | context.startActivity(intent)
38 | } catch (e: Throwable) {
39 | Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_SHORT).show()
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/preference/KernelModuleEnablerPreference.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2023 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-2023 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-2023 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-2023 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-2023 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-2023 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-2023 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.view.View
10 | import android.widget.TextView
11 | import androidx.core.content.getSystemService
12 | import com.google.android.material.snackbar.Snackbar
13 | import com.google.android.material.textfield.TextInputEditText
14 | import com.wireguard.android.R
15 |
16 | /**
17 | * Standalone utilities for interacting with the system clipboard.
18 | */
19 | object ClipboardUtils {
20 | @JvmStatic
21 | fun copyTextView(view: View) {
22 | val data = when (view) {
23 | is TextInputEditText -> Pair(view.editableText, view.hint)
24 | is TextView -> Pair(view.text, view.contentDescription)
25 | else -> return
26 | }
27 | if (data.first == null || data.first.isEmpty()) {
28 | return
29 | }
30 | val service = view.context.getSystemService() ?: return
31 | service.setPrimaryClip(ClipData.newPlainText(data.second, data.first))
32 | Snackbar.make(view, view.context.getString(R.string.copied_to_clipboard, data.second), Snackbar.LENGTH_LONG).show()
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/util/Extensions.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2023 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-2023 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 |
33 | private fun scanBitmapForResult(source: Bitmap): Result {
34 | val width = source.width
35 | val height = source.height
36 | val pixels = IntArray(width * height)
37 | source.getPixels(pixels, 0, width, 0, 0, width, height)
38 |
39 | val bBitmap = BinaryBitmap(HybridBinarizer(RGBLuminanceSource(width, height, pixels)))
40 | return reader.decode(bBitmap, mapOf(DecodeHintType.TRY_HARDER to true))
41 | }
42 |
43 | private fun downscaleBitmap(source: Bitmap, scaledSize: Int): Bitmap {
44 |
45 | val originalWidth = source.width
46 | val originalHeight = source.height
47 |
48 | var newWidth = -1
49 | var newHeight = -1
50 | val multFactor: Float
51 |
52 | when {
53 | originalHeight > originalWidth -> {
54 | newHeight = scaledSize
55 | multFactor = originalWidth.toFloat() / originalHeight.toFloat()
56 | newWidth = (newHeight * multFactor).toInt()
57 | }
58 |
59 | originalWidth > originalHeight -> {
60 | newWidth = scaledSize
61 | multFactor = originalHeight.toFloat() / originalWidth.toFloat()
62 | newHeight = (newWidth * multFactor).toInt()
63 | }
64 |
65 | originalHeight == originalWidth -> {
66 | newHeight = scaledSize
67 | newWidth = scaledSize
68 | }
69 | }
70 | return Bitmap.createScaledBitmap(source, newWidth, newHeight, false)
71 | }
72 |
73 | private fun doScan(data: Uri): Result {
74 | Log.d(TAG, "Starting to scan an image: $data")
75 | contentResolver.openInputStream(data).use { inputStream ->
76 | val originalBitmap = BitmapFactory.decodeStream(inputStream)
77 | ?: throw IllegalArgumentException("Can't decode stream to Bitmap")
78 |
79 | return try {
80 | scanBitmapForResult(originalBitmap).also {
81 | Log.d(TAG, "Found result in original image")
82 | }
83 | } catch (e: Exception) {
84 | Log.e(TAG, "Original image scan finished with error: $e, will try downscaled image")
85 | val scaleBitmap = downscaleBitmap(originalBitmap, 500)
86 | scanBitmapForResult(originalBitmap).also { scaleBitmap.recycle() }
87 | } finally {
88 | originalBitmap.recycle()
89 | }
90 | }
91 |
92 | }
93 |
94 | /**
95 | * Attempts to parse incoming data
96 | * @return result of the decoding operation
97 | * @throws NotFoundException when parser didn't find QR code in the image
98 | */
99 | suspend fun scan(data: Uri) = withContext(Dispatchers.Default) { doScan(data) }
100 |
101 | companion object {
102 | private const val TAG = "QrCodeFromFileScanner"
103 |
104 | /**
105 | * Given a reference to a file, check if this file could be parsed by this class
106 | * @return true if the file can be parsed, false if not
107 | */
108 | fun validContentType(contentResolver: ContentResolver, data: Uri): Boolean {
109 | return contentResolver.getType(data)?.startsWith("image/") == true
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2023 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-2023 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-2023 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-2023 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-2023 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-2023 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-2023 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 |
2 |
3 |
12 |
13 |
--------------------------------------------------------------------------------
/ui/src/main/res/anim/scale_up.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
--------------------------------------------------------------------------------
/ui/src/main/res/color/tv_list_item_tint.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_action_add_white.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_action_delete.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_action_edit.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_action_generate.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_action_open.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_action_save.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_action_scan_qr_code.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_action_select_all.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_action_share_white.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_arrow_back.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
12 |
15 |
16 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
13 |
14 |
17 |
20 |
21 |
22 |
25 |
26 |
29 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_settings.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_tile.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
25 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/list_item_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
5 |
6 |
-
9 |
10 |
11 | -
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout-sw600dp/main_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
25 |
26 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/add_tunnels_bottom_sheet.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
31 |
32 |
55 |
56 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/app_list_dialog_fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
19 |
20 |
21 |
25 |
26 |
31 |
32 |
36 |
37 |
41 |
42 |
43 |
47 |
48 |
56 |
57 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/app_list_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
16 |
17 |
20 |
21 |
22 |
31 |
32 |
39 |
40 |
52 |
53 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/config_naming_dialog_fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
19 |
20 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/log_viewer_activity.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
11 |
12 |
18 |
19 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/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 |
2 |
9 |
10 |
16 |
17 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/tunnel_list_fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
17 |
18 |
21 |
22 |
23 |
29 |
30 |
44 |
45 |
52 |
53 |
61 |
62 |
70 |
71 |
72 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/tunnel_list_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
19 |
20 |
23 |
24 |
27 |
28 |
29 |
39 |
40 |
51 |
52 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/tv_file_list_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
16 |
17 |
18 |
27 |
28 |
31 |
32 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/tv_tunnel_list_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
17 |
18 |
21 |
22 |
25 |
26 |
29 |
30 |
31 |
43 |
44 |
47 |
48 |
57 |
58 |
67 |
68 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/ui/src/main/res/menu/config_editor.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
--------------------------------------------------------------------------------
/ui/src/main/res/menu/log_viewer.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
--------------------------------------------------------------------------------
/ui/src/main/res/menu/main_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
--------------------------------------------------------------------------------
/ui/src/main/res/menu/tunnel_detail.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
--------------------------------------------------------------------------------
/ui/src/main/res/menu/tunnel_list_action_mode.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/heiher/wireguard-android/e21fb5b7ed6dffd2dc2a5e9e3340ecf274cf92eb/ui/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/heiher/wireguard-android/e21fb5b7ed6dffd2dc2a5e9e3340ecf274cf92eb/ui/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/heiher/wireguard-android/e21fb5b7ed6dffd2dc2a5e9e3340ecf274cf92eb/ui/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/heiher/wireguard-android/e21fb5b7ed6dffd2dc2a5e9e3340ecf274cf92eb/ui/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-xhdpi/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/heiher/wireguard-android/e21fb5b7ed6dffd2dc2a5e9e3340ecf274cf92eb/ui/src/main/res/mipmap-xhdpi/banner.png
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/heiher/wireguard-android/e21fb5b7ed6dffd2dc2a5e9e3340ecf274cf92eb/ui/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/heiher/wireguard-android/e21fb5b7ed6dffd2dc2a5e9e3340ecf274cf92eb/ui/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/heiher/wireguard-android/e21fb5b7ed6dffd2dc2a5e9e3340ecf274cf92eb/ui/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/heiher/wireguard-android/e21fb5b7ed6dffd2dc2a5e9e3340ecf274cf92eb/ui/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/heiher/wireguard-android/e21fb5b7ed6dffd2dc2a5e9e3340ecf274cf92eb/ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/heiher/wireguard-android/e21fb5b7ed6dffd2dc2a5e9e3340ecf274cf92eb/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 |
2 |
3 | false
4 | false
5 |
6 |
--------------------------------------------------------------------------------
/ui/src/main/res/values-night/logviewer_colors.xml:
--------------------------------------------------------------------------------
1 |
2 | #aaaaaa
3 | #ff0000
4 | #00ff00
5 | #ffff00
6 |
7 |
--------------------------------------------------------------------------------
/ui/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
31 |
32 |
--------------------------------------------------------------------------------
/ui/src/main/res/values-v23/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
--------------------------------------------------------------------------------
/ui/src/main/res/values-v27/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
--------------------------------------------------------------------------------
/ui/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ui/src/main/res/values/bools.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | true
4 | true
5 |
6 |
--------------------------------------------------------------------------------
/ui/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 | #1a73e8
3 | #005BC0
4 | #FFFFFF
5 | #D8E2FF
6 | #001A41
7 | #565E71
8 | #FFFFFF
9 | #DBE2F9
10 | #131B2C
11 | #715574
12 | #FFFFFF
13 | #FBD7FC
14 | #29132D
15 | #BA1A1A
16 | #FFDAD6
17 | #FFFFFF
18 | #410002
19 | #FEFBFF
20 | #1B1B1F
21 | #FEFBFF
22 | #1B1B1F
23 | #E1E2EC
24 | #44474F
25 | #74777F
26 | #F2F0F4
27 | #303033
28 | #ADC7FF
29 | #000000
30 | #005BC0
31 | #C4C6D0
32 | #000000
33 | #ADC7FF
34 | #002E68
35 | #004493
36 | #D8E2FF
37 | #BFC6DC
38 | #283041
39 | #3F4759
40 | #DBE2F9
41 | #DEBCDF
42 | #402843
43 | #583E5B
44 | #FBD7FC
45 | #FFB4AB
46 | #93000A
47 | #690005
48 | #FFDAD6
49 | #1B1B1F
50 | #E3E2E6
51 | #1B1B1F
52 | #E3E2E6
53 | #44474F
54 | #C4C6D0
55 | #8E9099
56 | #1B1B1F
57 | #E3E2E6
58 | #005BC0
59 | #000000
60 | #ADC7FF
61 | #44474F
62 | #000000
63 |
64 |
--------------------------------------------------------------------------------
/ui/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 56dp
5 | 8dp
6 | 8dp
7 | 16dp
8 | 16dp
9 |
10 |
--------------------------------------------------------------------------------
/ui/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #871719
4 |
--------------------------------------------------------------------------------
/ui/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/ui/src/main/res/values/logviewer_colors.xml:
--------------------------------------------------------------------------------
1 |
2 | #444444
3 | #aa0000
4 | #00aa00
5 | #aaaa00
6 |
7 |
--------------------------------------------------------------------------------
/ui/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
14 |
15 |
16 |
17 |
18 |
22 |
23 |
27 |
28 |
40 |
41 |
45 |
46 |
--------------------------------------------------------------------------------
/ui/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
31 |
32 |
--------------------------------------------------------------------------------
/ui/src/main/res/xml/app_restrictions.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
13 |
14 |
--------------------------------------------------------------------------------
/ui/src/main/res/xml/preferences.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
14 |
15 |
16 |
21 |
28 |
35 |
36 |
37 |
44 |
45 |
46 |
--------------------------------------------------------------------------------