mirror of
https://github.com/amnezia-vpn/amneziawg-android.git
synced 2026-07-03 14:05:13 +02:00
Compare commits
113 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3785752364 | |||
| 398d8a1e41 | |||
| dfd8ca6f79 | |||
| 7cff4367d7 | |||
| 9eaed5e745 | |||
| 68350bb4df | |||
| 12be972fcd | |||
| d200437813 | |||
| 3ffe7a5e68 | |||
| 08ff9f5ece | |||
| 4bee579e48 | |||
| a906c478c9 | |||
| 306d0648c6 | |||
| e99ccf9013 | |||
| 59935a12b9 | |||
| a9ec828506 | |||
| eebeece856 | |||
| 746ab00794 | |||
| b877593d55 | |||
| dcd596907a | |||
| 44c2afbfba | |||
| bd1679b7e0 | |||
| ff7d7e0edd | |||
| 2f088938c6 | |||
| 53adb0e9a6 | |||
| 6789c11a7b | |||
| c56065fcfe | |||
| 52a2ae36f6 | |||
| abcb51d2a6 | |||
| 8b9a40b3d7 | |||
| 4b36df504c | |||
| 35fe5bd5f0 | |||
| 79ae85c728 | |||
| 49ac61304e | |||
| d79cdb0d41 | |||
| a3726b07bf | |||
| 80c35a2053 | |||
| 601b58b670 | |||
| 9cf049775f | |||
| 92122e60c6 | |||
| f20d0f0659 | |||
| 9346a63753 | |||
| bab70ab51e | |||
| 2fc0bb1a03 | |||
| dd0ff8fe60 | |||
| 45a179580d | |||
| 0bcee7f9cc | |||
| af10b117b4 | |||
| 7aa7825209 | |||
| 8b7617294e | |||
| 9985b9b08e | |||
| 840d65881e | |||
| c18f6818e8 | |||
| dcd91cad1b | |||
| 898bb679d2 | |||
| 348d430cd3 | |||
| e3d98633fb | |||
| b451920408 | |||
| 1fa15e76e3 | |||
| 8a58270e03 | |||
| e08d873cc3 | |||
| 3748a1da88 | |||
| 9597d719ac | |||
| 041ce2134f | |||
| 4c17fc51e7 | |||
| 6cf7439053 | |||
| 814ab4937d | |||
| 3d088411e2 | |||
| 39e0c861e2 | |||
| d60efcd7c7 | |||
| 4dff537d21 | |||
| 761c2ade4a | |||
| 115a87af32 | |||
| c7ecaeb145 | |||
| 8e2d63db75 | |||
| 3208bac987 | |||
| fc1f2132fe | |||
| 09125e1e31 | |||
| 6b1164ad8d | |||
| c68a5c776b | |||
| f3ac787f5a | |||
| 751be2c469 | |||
| cfb7f1b016 | |||
| 33e07628db | |||
| b1b7d1d90c | |||
| 6cb9548f72 | |||
| 8207ae1e8d | |||
| 48e0e427eb | |||
| 03e95d2dd3 | |||
| 58b14cc650 | |||
| 3fa8e09545 | |||
| 9d76b354f0 | |||
| 1856a50c56 | |||
| ba0f3ece04 | |||
| e8ad75d924 | |||
| b858284b1e | |||
| 3c6e06f8d5 | |||
| 0d33a00e72 | |||
| 830d0992a7 | |||
| e4192ea172 | |||
| 4bbb1a0fcd | |||
| 77b5937fbb | |||
| 3144d36056 | |||
| 521bbb4b4c | |||
| 0a55e10b94 | |||
| d6e5fd9301 | |||
| 2c625f56fd | |||
| 7db0fa915e | |||
| e424765a61 | |||
| 1ca4dbf1a2 | |||
| 065893e31d | |||
| ef70aa88e1 | |||
| 6a11a199ca |
@@ -15,3 +15,4 @@ build/
|
||||
*.iml
|
||||
*.jks
|
||||
keystore.properties
|
||||
gradlew.bat
|
||||
|
||||
Generated
+10
-1
@@ -59,7 +59,16 @@
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
<value>
|
||||
<package name="kotlinx.android.synthetic" withSubpackages="true" static="false" />
|
||||
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="PACKAGES_IMPORT_LAYOUT">
|
||||
<value>
|
||||
<package name="" alias="false" withSubpackages="true" />
|
||||
<package name="java" alias="false" withSubpackages="true" />
|
||||
<package name="javax" alias="false" withSubpackages="true" />
|
||||
<package name="kotlin" alias="false" withSubpackages="true" />
|
||||
<package name="" alias="true" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="10" />
|
||||
|
||||
+25
-24
@@ -1,34 +1,26 @@
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
}
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
agpVersion = '3.6.2'
|
||||
activityVersion = '1.2.0-alpha07'
|
||||
agpVersion = '4.0.1'
|
||||
annotationsVersion = '1.1.0'
|
||||
appcompatVersion = '1.1.0'
|
||||
bintrayPluginVersion = '1.8.4'
|
||||
biometricVersion = '1.0.1'
|
||||
cardviewVersion = '1.0.0'
|
||||
appcompatVersion = '1.2.0'
|
||||
bintrayPluginVersion = '1.8.5'
|
||||
biometricVersion = '1.1.0-alpha02'
|
||||
collectionVersion = '1.1.0'
|
||||
constraintLayoutVersion = '1.1.3'
|
||||
constraintLayoutVersion = '2.0.1'
|
||||
coordinatorLayoutVersion = '1.1.0'
|
||||
coreKtxVersion = '1.2.0'
|
||||
coroutinesVersion = '1.3.5'
|
||||
eddsaVersion = '0.3.0'
|
||||
fragmentVersion = '1.2.4'
|
||||
coreKtxVersion = '1.3.1'
|
||||
coroutinesVersion = '1.3.9'
|
||||
datastoreVersion = '1.0.0-alpha01'
|
||||
desugarVersion = '1.0.10'
|
||||
fragmentVersion = '1.3.0-alpha07'
|
||||
jsr305Version = '3.0.2'
|
||||
junitVersion = '4.13'
|
||||
kotlinVersion = '1.3.71'
|
||||
materialComponentsVersion = '1.1.0'
|
||||
kotlinVersion = '1.4.10'
|
||||
lifecycleRuntimeKtxVersion = '2.3.0-alpha07'
|
||||
materialComponentsVersion = '22973ff10305397bfc72fed41684175fee8aeefb'
|
||||
mavenPluginVersion = '2.1'
|
||||
preferenceVersion = '1.1.0'
|
||||
streamsupportVersion = '1.7.2'
|
||||
threetenabpVersion = '1.2.3'
|
||||
preferenceVersion = '1.1.1'
|
||||
zxingEmbeddedVersion = '3.6.0'
|
||||
|
||||
groupName = 'com.wireguard.android'
|
||||
@@ -45,14 +37,23 @@ buildscript {
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
tasks {
|
||||
wrapper {
|
||||
gradleVersion = "6.2.2"
|
||||
gradleVersion = "6.6.1"
|
||||
distributionType = Wrapper.DistributionType.ALL
|
||||
distributionSha256Sum = "11657af6356b7587bfb37287b5992e94a9686d5c8a0a1b60b87b9928a2decde5"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+2
-1
@@ -1,5 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip
|
||||
distributionSha256Sum=11657af6356b7587bfb37287b5992e94a9686d5c8a0a1b60b87b9928a2decde5
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -82,6 +82,7 @@ esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
@@ -129,6 +130,7 @@ fi
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
|
||||
+8
-6
@@ -4,15 +4,14 @@ version wireguardVersionName
|
||||
group groupName
|
||||
|
||||
android {
|
||||
buildToolsVersion '29.0.3'
|
||||
compileSdkVersion 29
|
||||
compileSdkVersion 30
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
targetSdkVersion 30
|
||||
versionCode wireguardVersionCode
|
||||
versionName wireguardVersionName
|
||||
}
|
||||
@@ -21,6 +20,9 @@ android {
|
||||
path 'tools/CMakeLists.txt'
|
||||
}
|
||||
}
|
||||
libraryVariants.all {
|
||||
it.generateBuildConfigProvider.configure { enabled = false }
|
||||
}
|
||||
testOptions.unitTests.all {
|
||||
testLogging {
|
||||
events 'passed', 'skipped', 'failed', 'standardOut', 'standardError'
|
||||
@@ -42,15 +44,15 @@ android {
|
||||
}
|
||||
}
|
||||
}
|
||||
lintOptions {
|
||||
disable('LongLogTag')
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion"
|
||||
implementation "androidx.annotation:annotation:$annotationsVersion"
|
||||
implementation "androidx.collection:collection:$collectionVersion"
|
||||
implementation "com.google.code.findbugs:jsr305:$jsr305Version"
|
||||
implementation "com.jakewharton.threetenabp:threetenabp:$threetenabpVersion"
|
||||
implementation "net.i2p.crypto:eddsa:$eddsaVersion"
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ public interface Backend {
|
||||
*
|
||||
* @param tunnel The tunnel to examine the state of.
|
||||
* @return The state of the tunnel.
|
||||
* @throws Exception Exception raised when retrieving tunnel's state.
|
||||
*/
|
||||
Tunnel.State getState(Tunnel tunnel) throws Exception;
|
||||
|
||||
@@ -39,6 +40,7 @@ public interface Backend {
|
||||
*
|
||||
* @param tunnel The tunnel to retrieve statistics for.
|
||||
* @return The statistics for the tunnel.
|
||||
* @throws Exception Exception raised when retrieving statistics.
|
||||
*/
|
||||
Statistics getStatistics(Tunnel tunnel) throws Exception;
|
||||
|
||||
@@ -46,7 +48,7 @@ public interface Backend {
|
||||
* Determine version of underlying backend.
|
||||
*
|
||||
* @return The version of the backend.
|
||||
* @throws Exception
|
||||
* @throws Exception Exception raised while retrieving version.
|
||||
*/
|
||||
String getVersion() throws Exception;
|
||||
|
||||
@@ -59,6 +61,7 @@ public interface Backend {
|
||||
* {@code TOGGLE}.
|
||||
* @param config The configuration for this tunnel, may be null if state is {@code DOWN}.
|
||||
* @return The updated state of the tunnel.
|
||||
* @throws Exception Exception raised while changing state.
|
||||
*/
|
||||
Tunnel.State setState(Tunnel tunnel, Tunnel.State state, @Nullable Config config) throws Exception;
|
||||
}
|
||||
|
||||
@@ -7,24 +7,47 @@ package com.wireguard.android.backend;
|
||||
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
/**
|
||||
* A subclass of {@link Exception} that encapsulates the reasons for a failure originating in
|
||||
* implementations of {@link Backend}.
|
||||
*/
|
||||
@NonNullForAll
|
||||
public final class BackendException extends Exception {
|
||||
private final Object[] format;
|
||||
private final Reason reason;
|
||||
|
||||
/**
|
||||
* Public constructor for BackendException.
|
||||
*
|
||||
* @param reason The {@link Reason} which caused this exception to be thrown
|
||||
* @param format Format string values used when converting exceptions to user-facing strings.
|
||||
*/
|
||||
public BackendException(final Reason reason, final Object... format) {
|
||||
this.reason = reason;
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the format string values associated with the instance.
|
||||
*
|
||||
* @return Array of {@link Object} for string formatting purposes
|
||||
*/
|
||||
public Object[] getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reason for this exception.
|
||||
*
|
||||
* @return Associated {@link Reason} for this exception.
|
||||
*/
|
||||
public Reason getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum class containing all known reasons for why a {@link BackendException} might be thrown.
|
||||
*/
|
||||
public enum Reason {
|
||||
UNKNOWN_KERNEL_MODULE_NAME,
|
||||
WG_QUICK_CONFIG_ERROR_CODE,
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.system.OsConstants;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.backend.BackendException.Reason;
|
||||
@@ -33,6 +34,10 @@ import java.util.concurrent.TimeoutException;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.collection.ArraySet;
|
||||
|
||||
/**
|
||||
* Implementation of {@link Backend} that uses the wireguard-go userspace implementation to provide
|
||||
* WireGuard tunnels.
|
||||
*/
|
||||
@NonNullForAll
|
||||
public final class GoBackend implements Backend {
|
||||
private static final String TAG = "WireGuard/GoBackend";
|
||||
@@ -43,11 +48,22 @@ public final class GoBackend implements Backend {
|
||||
@Nullable private Tunnel currentTunnel;
|
||||
private int currentTunnelHandle = -1;
|
||||
|
||||
/**
|
||||
* Public constructor for GoBackend.
|
||||
*
|
||||
* @param context An Android {@link Context}
|
||||
*/
|
||||
public GoBackend(final Context context) {
|
||||
SharedLibraryLoader.loadSharedLibrary(context, "wg-go");
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a {@link AlwaysOnCallback} to be invoked when {@link VpnService} is started by the
|
||||
* system's Always-On VPN mode.
|
||||
*
|
||||
* @param cb Callback to be invoked
|
||||
*/
|
||||
public static void setAlwaysOnCallback(final AlwaysOnCallback cb) {
|
||||
alwaysOnCallback = cb;
|
||||
}
|
||||
@@ -64,6 +80,11 @@ public final class GoBackend implements Backend {
|
||||
|
||||
private static native String wgVersion();
|
||||
|
||||
/**
|
||||
* Method to get the names of running tunnels.
|
||||
*
|
||||
* @return A set of string values denoting names of running tunnels.
|
||||
*/
|
||||
@Override
|
||||
public Set<String> getRunningTunnelNames() {
|
||||
if (currentTunnel != null) {
|
||||
@@ -74,11 +95,23 @@ public final class GoBackend implements Backend {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the associated {@link State} for a given {@link Tunnel}.
|
||||
*
|
||||
* @param tunnel The tunnel to examine the state of.
|
||||
* @return {@link State} associated with the given tunnel.
|
||||
*/
|
||||
@Override
|
||||
public State getState(final Tunnel tunnel) {
|
||||
return currentTunnel == tunnel ? State.UP : State.DOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the associated {@link Statistics} for a given {@link Tunnel}.
|
||||
*
|
||||
* @param tunnel The tunnel to retrieve statistics for.
|
||||
* @return {@link Statistics} associated with the given tunnel.
|
||||
*/
|
||||
@Override
|
||||
public Statistics getStatistics(final Tunnel tunnel) {
|
||||
final Statistics stats = new Statistics();
|
||||
@@ -123,11 +156,26 @@ public final class GoBackend implements Backend {
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version of the underlying wireguard-go library.
|
||||
*
|
||||
* @return {@link String} value of the version of the wireguard-go library.
|
||||
*/
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return wgVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the state of a given {@link Tunnel}, optionally applying a given {@link Config}.
|
||||
*
|
||||
* @param tunnel The tunnel to control the state of.
|
||||
* @param state The new state for this tunnel. Must be {@code UP}, {@code DOWN}, or
|
||||
* {@code TOGGLE}.
|
||||
* @param config The configuration for this tunnel, may be null if state is {@code DOWN}.
|
||||
* @return {@link State} of the tunnel after state changes are applied.
|
||||
* @throws Exception Exception raised while changing tunnel state.
|
||||
*/
|
||||
@Override
|
||||
public State setState(final Tunnel tunnel, State state, @Nullable final Config config) throws Exception {
|
||||
final State originalState = getState(tunnel);
|
||||
@@ -193,15 +241,28 @@ public final class GoBackend implements Backend {
|
||||
for (final String excludedApplication : config.getInterface().getExcludedApplications())
|
||||
builder.addDisallowedApplication(excludedApplication);
|
||||
|
||||
for (final String includedApplication : config.getInterface().getIncludedApplications())
|
||||
builder.addAllowedApplication(includedApplication);
|
||||
|
||||
for (final InetNetwork addr : config.getInterface().getAddresses())
|
||||
builder.addAddress(addr.getAddress(), addr.getMask());
|
||||
|
||||
for (final InetAddress addr : config.getInterface().getDnsServers())
|
||||
builder.addDnsServer(addr.getHostAddress());
|
||||
|
||||
boolean sawDefaultRoute = false;
|
||||
for (final Peer peer : config.getPeers()) {
|
||||
for (final InetNetwork addr : peer.getAllowedIps())
|
||||
for (final InetNetwork addr : peer.getAllowedIps()) {
|
||||
if (addr.getMask() == 0)
|
||||
sawDefaultRoute = true;
|
||||
builder.addRoute(addr.getAddress(), addr.getMask());
|
||||
}
|
||||
}
|
||||
|
||||
// "Kill-switch" semantics
|
||||
if (!(sawDefaultRoute && config.getPeers().size() == 1)) {
|
||||
builder.allowFamily(OsConstants.AF_INET);
|
||||
builder.allowFamily(OsConstants.AF_INET6);
|
||||
}
|
||||
|
||||
builder.setMtu(config.getInterface().getMtu().orElse(1280));
|
||||
@@ -246,6 +307,10 @@ public final class GoBackend implements Backend {
|
||||
context.startService(new Intent(context, VpnService.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for {@link GoBackend} that is invoked when {@link VpnService} is started by the
|
||||
* system's Always-On VPN mode.
|
||||
*/
|
||||
public interface AlwaysOnCallback {
|
||||
void alwaysOnTriggered();
|
||||
}
|
||||
@@ -279,6 +344,9 @@ public final class GoBackend implements Backend {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link android.net.VpnService} implementation for {@link GoBackend}
|
||||
*/
|
||||
public static class VpnService extends android.net.VpnService {
|
||||
@Nullable private GoBackend owner;
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ import com.wireguard.util.NonNullForAll;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Class representing transfer statistics for a {@link Tunnel} instance.
|
||||
*/
|
||||
@NonNullForAll
|
||||
public class Statistics {
|
||||
private final Map<Key, Pair<Long, Long>> peerBytes = new HashMap<>();
|
||||
@@ -22,31 +25,70 @@ public class Statistics {
|
||||
Statistics() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a peer and its current data usage to the internal map.
|
||||
*
|
||||
* @param key A WireGuard public key bound to a particular peer
|
||||
* @param rx The received traffic for the {@link com.wireguard.config.Peer} referenced by
|
||||
* the provided {@link Key}. This value is in bytes
|
||||
* @param tx The transmitted traffic for the {@link com.wireguard.config.Peer} referenced by
|
||||
* the provided {@link Key}. This value is in bytes.
|
||||
*/
|
||||
void add(final Key key, final long rx, final long tx) {
|
||||
peerBytes.put(key, Pair.create(rx, tx));
|
||||
lastTouched = SystemClock.elapsedRealtime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the statistics are stale, indicating the need for the {@link Backend} to update them.
|
||||
*
|
||||
* @return boolean indicating if the current statistics instance has stale values.
|
||||
*/
|
||||
public boolean isStale() {
|
||||
return SystemClock.elapsedRealtime() - lastTouched > 900;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the received traffic (in bytes) for the {@link com.wireguard.config.Peer} referenced by
|
||||
* the provided {@link Key}
|
||||
*
|
||||
* @param peer A {@link Key} representing a {@link com.wireguard.config.Peer}.
|
||||
* @return a long representing the number of bytes received by this peer.
|
||||
*/
|
||||
public long peerRx(final Key peer) {
|
||||
if (!peerBytes.containsKey(peer))
|
||||
return 0;
|
||||
return peerBytes.get(peer).first;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the transmitted traffic (in bytes) for the {@link com.wireguard.config.Peer} referenced by
|
||||
* the provided {@link Key}
|
||||
*
|
||||
* @param peer A {@link Key} representing a {@link com.wireguard.config.Peer}.
|
||||
* @return a long representing the number of bytes transmitted by this peer.
|
||||
*/
|
||||
public long peerTx(final Key peer) {
|
||||
if (!peerBytes.containsKey(peer))
|
||||
return 0;
|
||||
return peerBytes.get(peer).second;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of peers being tracked by this instance.
|
||||
*
|
||||
* @return An array of {@link Key} instances representing WireGuard
|
||||
* {@link com.wireguard.config.Peer}s
|
||||
*/
|
||||
public Key[] peers() {
|
||||
return peerBytes.keySet().toArray(new Key[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total received traffic by all the peers being tracked by this instance
|
||||
*
|
||||
* @return a long representing the number of bytes received by the peers being tracked.
|
||||
*/
|
||||
public long totalRx() {
|
||||
long rx = 0;
|
||||
for (final Pair<Long, Long> val : peerBytes.values()) {
|
||||
@@ -55,6 +97,11 @@ public class Statistics {
|
||||
return rx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total transmitted traffic by all the peers being tracked by this instance
|
||||
*
|
||||
* @return a long representing the number of bytes transmitted by the peers being tracked.
|
||||
*/
|
||||
public long totalTx() {
|
||||
long tx = 0;
|
||||
for (final Pair<Long, Long> val : peerBytes.values()) {
|
||||
|
||||
@@ -36,11 +36,20 @@ public interface Tunnel {
|
||||
*/
|
||||
void onStateChange(State newState);
|
||||
|
||||
/**
|
||||
* Enum class to represent all possible states of a {@link Tunnel}.
|
||||
*/
|
||||
enum State {
|
||||
DOWN,
|
||||
TOGGLE,
|
||||
UP;
|
||||
|
||||
/**
|
||||
* Get the state of a {@link Tunnel}
|
||||
*
|
||||
* @param running boolean indicating if the tunnel is running.
|
||||
* @return State of the tunnel based on whether or not it is running.
|
||||
*/
|
||||
public static State of(final boolean running) {
|
||||
return running ? UP : DOWN;
|
||||
}
|
||||
|
||||
@@ -32,8 +32,6 @@ import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java9.util.stream.Collectors;
|
||||
import java9.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* WireGuard backend that uses {@code wg-quick} to implement tunnel configuration.
|
||||
@@ -67,7 +65,7 @@ public final class WgQuickBackend implements Backend {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
// wg puts all interface names on the same line. Split them into separate elements.
|
||||
return Stream.of(output.get(0).split(" ")).collect(Collectors.toUnmodifiableSet());
|
||||
return Set.of(output.get(0).split(" "));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -10,14 +10,9 @@ import android.system.OsConstants;
|
||||
import android.util.Base64;
|
||||
|
||||
import com.wireguard.android.util.RootShell.RootShellException;
|
||||
import com.wireguard.crypto.Ed25519;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import net.i2p.crypto.eddsa.EdDSAEngine;
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey;
|
||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
|
||||
import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec;
|
||||
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
@@ -28,7 +23,6 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidParameterException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.Signature;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
@@ -37,6 +31,10 @@ import java.util.Map;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Class that implements the logic for downloading and loading signed, prebuilt modules for
|
||||
* WireGuard into the running kernel.
|
||||
*/
|
||||
@NonNullForAll
|
||||
@SuppressWarnings("MagicNumber")
|
||||
public class ModuleLoader {
|
||||
@@ -49,6 +47,15 @@ public class ModuleLoader {
|
||||
private final File tmpDir;
|
||||
private final String userAgent;
|
||||
|
||||
/**
|
||||
* Public constructor for ModuleLoader
|
||||
*
|
||||
* @param context A {@link Context} instance.
|
||||
* @param rootShell A {@link RootShell} instance used to run elevated commands required for module
|
||||
* loading.
|
||||
* @param userAgent A {@link String} that represents the User-Agent string used for connections
|
||||
* to the upstream server.
|
||||
*/
|
||||
public ModuleLoader(final Context context, final RootShell rootShell, final String userAgent) {
|
||||
moduleDir = new File(context.getCacheDir(), "kmod");
|
||||
tmpDir = new File(context.getCacheDir(), "tmp");
|
||||
@@ -56,10 +63,23 @@ public class ModuleLoader {
|
||||
this.userAgent = userAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a WireGuard module is already loaded into the kernel.
|
||||
*
|
||||
* @return boolean indicating if WireGuard is already enabled in the kernel.
|
||||
*/
|
||||
public static boolean isModuleLoaded() {
|
||||
return new File("/sys/module/wireguard").exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the correct WireGuard module for the device
|
||||
*
|
||||
* @return {@link OsConstants}.EXIT_SUCCESS if everything succeeds, ENOENT otherwise.
|
||||
* @throws IOException if the remote hash list was not found or empty.
|
||||
* @throws RootShellException if {@link RootShell} has a failure executing elevated commands.
|
||||
* @throws NoSuchAlgorithmException if SHA256 algorithm is not available in device JDK.
|
||||
*/
|
||||
public Integer download() throws IOException, RootShellException, NoSuchAlgorithmException {
|
||||
final List<String> output = new ArrayList<>();
|
||||
rootShell.run(output, "sha256sum /proc/version|cut -d ' ' -f 1");
|
||||
@@ -119,17 +139,28 @@ public class ModuleLoader {
|
||||
return OsConstants.EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the downloaded module. ModuleLoader#download must be called before this.
|
||||
*
|
||||
* @throws IOException if {@link RootShell} has a failure executing elevated commands.
|
||||
* @throws RootShellException if {@link RootShell} has a failure executing elevated commands.
|
||||
*/
|
||||
public void loadModule() throws IOException, RootShellException {
|
||||
rootShell.run(null, String.format("insmod \"%s/wireguard-$(sha256sum /proc/version|cut -d ' ' -f 1).ko\"", moduleDir.getAbsolutePath()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the module might already exist in the app's data.
|
||||
*
|
||||
* @return boolean indicating whether downloadable module might exist already.
|
||||
*/
|
||||
public boolean moduleMightExist() {
|
||||
return moduleDir.exists() && moduleDir.isDirectory();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Map<String, Sha256Digest> verifySignedHashes(final String signifyDigest) {
|
||||
final byte[] publicKeyBytes = Base64.decode(MODULE_PUBLIC_KEY_BASE64, Base64.DEFAULT);
|
||||
byte[] publicKeyBytes = Base64.decode(MODULE_PUBLIC_KEY_BASE64, Base64.DEFAULT);
|
||||
|
||||
if (publicKeyBytes == null || publicKeyBytes.length != 32 + 10 || publicKeyBytes[0] != 'E' || publicKeyBytes[1] != 'd')
|
||||
return null;
|
||||
@@ -140,26 +171,17 @@ public class ModuleLoader {
|
||||
if (!lines[0].startsWith("untrusted comment: "))
|
||||
return null;
|
||||
|
||||
final byte[] signatureBytes = Base64.decode(lines[1], Base64.DEFAULT);
|
||||
byte[] signatureBytes = Base64.decode(lines[1], Base64.DEFAULT);
|
||||
if (signatureBytes == null || signatureBytes.length != 64 + 10)
|
||||
return null;
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
if (signatureBytes[i] != publicKeyBytes[i])
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final EdDSAParameterSpec parameterSpec = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519);
|
||||
final Signature signature = new EdDSAEngine(MessageDigest.getInstance(parameterSpec.getHashAlgorithm()));
|
||||
final byte[] rawPublicKeyBytes = new byte[32];
|
||||
System.arraycopy(publicKeyBytes, 10, rawPublicKeyBytes, 0, 32);
|
||||
signature.initVerify(new EdDSAPublicKey(new EdDSAPublicKeySpec(rawPublicKeyBytes, parameterSpec)));
|
||||
signature.update(lines[2].getBytes(StandardCharsets.UTF_8));
|
||||
if (!signature.verify(signatureBytes, 10, 64))
|
||||
return null;
|
||||
} catch (final Exception ignored) {
|
||||
publicKeyBytes = Arrays.copyOfRange(publicKeyBytes, 10, 10 + 32);
|
||||
signatureBytes = Arrays.copyOfRange(signatureBytes, 10, 10 + 64);
|
||||
if (!Ed25519.verify(lines[2].getBytes(StandardCharsets.UTF_8), signatureBytes, publicKeyBytes))
|
||||
return null;
|
||||
}
|
||||
|
||||
final Map<String, Sha256Digest> hashes = new HashMap<>();
|
||||
for (final String line : lines[2].split("\n")) {
|
||||
|
||||
@@ -143,12 +143,12 @@ public final class ToolsInstaller {
|
||||
extract();
|
||||
final StringBuilder script = new StringBuilder("set -ex; ");
|
||||
|
||||
script.append("trap 'rm -rf /sbin/.magisk/img/wireguard' INT TERM EXIT; ");
|
||||
script.append(String.format("rm -rf /sbin/.magisk/img/wireguard/; mkdir -p /sbin/.magisk/img/wireguard%s; ", INSTALL_DIR));
|
||||
script.append("printf 'name=WireGuard Command Line Tools\nversion=1.0\nversionCode=1\nauthor=zx2c4\ndescription=Command line tools for WireGuard\nminMagisk=1500\n' > /sbin/.magisk/img/wireguard/module.prop; ");
|
||||
script.append("touch /sbin/.magisk/img/wireguard/auto_mount; ");
|
||||
script.append("trap 'rm -rf /data/adb/moduleswireguard' INT TERM EXIT; ");
|
||||
script.append(String.format("rm -rf /data/adb/modules/wireguard/; mkdir -p /data/adb/modules/wireguard%s; ", INSTALL_DIR));
|
||||
script.append("printf 'name=WireGuard Command Line Tools\nversion=1.0\nversionCode=1\nauthor=zx2c4\ndescription=Command line tools for WireGuard\nminMagisk=1500\n' > /data/adb/modules/wireguard/module.prop; ");
|
||||
script.append("touch /data/adb/modules/wireguard/auto_mount; ");
|
||||
for (final String name : EXECUTABLES) {
|
||||
final File destination = new File("/sbin/.magisk/img/wireguard" + INSTALL_DIR, name);
|
||||
final File destination = new File("/data/adb/modules/wireguard" + INSTALL_DIR, name);
|
||||
script.append(String.format("cp '%s' '%s'; chmod 755 '%s'; chcon 'u:object_r:system_file:s0' '%s' || true; ",
|
||||
new File(localBinaryDir, name), destination, destination, destination));
|
||||
}
|
||||
@@ -191,7 +191,7 @@ public final class ToolsInstaller {
|
||||
synchronized (lock) {
|
||||
if (installAsMagiskModule == null) {
|
||||
try {
|
||||
installAsMagiskModule = rootShell.run(null, "[ -d /sbin/.magisk/mirror -a -d /sbin/.magisk/img -a ! -f /cache/.disable_magisk ]") == OsConstants.EXIT_SUCCESS;
|
||||
installAsMagiskModule = rootShell.run(null, "[ -d /data/adb/modules -a ! -f /cache/.disable_magisk ]") == OsConstants.EXIT_SUCCESS;
|
||||
} catch (final Exception ignored) {
|
||||
installAsMagiskModule = false;
|
||||
}
|
||||
|
||||
@@ -8,11 +8,10 @@ package com.wireguard.config;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import java9.util.Optional;
|
||||
|
||||
@NonNullForAll
|
||||
public final class Attribute {
|
||||
private static final Pattern LINE_PATTERN = Pattern.compile("(\\w+)\\s*=\\s*([^\\s#][^#]*)");
|
||||
|
||||
@@ -72,6 +72,7 @@ public class BadConfigException extends Exception {
|
||||
DNS("DNS"),
|
||||
ENDPOINT("Endpoint"),
|
||||
EXCLUDED_APPLICATIONS("ExcludedApplications"),
|
||||
INCLUDED_APPLICATIONS("IncludedApplications"),
|
||||
LISTEN_PORT("ListenPort"),
|
||||
MTU("MTU"),
|
||||
PERSISTENT_KEEPALIVE("PersistentKeepalive"),
|
||||
|
||||
@@ -17,10 +17,8 @@ import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -187,7 +185,7 @@ public final class Config {
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
public static final class Builder {
|
||||
// Defaults to an empty set.
|
||||
private final Set<Peer> peers = new LinkedHashSet<>();
|
||||
private final ArrayList<Peer> peers = new ArrayList<>();
|
||||
// No default; must be provided before building.
|
||||
@Nullable private Interface interfaze;
|
||||
|
||||
|
||||
@@ -7,18 +7,17 @@ package com.wireguard.config;
|
||||
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import org.threeten.bp.Duration;
|
||||
import org.threeten.bp.Instant;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java9.util.Optional;
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,13 +20,11 @@ import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java9.util.Lists;
|
||||
import java9.util.Optional;
|
||||
import java9.util.stream.Collectors;
|
||||
import java9.util.stream.StreamSupport;
|
||||
|
||||
/**
|
||||
* Represents the configuration for a WireGuard interface (an [Interface] block). Interfaces must
|
||||
@@ -43,6 +41,7 @@ public final class Interface {
|
||||
private final Set<InetNetwork> addresses;
|
||||
private final Set<InetAddress> dnsServers;
|
||||
private final Set<String> excludedApplications;
|
||||
private final Set<String> includedApplications;
|
||||
private final KeyPair keyPair;
|
||||
private final Optional<Integer> listenPort;
|
||||
private final Optional<Integer> mtu;
|
||||
@@ -52,6 +51,7 @@ public final class Interface {
|
||||
addresses = Collections.unmodifiableSet(new LinkedHashSet<>(builder.addresses));
|
||||
dnsServers = Collections.unmodifiableSet(new LinkedHashSet<>(builder.dnsServers));
|
||||
excludedApplications = Collections.unmodifiableSet(new LinkedHashSet<>(builder.excludedApplications));
|
||||
includedApplications = Collections.unmodifiableSet(new LinkedHashSet<>(builder.includedApplications));
|
||||
keyPair = Objects.requireNonNull(builder.keyPair, "Interfaces must have a private key");
|
||||
listenPort = builder.listenPort;
|
||||
mtu = builder.mtu;
|
||||
@@ -81,6 +81,9 @@ public final class Interface {
|
||||
case "excludedapplications":
|
||||
builder.parseExcludedApplications(attribute.getValue());
|
||||
break;
|
||||
case "includedapplications":
|
||||
builder.parseIncludedApplications(attribute.getValue());
|
||||
break;
|
||||
case "listenport":
|
||||
builder.parseListenPort(attribute.getValue());
|
||||
break;
|
||||
@@ -106,6 +109,7 @@ public final class Interface {
|
||||
return addresses.equals(other.addresses)
|
||||
&& dnsServers.equals(other.dnsServers)
|
||||
&& excludedApplications.equals(other.excludedApplications)
|
||||
&& includedApplications.equals(other.includedApplications)
|
||||
&& keyPair.equals(other.keyPair)
|
||||
&& listenPort.equals(other.listenPort)
|
||||
&& mtu.equals(other.mtu);
|
||||
@@ -141,6 +145,16 @@ public final class Interface {
|
||||
return excludedApplications;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set of applications included exclusively for using the interface.
|
||||
*
|
||||
* @return a set of package names
|
||||
*/
|
||||
public Set<String> getIncludedApplications() {
|
||||
// The collection is already immutable.
|
||||
return includedApplications;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the public/private key pair used by the interface.
|
||||
*
|
||||
@@ -174,6 +188,7 @@ public final class Interface {
|
||||
hash = 31 * hash + addresses.hashCode();
|
||||
hash = 31 * hash + dnsServers.hashCode();
|
||||
hash = 31 * hash + excludedApplications.hashCode();
|
||||
hash = 31 * hash + includedApplications.hashCode();
|
||||
hash = 31 * hash + keyPair.hashCode();
|
||||
hash = 31 * hash + listenPort.hashCode();
|
||||
hash = 31 * hash + mtu.hashCode();
|
||||
@@ -206,13 +221,13 @@ public final class Interface {
|
||||
if (!addresses.isEmpty())
|
||||
sb.append("Address = ").append(Attribute.join(addresses)).append('\n');
|
||||
if (!dnsServers.isEmpty()) {
|
||||
final List<String> dnsServerStrings = StreamSupport.stream(dnsServers)
|
||||
.map(InetAddress::getHostAddress)
|
||||
.collect(Collectors.toUnmodifiableList());
|
||||
final List<String> dnsServerStrings = dnsServers.stream().map(InetAddress::getHostAddress).collect(Collectors.toList());
|
||||
sb.append("DNS = ").append(Attribute.join(dnsServerStrings)).append('\n');
|
||||
}
|
||||
if (!excludedApplications.isEmpty())
|
||||
sb.append("ExcludedApplications = ").append(Attribute.join(excludedApplications)).append('\n');
|
||||
if (!includedApplications.isEmpty())
|
||||
sb.append("IncludedApplications = ").append(Attribute.join(includedApplications)).append('\n');
|
||||
listenPort.ifPresent(lp -> sb.append("ListenPort = ").append(lp).append('\n'));
|
||||
mtu.ifPresent(m -> sb.append("MTU = ").append(m).append('\n'));
|
||||
sb.append("PrivateKey = ").append(keyPair.getPrivateKey().toBase64()).append('\n');
|
||||
@@ -240,6 +255,8 @@ public final class Interface {
|
||||
private final Set<InetAddress> dnsServers = new LinkedHashSet<>();
|
||||
// Defaults to an empty set.
|
||||
private final Set<String> excludedApplications = new LinkedHashSet<>();
|
||||
// Defaults to an empty set.
|
||||
private final Set<String> includedApplications = new LinkedHashSet<>();
|
||||
// No default; must be provided before building.
|
||||
@Nullable private KeyPair keyPair;
|
||||
// Defaults to not present.
|
||||
@@ -271,6 +288,9 @@ public final class Interface {
|
||||
if (keyPair == null)
|
||||
throw new BadConfigException(Section.INTERFACE, Location.PRIVATE_KEY,
|
||||
Reason.MISSING_ATTRIBUTE, null);
|
||||
if (!includedApplications.isEmpty() && !excludedApplications.isEmpty())
|
||||
throw new BadConfigException(Section.INTERFACE, Location.INCLUDED_APPLICATIONS,
|
||||
Reason.INVALID_KEY, null);
|
||||
return new Interface(this);
|
||||
}
|
||||
|
||||
@@ -284,6 +304,16 @@ public final class Interface {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder includeApplication(final String application) {
|
||||
includedApplications.add(application);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder includeApplications(final Collection<String> applications) {
|
||||
includedApplications.addAll(applications);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder parseAddresses(final CharSequence addresses) throws BadConfigException {
|
||||
try {
|
||||
for (final String address : Attribute.split(addresses))
|
||||
@@ -305,7 +335,11 @@ public final class Interface {
|
||||
}
|
||||
|
||||
public Builder parseExcludedApplications(final CharSequence apps) {
|
||||
return excludeApplications(Lists.of(Attribute.split(apps)));
|
||||
return excludeApplications(List.of(Attribute.split(apps)));
|
||||
}
|
||||
|
||||
public Builder parseIncludedApplications(final CharSequence apps) {
|
||||
return includeApplications(List.of(Attribute.split(apps)));
|
||||
}
|
||||
|
||||
public Builder parseListenPort(final String listenPort) throws BadConfigException {
|
||||
|
||||
@@ -17,10 +17,10 @@ import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java9.util.Optional;
|
||||
|
||||
/**
|
||||
* Represents the configuration for a WireGuard peer (a [Peer] block). Peers must have a public key,
|
||||
|
||||
@@ -28,7 +28,7 @@ import androidx.annotation.Nullable;
|
||||
*/
|
||||
@SuppressWarnings({"MagicNumber", "NonConstantFieldWithUpperCaseName", "SuspiciousNameCombination"})
|
||||
@NonNullForAll
|
||||
final class Curve25519 {
|
||||
public final class Curve25519 {
|
||||
// Numbers modulo 2^255 - 19 are broken up into ten 26-bit words.
|
||||
private static final int NUM_LIMBS_255BIT = 10;
|
||||
private static final int NUM_LIMBS_510BIT = 20;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@ target_link_libraries(libwg-quick.so -ldl)
|
||||
|
||||
file(GLOB WG_SOURCES wireguard-tools/src/*.c ndk-compat/compat.c)
|
||||
add_executable(libwg.so ${WG_SOURCES})
|
||||
target_include_directories(libwg.so PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/uapi/" "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/")
|
||||
target_include_directories(libwg.so PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/uapi/linux/" "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/")
|
||||
target_compile_options(libwg.so PUBLIC -O3 -std=gnu11 -D_GNU_SOURCE -include ${CMAKE_CURRENT_SOURCE_DIR}/ndk-compat/compat.h -DHAVE_VISIBILITY_HIDDEN -DRUNSTATEDIR=\"/data/data/${ANDROID_PACKAGE_NAME}/cache\")
|
||||
|
||||
add_custom_target(libwg-go.so WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/libwg-go" COMMENT "Building wireguard-go" VERBATIM COMMAND make
|
||||
|
||||
@@ -20,11 +20,11 @@ export GOARCH := $(NDK_GO_ARCH_MAP_$(ANDROID_ARCH_NAME))
|
||||
export GOOS := android
|
||||
export CGO_ENABLED := 1
|
||||
|
||||
GO_VERSION := 1.14.1
|
||||
GO_VERSION := 1.15.2
|
||||
GO_PLATFORM := $(shell uname -s | tr '[:upper:]' '[:lower:]')-$(NDK_GO_ARCH_MAP_$(shell uname -m))
|
||||
GO_TARBALL := go$(GO_VERSION).$(GO_PLATFORM).tar.gz
|
||||
GO_HASH_darwin-amd64 := 6632f9d53fd95632e431e8c34295349cca3f0a124e3a28b760ae5c42b32816e3
|
||||
GO_HASH_linux-amd64 := 2f49eb17ce8b48c680cdb166ffd7389702c0dec6effa090c324804a5cac8a7f8
|
||||
GO_HASH_darwin-amd64 := 9bd39600d9fa1fa4a5ccce8761d249f7421cffe671376f791293c4138f3d7c62
|
||||
GO_HASH_linux-amd64 := b49fda1ca29a1946d6bb2a5a6982cf07ccd2aba849289508ee0f9918f6bb4552
|
||||
|
||||
default: $(DESTDIR)/libwg-go.so
|
||||
|
||||
|
||||
@@ -11,10 +11,6 @@ import "C"
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"golang.org/x/sys/unix"
|
||||
"golang.zx2c4.com/wireguard/device"
|
||||
"golang.zx2c4.com/wireguard/ipc"
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
"bytes"
|
||||
"log"
|
||||
"math"
|
||||
@@ -24,6 +20,12 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"golang.zx2c4.com/wireguard/conn"
|
||||
"golang.zx2c4.com/wireguard/device"
|
||||
"golang.zx2c4.com/wireguard/ipc"
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
)
|
||||
|
||||
type AndroidLogger struct {
|
||||
@@ -149,7 +151,11 @@ func wgGetSocketV4(tunnelHandle int32) int32 {
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
fd, err := handle.device.PeekLookAtSocketFd4()
|
||||
bind, _ := handle.device.Bind().(conn.PeekLookAtSocketFd)
|
||||
if bind == nil {
|
||||
return -1
|
||||
}
|
||||
fd, err := bind.PeekLookAtSocketFd4()
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
@@ -162,7 +168,11 @@ func wgGetSocketV6(tunnelHandle int32) int32 {
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
fd, err := handle.device.PeekLookAtSocketFd6()
|
||||
bind, _ := handle.device.Bind().(conn.PeekLookAtSocketFd)
|
||||
if bind == nil {
|
||||
return -1
|
||||
}
|
||||
fd, err := bind.PeekLookAtSocketFd6()
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
module golang.zx2c4.com/wireguard/android
|
||||
|
||||
go 1.13
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
golang.org/x/crypto v0.0.0-20200320181102-891825fb96df // indirect
|
||||
golang.org/x/net v0.0.0-20200320220750-118fecf932d8 // indirect
|
||||
golang.org/x/sys v0.0.0-20200321134203-328b4cd54aae
|
||||
golang.zx2c4.com/wireguard v0.0.20200320
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 // indirect
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 // indirect
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980
|
||||
golang.zx2c4.com/wireguard v0.0.20200321-0.20200622004228-b84f1d4db25e
|
||||
)
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200320181102-891825fb96df h1:lDWgvUvNnaTnNBc/dwOty86cFeKoKWbwy2wQj0gIxbU=
|
||||
golang.org/x/crypto v0.0.0-20200320181102-891825fb96df/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200320220750-118fecf932d8 h1:1+zQlQqEEhUeStBTi653GZAnAuivZq/2hz+Iz+OP7rg=
|
||||
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200321134203-328b4cd54aae h1:3tcmuaB7wwSZtelmiv479UjUB+vviwABz7a133ZwOKQ=
|
||||
golang.org/x/sys v0.0.0-20200321134203-328b4cd54aae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.zx2c4.com/wireguard v0.0.20200320 h1:1vE6zVeO7fix9cJX1Z9ZQ+ikPIIx7vIyU0o0tLDD88g=
|
||||
golang.zx2c4.com/wireguard v0.0.20200320/go.mod h1:lDian4Sw4poJ04SgHh35nzMVwGSYlPumkdnHcucAQoY=
|
||||
golang.zx2c4.com/wireguard v0.0.20200321-0.20200622004228-b84f1d4db25e h1:f8BS3yEMeIGx/zzJfihxDRedx6lT7EiJlfih4j6LY98=
|
||||
golang.zx2c4.com/wireguard v0.0.20200321-0.20200622004228-b84f1d4db25e/go.mod h1:GJvYs5O24/ASlwPiRklVnjMx2xQzrOic0DuU6GvYJL4=
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
From e44f456f1d0e429e08afed64a161175ff493f3ac Mon Sep 17 00:00:00 2001
|
||||
From 1d1ba1da11afd73008c0e942db7621697055a6b6 Mon Sep 17 00:00:00 2001
|
||||
From: "Jason A. Donenfeld" <Jason@zx2c4.com>
|
||||
Date: Wed, 27 Feb 2019 05:05:44 +0100
|
||||
Date: Tue, 15 Sep 2020 13:39:22 +0200
|
||||
Subject: [PATCH] runtime: use CLOCK_BOOTTIME in nanotime on Linux
|
||||
|
||||
This makes timers account for having expired while a computer was
|
||||
@@ -28,10 +28,10 @@ Change-Id: I7b2a6ca0c5bc5fce57ec0eeafe7b68270b429321
|
||||
8 files changed, 11 insertions(+), 11 deletions(-)
|
||||
|
||||
diff --git a/src/runtime/sys_linux_386.s b/src/runtime/sys_linux_386.s
|
||||
index 1b28098ad9..46b7071ed8 100644
|
||||
index 5b9b638ad7..448ad8b2e6 100644
|
||||
--- a/src/runtime/sys_linux_386.s
|
||||
+++ b/src/runtime/sys_linux_386.s
|
||||
@@ -317,13 +317,13 @@ noswitch:
|
||||
@@ -339,13 +339,13 @@ noswitch:
|
||||
|
||||
LEAL 8(SP), BX // &ts (struct timespec)
|
||||
MOVL BX, 4(SP)
|
||||
@@ -48,10 +48,10 @@ index 1b28098ad9..46b7071ed8 100644
|
||||
INVOKE_SYSCALL
|
||||
|
||||
diff --git a/src/runtime/sys_linux_amd64.s b/src/runtime/sys_linux_amd64.s
|
||||
index 58d3bc54b4..4bb9bde3d0 100644
|
||||
index fe9c6bce85..4836a7c774 100644
|
||||
--- a/src/runtime/sys_linux_amd64.s
|
||||
+++ b/src/runtime/sys_linux_amd64.s
|
||||
@@ -293,7 +293,7 @@ noswitch:
|
||||
@@ -311,7 +311,7 @@ noswitch:
|
||||
MOVQ runtime·vdsoClockgettimeSym(SB), AX
|
||||
CMPQ AX, $0
|
||||
JEQ fallback
|
||||
@@ -61,7 +61,7 @@ index 58d3bc54b4..4bb9bde3d0 100644
|
||||
CALL AX
|
||||
MOVQ 0(SP), AX // sec
|
||||
diff --git a/src/runtime/sys_linux_arm.s b/src/runtime/sys_linux_arm.s
|
||||
index e103da56dc..0b872b90a6 100644
|
||||
index 475f52344c..bb567abcf4 100644
|
||||
--- a/src/runtime/sys_linux_arm.s
|
||||
+++ b/src/runtime/sys_linux_arm.s
|
||||
@@ -11,7 +11,7 @@
|
||||
@@ -73,7 +73,7 @@ index e103da56dc..0b872b90a6 100644
|
||||
|
||||
// for EABI, as we don't support OABI
|
||||
#define SYS_BASE 0x0
|
||||
@@ -345,7 +345,7 @@ noswitch:
|
||||
@@ -366,7 +366,7 @@ noswitch:
|
||||
SUB $24, R13 // Space for results
|
||||
BIC $0x7, R13 // Align for C code
|
||||
|
||||
@@ -83,7 +83,7 @@ index e103da56dc..0b872b90a6 100644
|
||||
MOVW runtime·vdsoClockgettimeSym(SB), R2
|
||||
CMP $0, R2
|
||||
diff --git a/src/runtime/sys_linux_arm64.s b/src/runtime/sys_linux_arm64.s
|
||||
index b9588cec30..e444d50df4 100644
|
||||
index 198a5bacef..9715387f36 100644
|
||||
--- a/src/runtime/sys_linux_arm64.s
|
||||
+++ b/src/runtime/sys_linux_arm64.s
|
||||
@@ -13,7 +13,7 @@
|
||||
@@ -95,7 +95,7 @@ index b9588cec30..e444d50df4 100644
|
||||
|
||||
#define SYS_exit 93
|
||||
#define SYS_read 63
|
||||
@@ -297,7 +297,7 @@ noswitch:
|
||||
@@ -319,7 +319,7 @@ noswitch:
|
||||
BIC $15, R1
|
||||
MOVD R1, RSP
|
||||
|
||||
@@ -105,10 +105,10 @@ index b9588cec30..e444d50df4 100644
|
||||
CBZ R2, fallback
|
||||
|
||||
diff --git a/src/runtime/sys_linux_mips64x.s b/src/runtime/sys_linux_mips64x.s
|
||||
index 723cfe43d9..edd7a195eb 100644
|
||||
index afad056d06..2c9162b903 100644
|
||||
--- a/src/runtime/sys_linux_mips64x.s
|
||||
+++ b/src/runtime/sys_linux_mips64x.s
|
||||
@@ -278,7 +278,7 @@ noswitch:
|
||||
@@ -304,7 +304,7 @@ noswitch:
|
||||
AND $~15, R1 // Align for C code
|
||||
MOVV R1, R29
|
||||
|
||||
@@ -118,10 +118,10 @@ index 723cfe43d9..edd7a195eb 100644
|
||||
|
||||
MOVV runtime·vdsoClockgettimeSym(SB), R25
|
||||
diff --git a/src/runtime/sys_linux_mipsx.s b/src/runtime/sys_linux_mipsx.s
|
||||
index 15893a7a28..f3edf9a83a 100644
|
||||
index fab2ab3892..f9af103594 100644
|
||||
--- a/src/runtime/sys_linux_mipsx.s
|
||||
+++ b/src/runtime/sys_linux_mipsx.s
|
||||
@@ -235,7 +235,7 @@ TEXT runtime·walltime1(SB),NOSPLIT,$8-12
|
||||
@@ -238,7 +238,7 @@ TEXT runtime·walltime1(SB),NOSPLIT,$8-12
|
||||
RET
|
||||
|
||||
TEXT runtime·nanotime1(SB),NOSPLIT,$8-8
|
||||
@@ -131,13 +131,13 @@ index 15893a7a28..f3edf9a83a 100644
|
||||
MOVW $SYS_clock_gettime, R2
|
||||
SYSCALL
|
||||
diff --git a/src/runtime/sys_linux_ppc64x.s b/src/runtime/sys_linux_ppc64x.s
|
||||
index 8629fe3233..2402e2623a 100644
|
||||
index fd69ee70a5..ff6bc8355b 100644
|
||||
--- a/src/runtime/sys_linux_ppc64x.s
|
||||
+++ b/src/runtime/sys_linux_ppc64x.s
|
||||
@@ -233,7 +233,7 @@ fallback:
|
||||
@@ -249,7 +249,7 @@ fallback:
|
||||
JMP finish
|
||||
|
||||
TEXT runtime·nanotime1(SB),NOSPLIT,$16
|
||||
TEXT runtime·nanotime1(SB),NOSPLIT,$16-8
|
||||
- MOVD $1, R3 // CLOCK_MONOTONIC
|
||||
+ MOVD $7, R3 // CLOCK_BOOTTIME
|
||||
|
||||
@@ -157,5 +157,5 @@ index c15a1d5364..f52c4d5098 100644
|
||||
MOVW $SYS_clock_gettime, R1
|
||||
SYSCALL
|
||||
--
|
||||
2.25.1
|
||||
2.28.0
|
||||
|
||||
|
||||
Submodule tunnel/tools/wireguard-tools updated: a8063adc8a...265e81a344
+22
-24
@@ -10,33 +10,24 @@ group groupName
|
||||
final def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
|
||||
android {
|
||||
buildToolsVersion '29.0.3'
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
}
|
||||
compileSdkVersion 29
|
||||
dataBinding.enabled true
|
||||
viewBinding.enabled true
|
||||
compileSdkVersion 30
|
||||
buildFeatures.dataBinding = true
|
||||
buildFeatures.viewBinding = true
|
||||
defaultConfig {
|
||||
applicationId 'com.wireguard.android'
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
targetSdkVersion 30
|
||||
versionCode wireguardVersionCode
|
||||
versionName wireguardVersionName
|
||||
buildConfigField 'int', 'MIN_SDK_VERSION', "$minSdkVersion.apiLevel"
|
||||
}
|
||||
// If the keystore file exists
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
coreLibraryDesugaringEnabled = true
|
||||
}
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
// Initialize a new Properties() object called keystoreProperties.
|
||||
final def keystoreProperties = new Properties()
|
||||
|
||||
// Load your keystore.properties file into the keystoreProperties object.
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
|
||||
signingConfigs {
|
||||
@@ -66,25 +57,32 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation project(":tunnel")
|
||||
implementation "androidx.activity:activity-ktx:$activityVersion"
|
||||
implementation "androidx.annotation:annotation:$annotationsVersion"
|
||||
implementation "androidx.appcompat:appcompat:$appcompatVersion"
|
||||
implementation "androidx.cardview:cardview:$cardviewVersion"
|
||||
implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion"
|
||||
implementation "androidx.coordinatorlayout:coordinatorlayout:$coordinatorLayoutVersion"
|
||||
implementation "androidx.biometric:biometric:$biometricVersion"
|
||||
implementation "androidx.core:core-ktx:$coreKtxVersion"
|
||||
implementation "androidx.databinding:databinding-runtime:$agpVersion"
|
||||
implementation "androidx.fragment:fragment:$fragmentVersion"
|
||||
implementation "androidx.preference:preference:$preferenceVersion"
|
||||
implementation "com.google.android.material:material:$materialComponentsVersion"
|
||||
implementation "androidx.fragment:fragment-ktx:$fragmentVersion"
|
||||
implementation "androidx.preference:preference-ktx:$preferenceVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleRuntimeKtxVersion"
|
||||
implementation "androidx.datastore:datastore-preferences:$datastoreVersion"
|
||||
implementation "com.github.material-components:material-components-android:$materialComponentsVersion"
|
||||
implementation "com.journeyapps:zxing-android-embedded:$zxingEmbeddedVersion"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
||||
implementation "net.sourceforge.streamsupport:android-retrofuture:$streamsupportVersion"
|
||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$desugarVersion"
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.compilerArgs << '-Xlint:unchecked'
|
||||
options.deprecation = true
|
||||
}
|
||||
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+3
-3
@@ -2,9 +2,6 @@
|
||||
# escalates them as errors.
|
||||
-dontwarn sun.misc.Unsafe
|
||||
|
||||
# Retain some information to keep stacktraces usable
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# Fragment 1.2.4 allows Fragment classes to be obfuscated but
|
||||
# databinding references in XML seem to not be rewritten to
|
||||
# match, so we preserve the names as 1.2.3 did.
|
||||
@@ -12,3 +9,6 @@
|
||||
-keep public class <1> {
|
||||
public <init>();
|
||||
}
|
||||
|
||||
# Don't obfuscate
|
||||
-dontobfuscate
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
@@ -112,5 +115,16 @@
|
||||
android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||
android:value="false" />
|
||||
</service>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.APP_RESTRICTIONS"
|
||||
android:resource="@xml/app_restrictions" />
|
||||
</application>
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
|
||||
@@ -6,38 +6,44 @@ package com.wireguard.android
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.os.AsyncTask
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.StrictMode
|
||||
import android.os.StrictMode.ThreadPolicy
|
||||
import android.os.StrictMode.VmPolicy
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.datastore.DataStore
|
||||
import androidx.datastore.preferences.Preferences
|
||||
import androidx.datastore.preferences.createDataStore
|
||||
import com.wireguard.android.backend.Backend
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.wireguard.android.configStore.FileConfigStore
|
||||
import com.wireguard.android.model.TunnelManager
|
||||
import com.wireguard.android.util.AsyncWorker
|
||||
import com.wireguard.android.util.ExceptionLoggers
|
||||
import com.wireguard.android.util.ModuleLoader
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.wireguard.android.util.ToolsInstaller
|
||||
import java9.util.concurrent.CompletableFuture
|
||||
import com.wireguard.android.util.UserKnobs
|
||||
import com.wireguard.android.util.applicationScope
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.Locale
|
||||
|
||||
class Application : android.app.Application(), OnSharedPreferenceChangeListener {
|
||||
private val futureBackend = CompletableFuture<Backend>()
|
||||
private lateinit var asyncWorker: AsyncWorker
|
||||
class Application : android.app.Application() {
|
||||
private val futureBackend = CompletableDeferred<Backend>()
|
||||
private val coroutineScope = CoroutineScope(Job() + Dispatchers.Main.immediate)
|
||||
private var backend: Backend? = null
|
||||
private lateinit var moduleLoader: ModuleLoader
|
||||
private lateinit var rootShell: RootShell
|
||||
private lateinit var sharedPreferences: SharedPreferences
|
||||
private lateinit var preferencesDataStore: DataStore<Preferences>
|
||||
private lateinit var toolsInstaller: ToolsInstaller
|
||||
private lateinit var tunnelManager: TunnelManager
|
||||
|
||||
@@ -53,36 +59,70 @@ class Application : android.app.Application(), OnSharedPreferenceChangeListener
|
||||
}
|
||||
if (BuildConfig.DEBUG) {
|
||||
StrictMode.setVmPolicy(VmPolicy.Builder().detectAll().penaltyLog().build())
|
||||
StrictMode.setThreadPolicy(ThreadPolicy.Builder().detectAll().penaltyLog().build())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun determineBackend(): Backend {
|
||||
var backend: Backend? = null
|
||||
var didStartRootShell = false
|
||||
if (!ModuleLoader.isModuleLoaded() && moduleLoader.moduleMightExist()) {
|
||||
try {
|
||||
rootShell.start()
|
||||
didStartRootShell = true
|
||||
moduleLoader.loadModule()
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
if (!UserKnobs.disableKernelModule.first() && ModuleLoader.isModuleLoaded()) {
|
||||
try {
|
||||
if (!didStartRootShell)
|
||||
rootShell.start()
|
||||
val wgQuickBackend = WgQuickBackend(applicationContext, rootShell, toolsInstaller)
|
||||
wgQuickBackend.setMultipleTunnels(UserKnobs.multipleTunnels.first())
|
||||
backend = wgQuickBackend
|
||||
UserKnobs.multipleTunnels.onEach {
|
||||
wgQuickBackend.setMultipleTunnels(it)
|
||||
}.launchIn(coroutineScope)
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
if (backend == null) {
|
||||
backend = GoBackend(applicationContext)
|
||||
GoBackend.setAlwaysOnCallback { get().applicationScope.launch { get().tunnelManager.restoreState(true) } }
|
||||
}
|
||||
return backend
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
Log.i(TAG, USER_AGENT)
|
||||
super.onCreate()
|
||||
asyncWorker = AsyncWorker(AsyncTask.SERIAL_EXECUTOR, Handler(Looper.getMainLooper()))
|
||||
rootShell = RootShell(applicationContext)
|
||||
toolsInstaller = ToolsInstaller(applicationContext, rootShell)
|
||||
moduleLoader = ModuleLoader(applicationContext, rootShell, USER_AGENT)
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||
preferencesDataStore = applicationContext.createDataStore(name = "settings")
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
if (sharedPreferences.getBoolean("dark_theme", false)) AppCompatDelegate.MODE_NIGHT_YES else AppCompatDelegate.MODE_NIGHT_NO)
|
||||
coroutineScope.launch {
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
if (UserKnobs.darkTheme.first()) AppCompatDelegate.MODE_NIGHT_YES else AppCompatDelegate.MODE_NIGHT_NO)
|
||||
}
|
||||
} else {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
}
|
||||
tunnelManager = TunnelManager(FileConfigStore(applicationContext))
|
||||
tunnelManager.onCreate()
|
||||
asyncWorker.supplyAsync(Companion::getBackend).thenAccept { futureBackend.complete(it) }
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
if ("multiple_tunnels" == key && backend != null && backend is WgQuickBackend)
|
||||
(backend as WgQuickBackend).setMultipleTunnels(sharedPreferences.getBoolean(key, false))
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
backend = determineBackend()
|
||||
futureBackend.complete(backend!!)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
|
||||
coroutineScope.cancel()
|
||||
super.onTerminate()
|
||||
}
|
||||
|
||||
@@ -97,45 +137,7 @@ class Application : android.app.Application(), OnSharedPreferenceChangeListener
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getAsyncWorker() = get().asyncWorker
|
||||
|
||||
@JvmStatic
|
||||
fun getBackend(): Backend {
|
||||
val app = get()
|
||||
synchronized(app.futureBackend) {
|
||||
if (app.backend == null) {
|
||||
var backend: Backend? = null
|
||||
var didStartRootShell = false
|
||||
if (!ModuleLoader.isModuleLoaded() && app.moduleLoader.moduleMightExist()) {
|
||||
try {
|
||||
app.rootShell.start()
|
||||
didStartRootShell = true
|
||||
app.moduleLoader.loadModule()
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
if (!app.sharedPreferences.getBoolean("disable_kernel_module", false) && ModuleLoader.isModuleLoaded()) {
|
||||
try {
|
||||
if (!didStartRootShell)
|
||||
app.rootShell.start()
|
||||
val wgQuickBackend = WgQuickBackend(app.applicationContext, app.rootShell, app.toolsInstaller)
|
||||
wgQuickBackend.setMultipleTunnels(app.sharedPreferences.getBoolean("multiple_tunnels", false))
|
||||
backend = wgQuickBackend
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
if (backend == null) {
|
||||
backend = GoBackend(app.applicationContext)
|
||||
GoBackend.setAlwaysOnCallback { get().tunnelManager.restoreState(true).whenComplete(ExceptionLoggers.D) }
|
||||
}
|
||||
app.backend = backend
|
||||
}
|
||||
return app.backend!!
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getBackendAsync() = get().futureBackend
|
||||
suspend fun getBackend() = get().futureBackend.await()
|
||||
|
||||
@JvmStatic
|
||||
fun getModuleLoader() = get().moduleLoader
|
||||
@@ -144,13 +146,16 @@ class Application : android.app.Application(), OnSharedPreferenceChangeListener
|
||||
fun getRootShell() = get().rootShell
|
||||
|
||||
@JvmStatic
|
||||
fun getSharedPreferences() = get().sharedPreferences
|
||||
fun getPreferencesDataStore() = get().preferencesDataStore
|
||||
|
||||
@JvmStatic
|
||||
fun getToolsInstaller() = get().toolsInstaller
|
||||
|
||||
@JvmStatic
|
||||
fun getTunnelManager() = get().tunnelManager
|
||||
|
||||
@JvmStatic
|
||||
fun getCoroutineScope() = get().coroutineScope
|
||||
}
|
||||
|
||||
init {
|
||||
|
||||
@@ -8,19 +8,19 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import com.wireguard.android.backend.Backend
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.wireguard.android.util.ExceptionLoggers
|
||||
import com.wireguard.android.util.applicationScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class BootShutdownReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Application.getBackendAsync().thenAccept { backend: Backend? ->
|
||||
if (backend !is WgQuickBackend) return@thenAccept
|
||||
val action = intent.action ?: return@thenAccept
|
||||
applicationScope.launch {
|
||||
if (Application.getBackend() !is WgQuickBackend) return@launch
|
||||
val action = intent.action ?: return@launch
|
||||
val tunnelManager = Application.getTunnelManager()
|
||||
if (Intent.ACTION_BOOT_COMPLETED == action) {
|
||||
Log.i(TAG, "Broadcast receiver restoring state (boot)")
|
||||
tunnelManager.restoreState(false).whenComplete(ExceptionLoggers.D)
|
||||
tunnelManager.restoreState(false)
|
||||
} else if (Intent.ACTION_SHUTDOWN == action) {
|
||||
Log.i(TAG, "Broadcast receiver saving state (shutdown)")
|
||||
tunnelManager.saveState()
|
||||
|
||||
@@ -20,7 +20,9 @@ import com.wireguard.android.activity.MainActivity
|
||||
import com.wireguard.android.activity.TunnelToggleActivity
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.model.ObservableTunnel
|
||||
import com.wireguard.android.util.applicationScope
|
||||
import com.wireguard.android.widget.SlashDrawable
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Service that maintains the application's custom Quick Settings tile. This service is bound by the
|
||||
@@ -40,7 +42,7 @@ class QuickTileService : TileService() {
|
||||
var ret: IBinder? = null
|
||||
try {
|
||||
ret = super.onBind(intent)
|
||||
} catch (e: Exception) {
|
||||
} catch (e: Throwable) {
|
||||
Log.d(TAG, "Failed to bind to TileService", e)
|
||||
}
|
||||
return ret
|
||||
@@ -54,11 +56,12 @@ class QuickTileService : TileService() {
|
||||
tile.icon = if (tile.icon == iconOn) iconOff else iconOn
|
||||
tile.updateTile()
|
||||
}
|
||||
tunnel!!.setStateAsync(Tunnel.State.TOGGLE).whenComplete { _, t ->
|
||||
if (t == null) {
|
||||
applicationScope.launch {
|
||||
try {
|
||||
tunnel!!.setStateAsync(Tunnel.State.TOGGLE)
|
||||
updateTile()
|
||||
} else {
|
||||
val toggleIntent = Intent(this, TunnelToggleActivity::class.java)
|
||||
} catch (_: Throwable) {
|
||||
val toggleIntent = Intent(this@QuickTileService, TunnelToggleActivity::class.java)
|
||||
toggleIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(toggleIntent)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ package com.wireguard.android.activity
|
||||
import android.os.Bundle
|
||||
import androidx.databinding.CallbackRegistry
|
||||
import androidx.databinding.CallbackRegistry.NotifierCallback
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.model.ObservableTunnel
|
||||
|
||||
@@ -35,11 +36,8 @@ abstract class BaseActivity : ThemeChangeAwareActivity() {
|
||||
intent != null -> intent.getStringExtra(KEY_SELECTED_TUNNEL)
|
||||
else -> null
|
||||
}
|
||||
if (savedTunnelName != null) {
|
||||
Application.getTunnelManager()
|
||||
.tunnels
|
||||
.thenAccept { selectedTunnel = it[savedTunnelName] }
|
||||
}
|
||||
if (savedTunnelName != null)
|
||||
lifecycleScope.launchWhenCreated { selectedTunnel = Application.getTunnelManager().getTunnels()[savedTunnelName] }
|
||||
|
||||
// The selected tunnel must be set before the superclass method recreates fragments.
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -51,6 +49,7 @@ abstract class BaseActivity : ThemeChangeAwareActivity() {
|
||||
}
|
||||
|
||||
protected abstract fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?)
|
||||
|
||||
fun removeOnSelectedTunnelChangedListener(
|
||||
listener: OnSelectedTunnelChangedListener) {
|
||||
selectionChangeRegistry.remove(listener)
|
||||
|
||||
@@ -24,8 +24,11 @@ import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ShareCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -36,13 +39,8 @@ import com.wireguard.android.R
|
||||
import com.wireguard.android.databinding.LogViewerActivityBinding
|
||||
import com.wireguard.android.util.DownloadsFileSaver
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import com.wireguard.android.widget.EdgeToEdge.setUpFAB
|
||||
import com.wireguard.android.widget.EdgeToEdge.setUpRoot
|
||||
import com.wireguard.android.widget.EdgeToEdge.setUpScrollingContent
|
||||
import com.wireguard.crypto.KeyPair
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.BufferedReader
|
||||
@@ -60,33 +58,26 @@ import java.util.regex.Matcher
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class LogViewerActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: LogViewerActivityBinding
|
||||
private lateinit var logAdapter: LogEntryAdapter
|
||||
private var logLines = arrayListOf<LogLine>()
|
||||
private var rawLogLines = StringBuffer()
|
||||
private var recyclerView: RecyclerView? = null
|
||||
private var saveButton: MenuItem? = null
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Default)
|
||||
private val year by lazy {
|
||||
val yearFormatter: DateFormat = SimpleDateFormat("yyyy", Locale.US)
|
||||
yearFormatter.format(Date())
|
||||
}
|
||||
|
||||
@Suppress("Deprecation")
|
||||
private val defaultColor by lazy { resources.getColor(R.color.primary_text_color) }
|
||||
private val defaultColor by lazy { ResourcesCompat.getColor(resources, R.color.primary_text_color, theme) }
|
||||
|
||||
@Suppress("Deprecation")
|
||||
private val debugColor by lazy { resources.getColor(R.color.debug_tag_color) }
|
||||
private val debugColor by lazy { ResourcesCompat.getColor(resources, R.color.debug_tag_color, theme) }
|
||||
|
||||
@Suppress("Deprecation")
|
||||
private val errorColor by lazy { resources.getColor(R.color.error_tag_color) }
|
||||
private val errorColor by lazy { ResourcesCompat.getColor(resources, R.color.error_tag_color, theme) }
|
||||
|
||||
@Suppress("Deprecation")
|
||||
private val infoColor by lazy { resources.getColor(R.color.info_tag_color) }
|
||||
private val infoColor by lazy { ResourcesCompat.getColor(resources, R.color.info_tag_color, theme) }
|
||||
|
||||
@Suppress("Deprecation")
|
||||
private val warningColor by lazy { resources.getColor(R.color.warning_tag_color) }
|
||||
private val warningColor by lazy { ResourcesCompat.getColor(resources, R.color.warning_tag_color, theme) }
|
||||
|
||||
private var lastUri: Uri? = null
|
||||
|
||||
@@ -103,9 +94,6 @@ class LogViewerActivity : AppCompatActivity() {
|
||||
binding = LogViewerActivityBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
setUpFAB(binding.shareFab)
|
||||
setUpRoot(binding.root)
|
||||
setUpScrollingContent(binding.recyclerView, binding.shareFab)
|
||||
logAdapter = LogEntryAdapter()
|
||||
binding.recyclerView.apply {
|
||||
recyclerView = this
|
||||
@@ -114,7 +102,11 @@ class LogViewerActivity : AppCompatActivity() {
|
||||
addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL))
|
||||
}
|
||||
|
||||
coroutineScope.launch { streamingLog() }
|
||||
lifecycleScope.launch(Dispatchers.IO) { streamingLog() }
|
||||
|
||||
val revokeLastActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
revokeLastUri()
|
||||
}
|
||||
|
||||
binding.shareFab.setOnClickListener {
|
||||
revokeLastUri()
|
||||
@@ -129,17 +121,10 @@ class LogViewerActivity : AppCompatActivity() {
|
||||
.createChooserIntent()
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
grantUriPermission("android", lastUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
startActivityForResult(shareIntent, SHARE_ACTIVITY_REQUEST)
|
||||
revokeLastActivityResultLauncher.launch(shareIntent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == SHARE_ACTIVITY_REQUEST) {
|
||||
revokeLastUri()
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.log_viewer, menu)
|
||||
saveButton = menu?.findItem(R.id.save_log)
|
||||
@@ -153,93 +138,88 @@ class LogViewerActivity : AppCompatActivity() {
|
||||
true
|
||||
}
|
||||
R.id.save_log -> {
|
||||
coroutineScope.launch { saveLog() }
|
||||
saveButton?.isEnabled = false
|
||||
lifecycleScope.launch { saveLog() }
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
|
||||
private suspend fun saveLog() {
|
||||
val context = this
|
||||
withContext(Dispatchers.Main) {
|
||||
saveButton?.isEnabled = false
|
||||
withContext(Dispatchers.IO) {
|
||||
var exception: Throwable? = null
|
||||
var outputFile: DownloadsFileSaver.DownloadsFile? = null
|
||||
try {
|
||||
outputFile = DownloadsFileSaver.save(context, "wireguard-log.txt", "text/plain", true)
|
||||
outputFile.outputStream.use {
|
||||
it.write(rawLogLines.toString().toByteArray(Charsets.UTF_8))
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
outputFile?.delete()
|
||||
exception = e
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
saveButton?.isEnabled = true
|
||||
Snackbar.make(findViewById(android.R.id.content),
|
||||
if (exception == null) getString(R.string.log_export_success, outputFile?.fileName)
|
||||
else getString(R.string.log_export_error, ErrorMessages[exception]),
|
||||
if (exception == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG)
|
||||
.setAnchorView(binding.shareFab)
|
||||
.show()
|
||||
}
|
||||
var exception: Throwable? = null
|
||||
var outputFile: DownloadsFileSaver.DownloadsFile? = null
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
outputFile = DownloadsFileSaver.save(this@LogViewerActivity, "wireguard-log.txt", "text/plain", true)
|
||||
outputFile?.outputStream?.write(rawLogLines.toString().toByteArray(Charsets.UTF_8))
|
||||
} catch (e: Throwable) {
|
||||
outputFile?.delete()
|
||||
exception = e
|
||||
}
|
||||
}
|
||||
saveButton?.isEnabled = true
|
||||
if (outputFile == null)
|
||||
return
|
||||
Snackbar.make(findViewById(android.R.id.content),
|
||||
if (exception == null) getString(R.string.log_export_success, outputFile?.fileName)
|
||||
else getString(R.string.log_export_error, ErrorMessages[exception]),
|
||||
if (exception == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG)
|
||||
.setAnchorView(binding.shareFab)
|
||||
.show()
|
||||
}
|
||||
|
||||
private suspend fun streamingLog() = withContext(Dispatchers.IO) {
|
||||
val builder = ProcessBuilder().command("logcat", "-b", "all", "-v", "threadtime", "*:V")
|
||||
builder.environment()["LC_ALL"] = "C"
|
||||
val process = try {
|
||||
builder.start()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
return@withContext
|
||||
}
|
||||
val stdout = BufferedReader(InputStreamReader(process!!.inputStream, StandardCharsets.UTF_8))
|
||||
var haveScrolled = false
|
||||
val start = System.nanoTime()
|
||||
var startPeriod = start
|
||||
while (true) {
|
||||
val line = stdout.readLine() ?: break
|
||||
rawLogLines.append(line)
|
||||
rawLogLines.append('\n')
|
||||
val logLine = parseLine(line)
|
||||
withContext(Dispatchers.Main) {
|
||||
if (logLine != null) {
|
||||
recyclerView?.let {
|
||||
val shouldScroll = haveScrolled && !it.canScrollVertically(1)
|
||||
logLines.add(logLine)
|
||||
var process: Process? = null
|
||||
try {
|
||||
process = try {
|
||||
builder.start()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
return@withContext
|
||||
}
|
||||
val stdout = BufferedReader(InputStreamReader(process!!.inputStream, StandardCharsets.UTF_8))
|
||||
var haveScrolled = false
|
||||
val start = System.nanoTime()
|
||||
var startPeriod = start
|
||||
while (true) {
|
||||
val line = stdout.readLine() ?: break
|
||||
rawLogLines.append(line)
|
||||
rawLogLines.append('\n')
|
||||
val logLine = parseLine(line)
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
if (logLine != null) {
|
||||
recyclerView?.let {
|
||||
val shouldScroll = haveScrolled && !it.canScrollVertically(1)
|
||||
logLines.add(logLine)
|
||||
if (haveScrolled) logAdapter.notifyDataSetChanged()
|
||||
if (shouldScroll)
|
||||
it.scrollToPosition(logLines.size - 1)
|
||||
}
|
||||
} else {
|
||||
/* TODO: I'd prefer for the next line to be:
|
||||
* logLines.lastOrNull()?.msg += "\n$line"
|
||||
* However, as of writing, that causes the kotlin compiler to freak out and crash, spewing bytecode.
|
||||
*/
|
||||
logLines.lastOrNull()?.apply { msg += "\n$line" }
|
||||
if (haveScrolled) logAdapter.notifyDataSetChanged()
|
||||
if (shouldScroll)
|
||||
it.scrollToPosition(logLines.size - 1)
|
||||
}
|
||||
} else {
|
||||
/* I'd prefer for the next line to be:
|
||||
* logLines.lastOrNull()?.msg += "\n$line"
|
||||
* However, as of writing, that causes the kotlin compiler to freak out and crash, spewing bytecode.
|
||||
*/
|
||||
logLines.lastOrNull()?.apply { msg += "\n$line" }
|
||||
if (haveScrolled) logAdapter.notifyDataSetChanged()
|
||||
}
|
||||
if (!haveScrolled) {
|
||||
val end = System.nanoTime()
|
||||
val scroll = (end - start) > 1000000000L * 2.5 || !stdout.ready()
|
||||
if (logLines.isNotEmpty() && (scroll || (end - startPeriod) > 1000000000L / 4)) {
|
||||
logAdapter.notifyDataSetChanged()
|
||||
recyclerView?.scrollToPosition(logLines.size - 1)
|
||||
startPeriod = end
|
||||
if (!haveScrolled) {
|
||||
val end = System.nanoTime()
|
||||
val scroll = (end - start) > 1000000000L * 2.5 || !stdout.ready()
|
||||
if (logLines.isNotEmpty() && (scroll || (end - startPeriod) > 1000000000L / 4)) {
|
||||
logAdapter.notifyDataSetChanged()
|
||||
recyclerView?.scrollToPosition(logLines.size - 1)
|
||||
startPeriod = end
|
||||
}
|
||||
if (scroll) haveScrolled = true
|
||||
}
|
||||
if (scroll) haveScrolled = true
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
process?.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,7 +251,6 @@ class LogViewerActivity : AppCompatActivity() {
|
||||
*/
|
||||
private val THREADTIME_LINE: Pattern = Pattern.compile("^(\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3})(?:\\s+[0-9A-Za-z]+)?\\s+(\\d+)\\s+(\\d+)\\s+([A-Z])\\s+(.+?)\\s*: (.*)$")
|
||||
private val LOGS: MutableMap<String, ByteArray> = ConcurrentHashMap()
|
||||
private const val SHARE_ACTIVITY_REQUEST = 49133
|
||||
}
|
||||
|
||||
private inner class LogEntryAdapter : RecyclerView.Adapter<LogEntryAdapter.ViewHolder>() {
|
||||
@@ -348,7 +327,7 @@ class LogViewerActivity : AppCompatActivity() {
|
||||
return openPipeHelper(uri, "text/plain", null, log) { output, _, _, _, l ->
|
||||
try {
|
||||
FileOutputStream(output.fileDescriptor).write(l!!)
|
||||
} catch (_: Exception) {
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import androidx.appcompat.app.ActionBar
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
@@ -55,20 +54,9 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.main_activity)
|
||||
actionBar = supportActionBar
|
||||
isTwoPaneLayout = findViewById<View>(R.id.master_detail_wrapper) is LinearLayout
|
||||
isTwoPaneLayout = findViewById<View?>(R.id.master_detail_wrapper) != null
|
||||
supportFragmentManager.addOnBackStackChangedListener(this)
|
||||
onBackStackChanged()
|
||||
// Dispatch insets on back stack change
|
||||
// This is required to ensure replaced fragments are also able to consume insets
|
||||
findViewById<View>(R.id.master_detail_wrapper).setOnApplyWindowInsetsListener { _, insets ->
|
||||
val fragmentManager = supportFragmentManager
|
||||
fragmentManager.addOnBackStackChangedListener {
|
||||
fragmentManager.fragments.forEach {
|
||||
it.requireView().dispatchApplyWindowInsets(insets)
|
||||
}
|
||||
}
|
||||
insets
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
|
||||
@@ -5,48 +5,26 @@
|
||||
package com.wireguard.android.activity
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.SparseArray
|
||||
import android.view.MenuItem
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.wireguard.android.preference.PreferencesPreferenceDataStore
|
||||
import com.wireguard.android.util.AdminKnobs
|
||||
import com.wireguard.android.util.ModuleLoader
|
||||
import java.util.ArrayList
|
||||
import java.util.Arrays
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Interface for changing application-global persistent settings.
|
||||
*/
|
||||
class SettingsActivity : ThemeChangeAwareActivity() {
|
||||
private val permissionRequestCallbacks = SparseArray<(permissions: Array<String>, granted: IntArray) -> Unit>()
|
||||
private var permissionRequestCounter = 0
|
||||
|
||||
fun ensurePermissions(permissions: Array<String>, cb: (permissions: Array<String>, granted: IntArray) -> Unit) {
|
||||
val needPermissions: MutableList<String> = ArrayList(permissions.size)
|
||||
permissions.forEach {
|
||||
if (ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED) {
|
||||
needPermissions.add(it)
|
||||
}
|
||||
}
|
||||
if (needPermissions.isEmpty()) {
|
||||
val granted = IntArray(permissions.size)
|
||||
Arrays.fill(granted, PackageManager.PERMISSION_GRANTED)
|
||||
cb.invoke(permissions, granted)
|
||||
return
|
||||
}
|
||||
val idx = permissionRequestCounter++
|
||||
permissionRequestCallbacks.put(idx, cb)
|
||||
ActivityCompat.requestPermissions(this,
|
||||
needPermissions.toTypedArray(), idx)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (supportFragmentManager.findFragmentById(android.R.id.content) == null) {
|
||||
@@ -64,18 +42,9 @@ class SettingsActivity : ThemeChangeAwareActivity() {
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray) {
|
||||
val f = permissionRequestCallbacks[requestCode]
|
||||
if (f != null) {
|
||||
permissionRequestCallbacks.remove(requestCode)
|
||||
f.invoke(permissions, grantResults)
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, key: String?) {
|
||||
preferenceManager.preferenceDataStore = PreferencesPreferenceDataStore(lifecycleScope, Application.getPreferencesDataStore())
|
||||
addPreferencesFromResource(R.xml.preferences)
|
||||
preferenceScreen.initialExpandedChildrenCount = 4
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
@@ -87,14 +56,18 @@ class SettingsActivity : ThemeChangeAwareActivity() {
|
||||
val remoteApps = preferenceManager.findPreference<Preference>("allow_remote_control_intents")
|
||||
remoteApps?.parent?.removePreference(remoteApps)
|
||||
}
|
||||
if (AdminKnobs.disableConfigExport) {
|
||||
val zipExporter = preferenceManager.findPreference<Preference>("zip_exporter")
|
||||
zipExporter?.parent?.removePreference(zipExporter)
|
||||
}
|
||||
val wgQuickOnlyPrefs = arrayOf(
|
||||
preferenceManager.findPreference("tools_installer"),
|
||||
preferenceManager.findPreference("restore_on_boot"),
|
||||
preferenceManager.findPreference<Preference>("multiple_tunnels")
|
||||
).filterNotNull()
|
||||
wgQuickOnlyPrefs.forEach { it.isVisible = false }
|
||||
Application.getBackendAsync().thenAccept { backend ->
|
||||
if (backend is WgQuickBackend) {
|
||||
lifecycleScope.launch {
|
||||
if (Application.getBackend() is WgQuickBackend) {
|
||||
++preferenceScreen.initialExpandedChildrenCount
|
||||
wgQuickOnlyPrefs.forEach { it.isVisible = true }
|
||||
} else {
|
||||
@@ -110,13 +83,24 @@ class SettingsActivity : ThemeChangeAwareActivity() {
|
||||
moduleInstaller?.isVisible = false
|
||||
if (ModuleLoader.isModuleLoaded()) {
|
||||
moduleInstaller?.parent?.removePreference(moduleInstaller)
|
||||
lifecycleScope.launch {
|
||||
if (Application.getBackend() !is WgQuickBackend) {
|
||||
try {
|
||||
withContext(Dispatchers.IO) { Application.getRootShell().start() }
|
||||
} catch (_: Throwable) {
|
||||
kernelModuleDisabler?.parent?.removePreference(kernelModuleDisabler)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
kernelModuleDisabler?.parent?.removePreference(kernelModuleDisabler)
|
||||
Application.getAsyncWorker().runAsync(Application.getRootShell()::start).whenComplete { _, e ->
|
||||
if (e == null)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) { Application.getRootShell().start() }
|
||||
moduleInstaller?.isVisible = true
|
||||
else
|
||||
} catch (_: Throwable) {
|
||||
moduleInstaller?.parent?.removePreference(moduleInstaller)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,39 +4,30 @@
|
||||
*/
|
||||
package com.wireguard.android.activity
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import com.wireguard.android.Application
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.wireguard.android.util.UserKnobs
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
abstract class ThemeChangeAwareActivity : AppCompatActivity(), OnSharedPreferenceChangeListener {
|
||||
abstract class ThemeChangeAwareActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
Application.getSharedPreferences().registerOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
Application.getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
when (key) {
|
||||
"dark_theme" -> {
|
||||
AppCompatDelegate.setDefaultNightMode(if (sharedPreferences.getBoolean(key, false)) {
|
||||
UserKnobs.darkTheme.onEach {
|
||||
val newMode = if (it) {
|
||||
AppCompatDelegate.MODE_NIGHT_YES
|
||||
} else {
|
||||
AppCompatDelegate.MODE_NIGHT_NO
|
||||
})
|
||||
recreate()
|
||||
}
|
||||
}
|
||||
if (AppCompatDelegate.getDefaultNightMode() != newMode) {
|
||||
AppCompatDelegate.setDefaultNightMode(newMode)
|
||||
recreate()
|
||||
}
|
||||
}.launchIn(lifecycleScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,32 +12,36 @@ import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.QuickTileService
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
class TunnelToggleActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val tunnel = Application.getTunnelManager().lastUsedTunnel ?: return
|
||||
tunnel.setStateAsync(Tunnel.State.TOGGLE).whenComplete { _, t ->
|
||||
TileService.requestListeningState(this, ComponentName(this, QuickTileService::class.java))
|
||||
onToggleFinished(t)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
tunnel.setStateAsync(Tunnel.State.TOGGLE)
|
||||
} catch (e: Throwable) {
|
||||
TileService.requestListeningState(this@TunnelToggleActivity, ComponentName(this@TunnelToggleActivity, QuickTileService::class.java))
|
||||
val error = ErrorMessages[e]
|
||||
val message = getString(R.string.toggle_error, error)
|
||||
Log.e(TAG, message, e)
|
||||
Toast.makeText(this@TunnelToggleActivity, message, Toast.LENGTH_LONG).show()
|
||||
finishAffinity()
|
||||
return@launch
|
||||
}
|
||||
TileService.requestListeningState(this@TunnelToggleActivity, ComponentName(this@TunnelToggleActivity, QuickTileService::class.java))
|
||||
finishAffinity()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onToggleFinished(throwable: Throwable?) {
|
||||
if (throwable == null) return
|
||||
val error = ErrorMessages[throwable]
|
||||
val message = getString(R.string.toggle_error, error)
|
||||
Log.e(TAG, message, throwable)
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "WireGuard/TunnelToggleActivity"
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ import com.wireguard.android.widget.ToggleSwitch
|
||||
import com.wireguard.android.widget.ToggleSwitch.OnBeforeCheckedChangeListener
|
||||
import com.wireguard.config.Attribute
|
||||
import com.wireguard.config.InetNetwork
|
||||
import java9.util.Optional
|
||||
import java.net.InetAddress
|
||||
import java.util.Optional
|
||||
|
||||
/**
|
||||
* Static methods for use by generated code in the Android data binding library.
|
||||
@@ -135,13 +136,30 @@ object BindingAdapters {
|
||||
|
||||
@JvmStatic
|
||||
@BindingAdapter("android:text")
|
||||
fun setText(view: TextView, text: Optional<*>) {
|
||||
view.text = text.map { it.toString() }.orElse("")
|
||||
fun setOptionalText(view: TextView, text: Optional<*>?) {
|
||||
view.text = text?.map { it.toString() }?.orElse("") ?: ""
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@BindingAdapter("android:text")
|
||||
fun setText(view: TextView, networks: Iterable<InetNetwork?>?) {
|
||||
fun setInetNetworkSetText(view: TextView, networks: Iterable<InetNetwork?>?) {
|
||||
view.text = if (networks != null) Attribute.join(networks) else ""
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@BindingAdapter("android:text")
|
||||
fun setInetAddressSetText(view: TextView, addresses: Iterable<InetAddress?>?) {
|
||||
view.text = if (addresses != null) Attribute.join(addresses.map { it?.hostAddress }) else ""
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun tryParseInt(s: String?): Int {
|
||||
if (s == null)
|
||||
return 0
|
||||
return try {
|
||||
Integer.parseInt(s)
|
||||
} catch (_: Throwable) {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -17,8 +17,10 @@ import java.lang.ref.WeakReference
|
||||
/**
|
||||
* A generic `RecyclerView.Adapter` backed by a `ObservableKeyedArrayList`.
|
||||
*/
|
||||
class ObservableKeyedRecyclerViewAdapter<K, E : Keyed<out K>> internal constructor(context: Context, private val layoutId: Int,
|
||||
list: ObservableKeyedArrayList<K, E>?) : RecyclerView.Adapter<ObservableKeyedRecyclerViewAdapter.ViewHolder>() {
|
||||
class ObservableKeyedRecyclerViewAdapter<K, E : Keyed<out K>> internal constructor(
|
||||
context: Context, private val layoutId: Int,
|
||||
list: ObservableKeyedArrayList<K, E>?
|
||||
) : RecyclerView.Adapter<ObservableKeyedRecyclerViewAdapter.ViewHolder>() {
|
||||
private val callback = OnListChangedCallback(this)
|
||||
private val layoutInflater: LayoutInflater = LayoutInflater.from(context)
|
||||
private var list: ObservableKeyedArrayList<K, E>? = null
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
package com.wireguard.android.fragment
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
@@ -12,13 +12,12 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewTreeObserver
|
||||
import android.widget.FrameLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.activity.TunnelCreatorActivity
|
||||
import com.wireguard.android.util.resolveAttribute
|
||||
|
||||
class AddTunnelsSheet : BottomSheetDialogFragment() {
|
||||
@@ -41,7 +40,13 @@ class AddTunnelsSheet : BottomSheetDialogFragment() {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
if (savedInstanceState != null) dismiss()
|
||||
return inflater.inflate(R.layout.add_tunnels_bottom_sheet, container, false)
|
||||
val view = inflater.inflate(R.layout.add_tunnels_bottom_sheet, container, false)
|
||||
if (activity?.packageManager?.hasSystemFeature(PackageManager.FEATURE_CAMERA) != true) {
|
||||
val qrcode = view.findViewById<View>(R.id.create_from_qrcode)
|
||||
qrcode.isEnabled = false
|
||||
qrcode.visibility = View.GONE
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@@ -81,28 +86,23 @@ class AddTunnelsSheet : BottomSheetDialogFragment() {
|
||||
behavior?.removeBottomSheetCallback(bottomSheetCallback)
|
||||
}
|
||||
|
||||
private fun requireTargetFragment(): Fragment {
|
||||
return requireNotNull(targetFragment) { "A target fragment should always be set" }
|
||||
}
|
||||
|
||||
private fun onRequestCreateConfig() {
|
||||
startActivity(Intent(activity, TunnelCreatorActivity::class.java))
|
||||
setFragmentResult(REQUEST_KEY_NEW_TUNNEL, bundleOf(REQUEST_METHOD to REQUEST_CREATE))
|
||||
}
|
||||
|
||||
private fun onRequestImportConfig() {
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
}
|
||||
requireTargetFragment().startActivityForResult(intent, TunnelListFragment.REQUEST_IMPORT)
|
||||
setFragmentResult(REQUEST_KEY_NEW_TUNNEL, bundleOf(REQUEST_METHOD to REQUEST_IMPORT))
|
||||
}
|
||||
|
||||
private fun onRequestScanQRCode() {
|
||||
val integrator = IntentIntegrator.forSupportFragment(requireTargetFragment()).apply {
|
||||
setOrientationLocked(false)
|
||||
setBeepEnabled(false)
|
||||
setPrompt(getString(R.string.qr_code_hint))
|
||||
}
|
||||
integrator.initiateScan(listOf(IntentIntegrator.QR_CODE))
|
||||
setFragmentResult(REQUEST_KEY_NEW_TUNNEL, bundleOf(REQUEST_METHOD to REQUEST_SCAN))
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val REQUEST_KEY_NEW_TUNNEL = "request_new_tunnel"
|
||||
const val REQUEST_METHOD = "request_method"
|
||||
const val REQUEST_CREATE = "request_create"
|
||||
const val REQUEST_IMPORT = "request_import"
|
||||
const val REQUEST_SCAN = "request_scan"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,64 +5,104 @@
|
||||
package com.wireguard.android.fragment
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Button
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.databinding.Observable
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.wireguard.android.Application
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.wireguard.android.BR
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.databinding.AppListDialogFragmentBinding
|
||||
import com.wireguard.android.databinding.ObservableKeyedArrayList
|
||||
import com.wireguard.android.model.ApplicationData
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class AppListDialogFragment : DialogFragment() {
|
||||
private val appData: ObservableKeyedArrayList<String, ApplicationData> = ObservableKeyedArrayList()
|
||||
private var currentlyExcludedApps = emptyList<String>()
|
||||
private val appData = ObservableKeyedArrayList<String, ApplicationData>()
|
||||
private var currentlySelectedApps = emptyList<String>()
|
||||
private var initiallyExcluded = false
|
||||
private var button: Button? = null
|
||||
private var tabs: TabLayout? = null
|
||||
|
||||
private fun loadData() {
|
||||
val activity = activity ?: return
|
||||
val pm = activity.packageManager
|
||||
Application.getAsyncWorker().supplyAsync<List<ApplicationData>> {
|
||||
val launcherIntent = Intent(Intent.ACTION_MAIN, null)
|
||||
launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
val resolveInfos = pm.queryIntentActivities(launcherIntent, 0)
|
||||
val applicationData: MutableList<ApplicationData> = ArrayList()
|
||||
resolveInfos.forEach {
|
||||
val packageName = it.activityInfo.packageName
|
||||
applicationData.add(ApplicationData(it.loadIcon(pm), it.loadLabel(pm).toString(), packageName, currentlyExcludedApps.contains(packageName)))
|
||||
}
|
||||
applicationData.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
applicationData
|
||||
}.whenComplete { data, throwable ->
|
||||
if (data != null) {
|
||||
appData.clear()
|
||||
appData.addAll(data)
|
||||
} else {
|
||||
val error = ErrorMessages[throwable]
|
||||
val message = activity.getString(R.string.error_fetching_apps, error)
|
||||
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
|
||||
dismissAllowingStateLoss()
|
||||
lifecycleScope.launch(Dispatchers.Default) {
|
||||
try {
|
||||
val applicationData: MutableList<ApplicationData> = ArrayList()
|
||||
withContext(Dispatchers.IO) {
|
||||
val launcherIntent = Intent(Intent.ACTION_MAIN, null)
|
||||
launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
val resolveInfos = pm.queryIntentActivities(launcherIntent, 0)
|
||||
resolveInfos.forEach {
|
||||
val packageName = it.activityInfo.packageName
|
||||
val appData = ApplicationData(it.loadIcon(pm), it.loadLabel(pm).toString(), packageName, currentlySelectedApps.contains(packageName))
|
||||
applicationData.add(appData)
|
||||
appData.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
||||
if (propertyId == BR.selected)
|
||||
setButtonText()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
applicationData.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
appData.clear()
|
||||
appData.addAll(applicationData)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
val error = ErrorMessages[e]
|
||||
val message = activity.getString(R.string.error_fetching_apps, error)
|
||||
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val excludedApps = requireArguments().getStringArrayList(KEY_EXCLUDED_APPS)
|
||||
currentlyExcludedApps = (excludedApps ?: emptyList())
|
||||
currentlySelectedApps = (arguments?.getStringArrayList(KEY_SELECTED_APPS) ?: emptyList())
|
||||
initiallyExcluded = arguments?.getBoolean(KEY_IS_EXCLUDED) ?: true
|
||||
}
|
||||
|
||||
private fun setButtonText() {
|
||||
val numSelected = appData.count { it.isSelected }
|
||||
button?.text = if (numSelected == 0)
|
||||
getString(R.string.use_all_applications)
|
||||
else when (tabs?.selectedTabPosition) {
|
||||
0 -> resources.getQuantityString(R.plurals.exclude_n_applications, numSelected, numSelected)
|
||||
1 -> resources.getQuantityString(R.plurals.include_n_applications, numSelected, numSelected)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val alertDialogBuilder = AlertDialog.Builder(requireActivity())
|
||||
alertDialogBuilder.setTitle(R.string.excluded_applications)
|
||||
val binding = AppListDialogFragmentBinding.inflate(requireActivity().layoutInflater, null, false)
|
||||
binding.executePendingBindings()
|
||||
alertDialogBuilder.setView(binding.root)
|
||||
alertDialogBuilder.setPositiveButton(R.string.set_exclusions) { _, _ -> setExclusionsAndDismiss() }
|
||||
tabs = binding.tabs
|
||||
tabs?.apply {
|
||||
selectTab(binding.tabs.getTabAt(if (initiallyExcluded) 0 else 1))
|
||||
addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) = Unit
|
||||
override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) = setButtonText()
|
||||
})
|
||||
}
|
||||
alertDialogBuilder.setPositiveButton(" ") { _, _ -> setSelectionAndDismiss() }
|
||||
alertDialogBuilder.setNegativeButton(R.string.cancel) { dialog, _ -> dialog.dismiss() }
|
||||
alertDialogBuilder.setNeutralButton(R.string.toggle_all) { _, _ -> }
|
||||
binding.fragment = this
|
||||
@@ -70,41 +110,41 @@ class AppListDialogFragment : DialogFragment() {
|
||||
loadData()
|
||||
val dialog = alertDialogBuilder.create()
|
||||
dialog.setOnShowListener {
|
||||
dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener {
|
||||
val selectedItems = appData
|
||||
.filter { it.isExcludedFromTunnel }
|
||||
|
||||
val excludeAll = selectedItems.isEmpty()
|
||||
button = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||
setButtonText()
|
||||
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener { _ ->
|
||||
val selectAll = appData.none { it.isSelected }
|
||||
appData.forEach {
|
||||
it.isExcludedFromTunnel = excludeAll
|
||||
it.isSelected = selectAll
|
||||
}
|
||||
}
|
||||
}
|
||||
return dialog
|
||||
}
|
||||
|
||||
private fun setExclusionsAndDismiss() {
|
||||
val excludedApps: MutableList<String> = ArrayList()
|
||||
private fun setSelectionAndDismiss() {
|
||||
val selectedApps: MutableList<String> = ArrayList()
|
||||
for (data in appData) {
|
||||
if (data.isExcludedFromTunnel) {
|
||||
excludedApps.add(data.packageName)
|
||||
if (data.isSelected) {
|
||||
selectedApps.add(data.packageName)
|
||||
}
|
||||
}
|
||||
(targetFragment as AppExclusionListener?)!!.onExcludedAppsSelected(excludedApps)
|
||||
setFragmentResult(REQUEST_SELECTION, bundleOf(
|
||||
KEY_SELECTED_APPS to selectedApps.toTypedArray(),
|
||||
KEY_IS_EXCLUDED to (tabs?.selectedTabPosition == 0)
|
||||
))
|
||||
dismiss()
|
||||
}
|
||||
|
||||
interface AppExclusionListener {
|
||||
fun onExcludedAppsSelected(excludedApps: List<String>)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_EXCLUDED_APPS = "excludedApps"
|
||||
fun <T> newInstance(excludedApps: ArrayList<String?>?, target: T): AppListDialogFragment where T : Fragment?, T : AppExclusionListener? {
|
||||
const val KEY_SELECTED_APPS = "selected_apps"
|
||||
const val KEY_IS_EXCLUDED = "is_excluded"
|
||||
const val REQUEST_SELECTION = "request_selection"
|
||||
fun newInstance(selectedApps: ArrayList<String?>?, isExcluded: Boolean): AppListDialogFragment {
|
||||
val extras = Bundle()
|
||||
extras.putStringArrayList(KEY_EXCLUDED_APPS, excludedApps)
|
||||
extras.putStringArrayList(KEY_SELECTED_APPS, selectedApps)
|
||||
extras.putBoolean(KEY_IS_EXCLUDED, isExcluded)
|
||||
val fragment = AppListDialogFragment()
|
||||
fragment.setTargetFragment(target, 0)
|
||||
fragment.arguments = extras
|
||||
return fragment
|
||||
}
|
||||
|
||||
@@ -5,25 +5,26 @@
|
||||
package com.wireguard.android.fragment
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.activity.BaseActivity
|
||||
import com.wireguard.android.activity.BaseActivity.OnSelectedTunnelChangedListener
|
||||
import com.wireguard.android.backend.Backend
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.databinding.TunnelDetailFragmentBinding
|
||||
import com.wireguard.android.databinding.TunnelListItemBinding
|
||||
import com.wireguard.android.model.ObservableTunnel
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Base class for fragments that need to know the currently-selected tunnel. Only does anything when
|
||||
@@ -33,21 +34,21 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
|
||||
private var baseActivity: BaseActivity? = null
|
||||
private var pendingTunnel: ObservableTunnel? = null
|
||||
private var pendingTunnelUp: Boolean? = null
|
||||
private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
val tunnel = pendingTunnel
|
||||
val checked = pendingTunnelUp
|
||||
if (tunnel != null && checked != null)
|
||||
setTunnelStateWithPermissionsResult(tunnel, checked)
|
||||
pendingTunnel = null
|
||||
pendingTunnelUp = null
|
||||
}
|
||||
|
||||
protected var selectedTunnel: ObservableTunnel?
|
||||
get() = baseActivity?.selectedTunnel
|
||||
protected set(tunnel) {
|
||||
baseActivity?.selectedTunnel = tunnel
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == REQUEST_CODE_VPN_PERMISSION) {
|
||||
if (pendingTunnel != null && pendingTunnelUp != null) setTunnelStateWithPermissionsResult(pendingTunnel!!, pendingTunnelUp!!)
|
||||
pendingTunnel = null
|
||||
pendingTunnelUp = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
if (context is BaseActivity) {
|
||||
@@ -70,14 +71,14 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
|
||||
is TunnelListItemBinding -> binding.item
|
||||
else -> return
|
||||
} ?: return
|
||||
Application.getBackendAsync().thenAccept { backend: Backend? ->
|
||||
if (backend is GoBackend) {
|
||||
lifecycleScope.launch {
|
||||
if (Application.getBackend() is GoBackend) {
|
||||
val intent = GoBackend.VpnService.prepare(view.context)
|
||||
if (intent != null) {
|
||||
pendingTunnel = tunnel
|
||||
pendingTunnelUp = checked
|
||||
startActivityForResult(intent, REQUEST_CODE_VPN_PERMISSION)
|
||||
return@thenAccept
|
||||
permissionActivityResultLauncher.launch(intent)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
setTunnelStateWithPermissionsResult(tunnel, checked)
|
||||
@@ -85,24 +86,26 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
|
||||
}
|
||||
|
||||
private fun setTunnelStateWithPermissionsResult(tunnel: ObservableTunnel, checked: Boolean) {
|
||||
tunnel.setStateAsync(Tunnel.State.of(checked)).whenComplete { _, throwable ->
|
||||
if (throwable == null) return@whenComplete
|
||||
val error = ErrorMessages[throwable]
|
||||
val messageResId = if (checked) R.string.error_up else R.string.error_down
|
||||
val message = requireContext().getString(messageResId, error)
|
||||
val view = view
|
||||
if (view != null)
|
||||
Snackbar.make(view, message, Snackbar.LENGTH_LONG)
|
||||
.setAnchorView(view.findViewById<View>(R.id.create_fab))
|
||||
.show()
|
||||
else
|
||||
Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show()
|
||||
Log.e(TAG, message, throwable)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
tunnel.setStateAsync(Tunnel.State.of(checked))
|
||||
} catch (e: Throwable) {
|
||||
val error = ErrorMessages[e]
|
||||
val messageResId = if (checked) R.string.error_up else R.string.error_down
|
||||
val message = requireContext().getString(messageResId, error)
|
||||
val view = view
|
||||
if (view != null)
|
||||
Snackbar.make(view, message, Snackbar.LENGTH_LONG)
|
||||
.setAnchorView(view.findViewById(R.id.create_fab))
|
||||
.show()
|
||||
else
|
||||
Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show()
|
||||
Log.e(TAG, message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_CODE_VPN_PERMISSION = 23491
|
||||
private const val TAG = "WireGuard/BaseFragment"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,17 +5,20 @@
|
||||
package com.wireguard.android.fragment
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.databinding.ConfigNamingDialogFragmentBinding
|
||||
import com.wireguard.config.BadConfigException
|
||||
import com.wireguard.config.Config
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.nio.charset.StandardCharsets
|
||||
@@ -28,11 +31,12 @@ class ConfigNamingDialogFragment : DialogFragment() {
|
||||
private fun createTunnelAndDismiss() {
|
||||
binding?.let {
|
||||
val name = it.tunnelNameText.text.toString()
|
||||
Application.getTunnelManager().create(name, config).whenComplete { tunnel, throwable ->
|
||||
if (tunnel != null) {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
Application.getTunnelManager().create(name, config)
|
||||
dismiss()
|
||||
} else {
|
||||
it.tunnelNameTextLayout.error = throwable.message
|
||||
} catch (e: Throwable) {
|
||||
it.tunnelNameTextLayout.error = e.message
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,7 +53,7 @@ class ConfigNamingDialogFragment : DialogFragment() {
|
||||
val configBytes = configText!!.toByteArray(StandardCharsets.UTF_8)
|
||||
config = try {
|
||||
Config.parse(ByteArrayInputStream(configBytes))
|
||||
} catch (e: Exception) {
|
||||
} catch (e: Throwable) {
|
||||
when (e) {
|
||||
is BadConfigException, is IOException -> throw IllegalArgumentException("Invalid config passed to ${javaClass.simpleName}", e)
|
||||
else -> throw e
|
||||
@@ -59,7 +63,7 @@ class ConfigNamingDialogFragment : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val activity = requireActivity()
|
||||
imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm = activity.getSystemService()
|
||||
val alertDialogBuilder = AlertDialog.Builder(activity)
|
||||
alertDialogBuilder.setTitle(R.string.import_from_qr_code)
|
||||
binding = ConfigNamingDialogFragmentBinding.inflate(activity.layoutInflater, null, false)
|
||||
@@ -69,7 +73,18 @@ class ConfigNamingDialogFragment : DialogFragment() {
|
||||
}
|
||||
alertDialogBuilder.setPositiveButton(R.string.create_tunnel, null)
|
||||
alertDialogBuilder.setNegativeButton(R.string.cancel) { _, _ -> dismiss() }
|
||||
return alertDialogBuilder.create()
|
||||
return alertDialogBuilder.create().apply {
|
||||
setOnShowListener {
|
||||
findViewById<TextInputEditText>(R.id.tunnel_name_text)?.apply {
|
||||
setOnFocusChangeListener { v, _ ->
|
||||
v.post {
|
||||
imm?.showSoftInput(v, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
}
|
||||
requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
||||
@@ -11,13 +11,13 @@ import android.view.MenuInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.databinding.TunnelDetailFragmentBinding
|
||||
import com.wireguard.android.databinding.TunnelDetailPeerBinding
|
||||
import com.wireguard.android.model.ObservableTunnel
|
||||
import com.wireguard.android.widget.EdgeToEdge.setUpRoot
|
||||
import com.wireguard.android.widget.EdgeToEdge.setUpScrollingContent
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
|
||||
@@ -53,11 +53,7 @@ class TunnelDetailFragment : BaseFragment() {
|
||||
savedInstanceState: Bundle?): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
binding = TunnelDetailFragmentBinding.inflate(inflater, container, false)
|
||||
binding?.apply {
|
||||
executePendingBindings()
|
||||
setUpRoot(root as ViewGroup)
|
||||
setUpScrollingContent(root as ViewGroup, null)
|
||||
}
|
||||
binding?.executePendingBindings()
|
||||
return binding!!.root
|
||||
}
|
||||
|
||||
@@ -79,7 +75,13 @@ class TunnelDetailFragment : BaseFragment() {
|
||||
override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?) {
|
||||
binding ?: return
|
||||
binding!!.tunnel = newTunnel
|
||||
if (newTunnel == null) binding!!.config = null else newTunnel.configAsync.thenAccept { config -> binding!!.config = config }
|
||||
if (newTunnel == null) binding!!.config = null else lifecycleScope.launch {
|
||||
try {
|
||||
binding!!.config = newTunnel.getConfigAsync()
|
||||
} catch (_: Throwable) {
|
||||
binding!!.config = null
|
||||
}
|
||||
}
|
||||
lastState = Tunnel.State.TOGGLE
|
||||
updateStats()
|
||||
}
|
||||
@@ -105,30 +107,31 @@ class TunnelDetailFragment : BaseFragment() {
|
||||
val state = tunnel.state
|
||||
if (state != Tunnel.State.UP && lastState == state) return
|
||||
lastState = state
|
||||
tunnel.statisticsAsync.whenComplete { statistics, throwable ->
|
||||
if (throwable != null) {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val statistics = tunnel.getStatisticsAsync()
|
||||
for (i in 0 until binding!!.peersLayout.childCount) {
|
||||
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding!!.peersLayout.getChildAt(i))
|
||||
?: continue
|
||||
val publicKey = peer.item!!.publicKey
|
||||
val rx = statistics.peerRx(publicKey)
|
||||
val tx = statistics.peerTx(publicKey)
|
||||
if (rx == 0L && tx == 0L) {
|
||||
peer.transferLabel.visibility = View.GONE
|
||||
peer.transferText.visibility = View.GONE
|
||||
continue
|
||||
}
|
||||
peer.transferText.text = requireContext().getString(R.string.transfer_rx_tx, formatBytes(rx), formatBytes(tx))
|
||||
peer.transferLabel.visibility = View.VISIBLE
|
||||
peer.transferText.visibility = View.VISIBLE
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
for (i in 0 until binding!!.peersLayout.childCount) {
|
||||
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding!!.peersLayout.getChildAt(i))
|
||||
?: continue
|
||||
peer.transferLabel.visibility = View.GONE
|
||||
peer.transferText.visibility = View.GONE
|
||||
}
|
||||
return@whenComplete
|
||||
}
|
||||
for (i in 0 until binding!!.peersLayout.childCount) {
|
||||
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding!!.peersLayout.getChildAt(i))
|
||||
?: continue
|
||||
val publicKey = peer.item!!.publicKey
|
||||
val rx = statistics.peerRx(publicKey)
|
||||
val tx = statistics.peerTx(publicKey)
|
||||
if (rx == 0L && tx == 0L) {
|
||||
peer.transferLabel.visibility = View.GONE
|
||||
peer.transferText.visibility = View.GONE
|
||||
continue
|
||||
}
|
||||
peer.transferText.text = requireContext().getString(R.string.transfer_rx_tx, formatBytes(rx), formatBytes(tx))
|
||||
peer.transferLabel.visibility = View.VISIBLE
|
||||
peer.transferText.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,24 +18,24 @@ import android.view.WindowManager
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.databinding.TunnelEditorFragmentBinding
|
||||
import com.wireguard.android.fragment.AppListDialogFragment.AppExclusionListener
|
||||
import com.wireguard.android.model.ObservableTunnel
|
||||
import com.wireguard.android.util.AdminKnobs
|
||||
import com.wireguard.android.util.BiometricAuthenticator
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import com.wireguard.android.viewmodel.ConfigProxy
|
||||
import com.wireguard.android.widget.EdgeToEdge.setUpRoot
|
||||
import com.wireguard.android.widget.EdgeToEdge.setUpScrollingContent
|
||||
import com.wireguard.config.Config
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Fragment for editing a WireGuard configuration.
|
||||
*/
|
||||
class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
|
||||
class TunnelEditorFragment : BaseFragment() {
|
||||
private var haveShownKeys = false
|
||||
private var binding: TunnelEditorFragmentBinding? = null
|
||||
private var tunnel: ObservableTunnel? = null
|
||||
@@ -75,8 +75,6 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
|
||||
binding = TunnelEditorFragmentBinding.inflate(inflater, container, false)
|
||||
binding?.apply {
|
||||
executePendingBindings()
|
||||
setUpRoot(root as ViewGroup)
|
||||
setUpScrollingContent(mainContainer, null)
|
||||
privateKeyTextLayout.setEndIconOnClickListener { config?.`interface`?.generateKeyPair() }
|
||||
}
|
||||
return binding?.root
|
||||
@@ -88,14 +86,6 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onExcludedAppsSelected(excludedApps: List<String>) {
|
||||
requireNotNull(binding) { "Tried to set excluded apps while no view was loaded" }
|
||||
binding!!.config!!.`interface`.excludedApplications.apply {
|
||||
clear()
|
||||
addAll(excludedApps)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFinished() {
|
||||
// Hide the keyboard; it rarely goes away on its own.
|
||||
val activity = activity ?: return
|
||||
@@ -105,14 +95,11 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
|
||||
inputManager?.hideSoftInputFromWindow(focusedView.windowToken,
|
||||
InputMethodManager.HIDE_NOT_ALWAYS)
|
||||
}
|
||||
// Tell the activity to finish itself or go back to the detail view.
|
||||
activity.runOnUiThread {
|
||||
// TODO(smaeul): Remove this hack when fixing the Config ViewModel
|
||||
// The selected tunnel has to actually change, but we have to remember this one.
|
||||
val savedTunnel = tunnel
|
||||
if (savedTunnel === selectedTunnel) selectedTunnel = null
|
||||
selectedTunnel = savedTunnel
|
||||
}
|
||||
parentFragmentManager.popBackStackImmediate()
|
||||
|
||||
// If we just made a new one, save it to select the details page.
|
||||
if (selectedTunnel != tunnel)
|
||||
selectedTunnel = tunnel
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
@@ -120,7 +107,7 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
|
||||
binding ?: return false
|
||||
val newConfig = try {
|
||||
binding!!.config!!.resolve()
|
||||
} catch (e: Exception) {
|
||||
} catch (e: Throwable) {
|
||||
val error = ErrorMessages[e]
|
||||
val tunnelName = if (tunnel == null) binding!!.name else tunnel!!.name
|
||||
val message = getString(R.string.config_save_error, tunnelName, error)
|
||||
@@ -128,20 +115,35 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
|
||||
Snackbar.make(binding!!.mainContainer, error, Snackbar.LENGTH_LONG).show()
|
||||
return false
|
||||
}
|
||||
when {
|
||||
tunnel == null -> {
|
||||
Log.d(TAG, "Attempting to create new tunnel " + binding!!.name)
|
||||
val manager = Application.getTunnelManager()
|
||||
manager.create(binding!!.name!!, newConfig).whenComplete(this::onTunnelCreated)
|
||||
}
|
||||
tunnel!!.name != binding!!.name -> {
|
||||
Log.d(TAG, "Attempting to rename tunnel to " + binding!!.name)
|
||||
tunnel!!.setNameAsync(binding!!.name!!).whenComplete { _, t -> onTunnelRenamed(tunnel!!, newConfig, t) }
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "Attempting to save config of " + tunnel!!.name)
|
||||
tunnel!!.setConfigAsync(newConfig)
|
||||
.whenComplete { _, t -> onConfigSaved(tunnel!!, t) }
|
||||
lifecycleScope.launch {
|
||||
when {
|
||||
tunnel == null -> {
|
||||
Log.d(TAG, "Attempting to create new tunnel " + binding!!.name)
|
||||
val manager = Application.getTunnelManager()
|
||||
try {
|
||||
onTunnelCreated(manager.create(binding!!.name!!, newConfig), null)
|
||||
} catch (e: Throwable) {
|
||||
onTunnelCreated(null, e)
|
||||
}
|
||||
}
|
||||
tunnel!!.name != binding!!.name -> {
|
||||
Log.d(TAG, "Attempting to rename tunnel to " + binding!!.name)
|
||||
try {
|
||||
tunnel!!.setNameAsync(binding!!.name!!)
|
||||
onTunnelRenamed(tunnel!!, newConfig, null)
|
||||
} catch (e: Throwable) {
|
||||
onTunnelRenamed(tunnel!!, newConfig, e)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "Attempting to save config of " + tunnel!!.name)
|
||||
try {
|
||||
tunnel!!.setConfigAsync(newConfig)
|
||||
onConfigSaved(tunnel!!, null)
|
||||
} catch (e: Throwable) {
|
||||
onConfigSaved(tunnel!!, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
@@ -150,11 +152,35 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun onRequestSetExcludedApplications(view: View?) {
|
||||
fun onRequestSetExcludedIncludedApplications(view: View?) {
|
||||
if (binding != null) {
|
||||
val excludedApps = ArrayList(binding!!.config!!.`interface`.excludedApplications)
|
||||
val fragment = AppListDialogFragment.newInstance(excludedApps, this)
|
||||
fragment.show(parentFragmentManager, null)
|
||||
var isExcluded = true
|
||||
var selectedApps = ArrayList(binding!!.config!!.`interface`.excludedApplications)
|
||||
if (selectedApps.isEmpty()) {
|
||||
selectedApps = ArrayList(binding!!.config!!.`interface`.includedApplications)
|
||||
if (selectedApps.isNotEmpty())
|
||||
isExcluded = false
|
||||
}
|
||||
val fragment = AppListDialogFragment.newInstance(selectedApps, isExcluded)
|
||||
childFragmentManager.setFragmentResultListener(AppListDialogFragment.REQUEST_SELECTION, viewLifecycleOwner) { _, bundle ->
|
||||
requireNotNull(binding) { "Tried to set excluded/included apps while no view was loaded" }
|
||||
val newSelections = requireNotNull(bundle.getStringArray(AppListDialogFragment.KEY_SELECTED_APPS))
|
||||
val excluded = requireNotNull(bundle.getBoolean(AppListDialogFragment.KEY_IS_EXCLUDED))
|
||||
if (excluded) {
|
||||
binding!!.config!!.`interface`.includedApplications.clear()
|
||||
binding!!.config!!.`interface`.excludedApplications.apply {
|
||||
clear()
|
||||
addAll(newSelections)
|
||||
}
|
||||
} else {
|
||||
binding!!.config!!.`interface`.excludedApplications.clear()
|
||||
binding!!.config!!.`interface`.includedApplications.apply {
|
||||
clear()
|
||||
addAll(newSelections)
|
||||
}
|
||||
}
|
||||
}
|
||||
fragment.show(childFragmentManager, null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,13 +197,18 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
|
||||
binding!!.config = ConfigProxy()
|
||||
if (tunnel != null) {
|
||||
binding!!.name = tunnel!!.name
|
||||
tunnel!!.configAsync.thenAccept(this::onConfigLoaded)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
onConfigLoaded(tunnel!!.getConfigAsync())
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding!!.name = ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTunnelCreated(newTunnel: ObservableTunnel, throwable: Throwable?) {
|
||||
private fun onTunnelCreated(newTunnel: ObservableTunnel?, throwable: Throwable?) {
|
||||
val message: String
|
||||
if (throwable == null) {
|
||||
tunnel = newTunnel
|
||||
@@ -203,7 +234,14 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
|
||||
Log.d(TAG, message)
|
||||
// Now save the rest of configuration changes.
|
||||
Log.d(TAG, "Attempting to save config of renamed tunnel " + tunnel!!.name)
|
||||
renamedTunnel.setConfigAsync(newConfig).whenComplete { _, t -> onConfigSaved(renamedTunnel, t) }
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
renamedTunnel.setConfigAsync(newConfig)
|
||||
onConfigSaved(renamedTunnel, null)
|
||||
} catch (e: Throwable) {
|
||||
onConfigSaved(renamedTunnel, e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val error = ErrorMessages[throwable]
|
||||
message = getString(R.string.tunnel_rename_error, error)
|
||||
@@ -237,6 +275,7 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
|
||||
val edit = view as? EditText ?: return
|
||||
if (edit.inputType == InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) return
|
||||
if (!haveShownKeys && edit.text.isNotEmpty()) {
|
||||
if (AdminKnobs.disableConfigExport) return
|
||||
showingAuthenticator = true
|
||||
BiometricAuthenticator.authenticate(R.string.biometric_prompt_private_key_title, this) {
|
||||
showingAuthenticator = false
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
*/
|
||||
package com.wireguard.android.fragment
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
import android.net.Uri
|
||||
@@ -17,24 +15,32 @@ import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.AnimationUtils
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.activity.TunnelCreatorActivity
|
||||
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler
|
||||
import com.wireguard.android.databinding.TunnelListFragmentBinding
|
||||
import com.wireguard.android.databinding.TunnelListItemBinding
|
||||
import com.wireguard.android.fragment.ConfigNamingDialogFragment.Companion.newInstance
|
||||
import com.wireguard.android.model.ObservableTunnel
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import com.wireguard.android.widget.EdgeToEdge.setUpFAB
|
||||
import com.wireguard.android.widget.EdgeToEdge.setUpRoot
|
||||
import com.wireguard.android.widget.EdgeToEdge.setUpScrollingContent
|
||||
import com.wireguard.android.widget.MultiselectableRelativeLayout
|
||||
import com.wireguard.config.Config
|
||||
import java9.util.concurrent.CompletableFuture
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.BufferedReader
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStreamReader
|
||||
@@ -52,6 +58,15 @@ class TunnelListFragment : BaseFragment() {
|
||||
private val actionModeListener = ActionModeListener()
|
||||
private var actionMode: ActionMode? = null
|
||||
private var binding: TunnelListFragmentBinding? = null
|
||||
private val tunnelFileImportResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { data ->
|
||||
importTunnel(data)
|
||||
}
|
||||
|
||||
private val qrImportResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
val qrCode = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
||||
qrCode?.contents?.let { importTunnel(it) }
|
||||
}
|
||||
|
||||
private fun importTunnel(configText: String) {
|
||||
try {
|
||||
// Ensure the config text is parseable before proceeding…
|
||||
@@ -59,115 +74,103 @@ class TunnelListFragment : BaseFragment() {
|
||||
|
||||
// Config text is valid, now create the tunnel…
|
||||
newInstance(configText).show(parentFragmentManager, null)
|
||||
} catch (e: Exception) {
|
||||
} catch (e: Throwable) {
|
||||
onTunnelImportFinished(emptyList(), listOf<Throwable>(e))
|
||||
}
|
||||
}
|
||||
|
||||
private fun importTunnel(uri: Uri?) {
|
||||
val activity = activity
|
||||
if (activity == null || uri == null) {
|
||||
return
|
||||
}
|
||||
val contentResolver = activity.contentResolver
|
||||
|
||||
val futureTunnels = ArrayList<CompletableFuture<ObservableTunnel>>()
|
||||
val throwables = ArrayList<Throwable>()
|
||||
Application.getAsyncWorker().supplyAsync {
|
||||
val columns = arrayOf(OpenableColumns.DISPLAY_NAME)
|
||||
var name = ""
|
||||
contentResolver.query(uri, columns, null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst() && !cursor.isNull(0)) {
|
||||
name = cursor.getString(0)
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val activity = activity
|
||||
if (activity == null || uri == null) {
|
||||
return@withContext
|
||||
}
|
||||
}
|
||||
if (name.isEmpty()) {
|
||||
name = Uri.decode(uri.lastPathSegment)
|
||||
}
|
||||
var idx = name.lastIndexOf('/')
|
||||
if (idx >= 0) {
|
||||
require(idx < name.length - 1) { resources.getString(R.string.illegal_filename_error, name) }
|
||||
name = name.substring(idx + 1)
|
||||
}
|
||||
val isZip = name.toLowerCase(Locale.ROOT).endsWith(".zip")
|
||||
if (name.toLowerCase(Locale.ROOT).endsWith(".conf")) {
|
||||
name = name.substring(0, name.length - ".conf".length)
|
||||
} else {
|
||||
require(isZip) { resources.getString(R.string.bad_extension_error) }
|
||||
}
|
||||
val contentResolver = activity.contentResolver
|
||||
val futureTunnels = ArrayList<Deferred<ObservableTunnel>>()
|
||||
val throwables = ArrayList<Throwable>()
|
||||
try {
|
||||
val columns = arrayOf(OpenableColumns.DISPLAY_NAME)
|
||||
var name = ""
|
||||
contentResolver.query(uri, columns, null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst() && !cursor.isNull(0)) {
|
||||
name = cursor.getString(0)
|
||||
}
|
||||
}
|
||||
if (name.isEmpty()) {
|
||||
name = Uri.decode(uri.lastPathSegment)
|
||||
}
|
||||
var idx = name.lastIndexOf('/')
|
||||
if (idx >= 0) {
|
||||
require(idx < name.length - 1) { resources.getString(R.string.illegal_filename_error, name) }
|
||||
name = name.substring(idx + 1)
|
||||
}
|
||||
val isZip = name.toLowerCase(Locale.ROOT).endsWith(".zip")
|
||||
if (name.toLowerCase(Locale.ROOT).endsWith(".conf")) {
|
||||
name = name.substring(0, name.length - ".conf".length)
|
||||
} else {
|
||||
require(isZip) { resources.getString(R.string.bad_extension_error) }
|
||||
}
|
||||
|
||||
if (isZip) {
|
||||
ZipInputStream(contentResolver.openInputStream(uri)).use { zip ->
|
||||
val reader = BufferedReader(InputStreamReader(zip, StandardCharsets.UTF_8))
|
||||
var entry: ZipEntry?
|
||||
while (true) {
|
||||
entry = zip.nextEntry ?: break
|
||||
name = entry.name
|
||||
idx = name.lastIndexOf('/')
|
||||
if (idx >= 0) {
|
||||
if (idx >= name.length - 1) {
|
||||
continue
|
||||
if (isZip) {
|
||||
ZipInputStream(contentResolver.openInputStream(uri)).use { zip ->
|
||||
val reader = BufferedReader(InputStreamReader(zip, StandardCharsets.UTF_8))
|
||||
var entry: ZipEntry?
|
||||
while (true) {
|
||||
entry = zip.nextEntry ?: break
|
||||
name = entry.name
|
||||
idx = name.lastIndexOf('/')
|
||||
if (idx >= 0) {
|
||||
if (idx >= name.length - 1) {
|
||||
continue
|
||||
}
|
||||
name = name.substring(name.lastIndexOf('/') + 1)
|
||||
}
|
||||
if (name.toLowerCase(Locale.ROOT).endsWith(".conf")) {
|
||||
name = name.substring(0, name.length - ".conf".length)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
try {
|
||||
Config.parse(reader)
|
||||
} catch (e: Throwable) {
|
||||
throwables.add(e)
|
||||
null
|
||||
}?.let {
|
||||
val nameCopy = name
|
||||
futureTunnels.add(async(SupervisorJob()) { Application.getTunnelManager().create(nameCopy, it) })
|
||||
}
|
||||
}
|
||||
name = name.substring(name.lastIndexOf('/') + 1)
|
||||
}
|
||||
if (name.toLowerCase(Locale.ROOT).endsWith(".conf")) {
|
||||
name = name.substring(0, name.length - ".conf".length)
|
||||
} else {
|
||||
futureTunnels.add(async(SupervisorJob()) { Application.getTunnelManager().create(name, Config.parse(contentResolver.openInputStream(uri)!!)) })
|
||||
}
|
||||
|
||||
if (futureTunnels.isEmpty()) {
|
||||
if (throwables.size == 1) {
|
||||
throw throwables[0]
|
||||
} else {
|
||||
continue
|
||||
require(throwables.isNotEmpty()) { resources.getString(R.string.no_configs_error) }
|
||||
}
|
||||
}
|
||||
val tunnels = futureTunnels.mapNotNull {
|
||||
try {
|
||||
Config.parse(reader)
|
||||
} catch (e: Exception) {
|
||||
throwables.add(e)
|
||||
null
|
||||
}?.let {
|
||||
futureTunnels.add(Application.getTunnelManager().create(name, it).toCompletableFuture())
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
futureTunnels.add(
|
||||
Application.getTunnelManager().create(
|
||||
name,
|
||||
Config.parse(contentResolver.openInputStream(uri)!!)
|
||||
).toCompletableFuture()
|
||||
)
|
||||
}
|
||||
|
||||
if (futureTunnels.isEmpty()) {
|
||||
if (throwables.size == 1) {
|
||||
throw throwables[0]
|
||||
} else {
|
||||
require(throwables.isNotEmpty()) { resources.getString(R.string.no_configs_error) }
|
||||
}
|
||||
}
|
||||
CompletableFuture.allOf(*futureTunnels.toTypedArray())
|
||||
}.whenComplete { future, exception ->
|
||||
if (exception != null) {
|
||||
onTunnelImportFinished(emptyList(), listOf(exception))
|
||||
} else {
|
||||
future.whenComplete { _, _ ->
|
||||
val tunnels = mutableListOf<ObservableTunnel>()
|
||||
for (futureTunnel in futureTunnels) {
|
||||
val tunnel: ObservableTunnel? = try {
|
||||
futureTunnel.getNow(null)
|
||||
} catch (e: Exception) {
|
||||
it.await()
|
||||
} catch (e: Throwable) {
|
||||
throwables.add(e)
|
||||
null
|
||||
}
|
||||
|
||||
if (tunnel != null) {
|
||||
tunnels.add(tunnel)
|
||||
}
|
||||
}
|
||||
onTunnelImportFinished(tunnels, throwables)
|
||||
withContext(Dispatchers.Main.immediate) { onTunnelImportFinished(tunnels, throwables) }
|
||||
} catch (e: Throwable) {
|
||||
withContext(Dispatchers.Main.immediate) { onTunnelImportFinished(emptyList(), listOf(e)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
if (savedInstanceState != null) {
|
||||
val checkedItems = savedInstanceState.getIntegerArrayList(CHECKED_ITEMS)
|
||||
if (checkedItems != null) {
|
||||
@@ -176,38 +179,33 @@ class TunnelListFragment : BaseFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when (requestCode) {
|
||||
REQUEST_IMPORT -> {
|
||||
if (resultCode == Activity.RESULT_OK && data != null) importTunnel(data.data)
|
||||
return
|
||||
}
|
||||
IntentIntegrator.REQUEST_CODE -> {
|
||||
val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
|
||||
if (result != null && result.contents != null) {
|
||||
importTunnel(result.contents)
|
||||
}
|
||||
return
|
||||
}
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
binding = TunnelListFragmentBinding.inflate(inflater, container, false)
|
||||
val bottomSheet = AddTunnelsSheet()
|
||||
binding?.apply {
|
||||
createFab.setOnClickListener {
|
||||
val bottomSheet = AddTunnelsSheet()
|
||||
bottomSheet.setTargetFragment(fragment, REQUEST_TARGET_FRAGMENT)
|
||||
bottomSheet.show(parentFragmentManager, "BOTTOM_SHEET")
|
||||
childFragmentManager.setFragmentResultListener(AddTunnelsSheet.REQUEST_KEY_NEW_TUNNEL, viewLifecycleOwner) { _, bundle ->
|
||||
when (bundle.getString(AddTunnelsSheet.REQUEST_METHOD)) {
|
||||
AddTunnelsSheet.REQUEST_CREATE -> {
|
||||
startActivity(Intent(requireActivity(), TunnelCreatorActivity::class.java))
|
||||
}
|
||||
AddTunnelsSheet.REQUEST_IMPORT -> {
|
||||
tunnelFileImportResultLauncher.launch("*/*")
|
||||
}
|
||||
AddTunnelsSheet.REQUEST_SCAN -> {
|
||||
qrImportResultLauncher.launch(IntentIntegrator(requireActivity())
|
||||
.setOrientationLocked(false)
|
||||
.setBeepEnabled(false)
|
||||
.setPrompt(getString(R.string.qr_code_hint))
|
||||
.createScanIntent())
|
||||
}
|
||||
}
|
||||
}
|
||||
bottomSheet.show(childFragmentManager, "BOTTOM_SHEET")
|
||||
}
|
||||
executePendingBindings()
|
||||
setUpRoot(root as ViewGroup)
|
||||
setUpFAB(createFab)
|
||||
setUpScrollingContent(tunnelList, createFab)
|
||||
}
|
||||
return binding!!.root
|
||||
}
|
||||
@@ -224,7 +222,8 @@ class TunnelListFragment : BaseFragment() {
|
||||
|
||||
override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?) {
|
||||
binding ?: return
|
||||
Application.getTunnelManager().tunnels.thenAccept { tunnels ->
|
||||
lifecycleScope.launch {
|
||||
val tunnels = Application.getTunnelManager().getTunnels()
|
||||
if (newTunnel != null) viewForTunnel(newTunnel, tunnels).setSingleSelected(true)
|
||||
if (oldTunnel != null) viewForTunnel(oldTunnel, tunnels).setSingleSelected(false)
|
||||
}
|
||||
@@ -266,11 +265,10 @@ class TunnelListFragment : BaseFragment() {
|
||||
super.onViewStateRestored(savedInstanceState)
|
||||
binding ?: return
|
||||
binding!!.fragment = this
|
||||
Application.getTunnelManager().tunnels.thenAccept { tunnels -> binding!!.tunnels = tunnels }
|
||||
val parent = this
|
||||
lifecycleScope.launch { binding!!.tunnels = Application.getTunnelManager().getTunnels() }
|
||||
binding!!.rowConfigurationHandler = object : RowConfigurationHandler<TunnelListItemBinding, ObservableTunnel> {
|
||||
override fun onConfigureRow(binding: TunnelListItemBinding, item: ObservableTunnel, position: Int) {
|
||||
binding.fragment = parent
|
||||
binding.fragment = this@TunnelListFragment
|
||||
binding.root.setOnClickListener {
|
||||
if (actionMode == null) {
|
||||
selectedTunnel = item
|
||||
@@ -314,20 +312,29 @@ class TunnelListFragment : BaseFragment() {
|
||||
return when (item.itemId) {
|
||||
R.id.menu_action_delete -> {
|
||||
val copyCheckedItems = HashSet(checkedItems)
|
||||
Application.getTunnelManager().tunnels.thenAccept { tunnels ->
|
||||
val tunnelsToDelete = ArrayList<ObservableTunnel>()
|
||||
for (position in copyCheckedItems) tunnelsToDelete.add(tunnels[position])
|
||||
val futures = tunnelsToDelete.map { it.delete().toCompletableFuture() }.toTypedArray()
|
||||
CompletableFuture.allOf(*futures)
|
||||
.thenApply { futures.size }
|
||||
.whenComplete(this@TunnelListFragment::onTunnelDeletionFinished)
|
||||
binding?.createFab?.apply {
|
||||
visibility = View.VISIBLE
|
||||
scaleX = 1f
|
||||
scaleY = 1f
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val tunnels = Application.getTunnelManager().getTunnels()
|
||||
val tunnelsToDelete = ArrayList<ObservableTunnel>()
|
||||
for (position in copyCheckedItems) tunnelsToDelete.add(tunnels[position])
|
||||
val futures = tunnelsToDelete.map { async(SupervisorJob()) { it.deleteAsync() } }
|
||||
onTunnelDeletionFinished(futures.awaitAll().size, null)
|
||||
} catch (e: Throwable) {
|
||||
onTunnelDeletionFinished(0, e)
|
||||
}
|
||||
}
|
||||
checkedItems.clear()
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
R.id.menu_action_select_all -> {
|
||||
Application.getTunnelManager().tunnels.thenAccept { tunnels ->
|
||||
lifecycleScope.launch {
|
||||
val tunnels = Application.getTunnelManager().getTunnels()
|
||||
for (i in 0 until tunnels.size) {
|
||||
setItemChecked(i, true)
|
||||
}
|
||||
@@ -343,14 +350,16 @@ class TunnelListFragment : BaseFragment() {
|
||||
if (activity != null) {
|
||||
resources = activity!!.resources
|
||||
}
|
||||
animateFab(binding?.createFab, false)
|
||||
mode.menuInflater.inflate(R.menu.tunnel_list_action_mode, menu)
|
||||
binding!!.tunnelList.adapter!!.notifyDataSetChanged()
|
||||
binding?.tunnelList?.adapter?.notifyDataSetChanged()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode) {
|
||||
actionMode = null
|
||||
resources = null
|
||||
animateFab(binding?.createFab, true)
|
||||
checkedItems.clear()
|
||||
binding!!.tunnelList.adapter!!.notifyDataSetChanged()
|
||||
}
|
||||
@@ -391,6 +400,26 @@ class TunnelListFragment : BaseFragment() {
|
||||
mode.title = resources!!.getQuantityString(R.plurals.delete_title, count, count)
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateFab(view: View?, show: Boolean) {
|
||||
view ?: return
|
||||
val animation = AnimationUtils.loadAnimation(
|
||||
context, if (show) R.anim.scale_up else R.anim.scale_down
|
||||
)
|
||||
animation.setAnimationListener(object : Animation.AnimationListener {
|
||||
override fun onAnimationRepeat(animation: Animation?) {
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animation?) {
|
||||
if (!show) view.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onAnimationStart(animation: Animation?) {
|
||||
if (show) view.visibility = View.VISIBLE
|
||||
}
|
||||
})
|
||||
view.startAnimation(animation)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -10,13 +10,13 @@ import androidx.databinding.Bindable
|
||||
import com.wireguard.android.BR
|
||||
import com.wireguard.android.databinding.Keyed
|
||||
|
||||
class ApplicationData(val icon: Drawable, val name: String, val packageName: String, isExcludedFromTunnel: Boolean) : BaseObservable(), Keyed<String> {
|
||||
class ApplicationData(val icon: Drawable, val name: String, val packageName: String, isSelected: Boolean) : BaseObservable(), Keyed<String> {
|
||||
override val key = name
|
||||
|
||||
@get:Bindable
|
||||
var isExcludedFromTunnel = isExcludedFromTunnel
|
||||
var isSelected = isSelected
|
||||
set(value) {
|
||||
field = value
|
||||
notifyPropertyChanged(BR.excludedFromTunnel)
|
||||
notifyPropertyChanged(BR.selected)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,18 @@
|
||||
*/
|
||||
package com.wireguard.android.model
|
||||
|
||||
import android.util.Log
|
||||
import androidx.databinding.BaseObservable
|
||||
import androidx.databinding.Bindable
|
||||
import com.wireguard.android.BR
|
||||
import com.wireguard.android.backend.Statistics
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.databinding.Keyed
|
||||
import com.wireguard.android.util.ExceptionLoggers
|
||||
import com.wireguard.android.util.applicationScope
|
||||
import com.wireguard.config.Config
|
||||
import java9.util.concurrent.CompletableFuture
|
||||
import java9.util.concurrent.CompletionStage
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Encapsulates the volatile and nonvolatile state of a WireGuard tunnel.
|
||||
@@ -30,10 +32,12 @@ class ObservableTunnel internal constructor(
|
||||
@Bindable
|
||||
override fun getName() = name
|
||||
|
||||
fun setNameAsync(name: String): CompletionStage<String> = if (name != this.name)
|
||||
manager.setTunnelName(this, name)
|
||||
else
|
||||
CompletableFuture.completedFuture(this.name)
|
||||
suspend fun setNameAsync(name: String): String = withContext(Dispatchers.Main.immediate) {
|
||||
if (name != this@ObservableTunnel.name)
|
||||
manager.setTunnelName(this@ObservableTunnel, name)
|
||||
else
|
||||
this@ObservableTunnel.name
|
||||
}
|
||||
|
||||
fun onNameChanged(name: String): String {
|
||||
this.name = name
|
||||
@@ -57,31 +61,42 @@ class ObservableTunnel internal constructor(
|
||||
return state
|
||||
}
|
||||
|
||||
fun setStateAsync(state: Tunnel.State): CompletionStage<Tunnel.State> = if (state != this.state)
|
||||
manager.setTunnelState(this, state)
|
||||
else
|
||||
CompletableFuture.completedFuture(this.state)
|
||||
suspend fun setStateAsync(state: Tunnel.State): Tunnel.State = withContext(Dispatchers.Main.immediate) {
|
||||
if (state != this@ObservableTunnel.state)
|
||||
manager.setTunnelState(this@ObservableTunnel, state)
|
||||
else
|
||||
this@ObservableTunnel.state
|
||||
}
|
||||
|
||||
|
||||
@get:Bindable
|
||||
var config = config
|
||||
get() {
|
||||
if (field == null)
|
||||
manager.getTunnelConfig(this).whenComplete(ExceptionLoggers.E)
|
||||
// Opportunistically fetch this if we don't have a cached one, and rely on data bindings to update it eventually
|
||||
applicationScope.launch {
|
||||
try {
|
||||
manager.getTunnelConfig(this@ObservableTunnel)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
}
|
||||
return field
|
||||
}
|
||||
private set
|
||||
|
||||
val configAsync: CompletionStage<Config>
|
||||
get() = if (config == null)
|
||||
manager.getTunnelConfig(this)
|
||||
else
|
||||
CompletableFuture.completedFuture(config)
|
||||
suspend fun getConfigAsync(): Config = withContext(Dispatchers.Main.immediate) {
|
||||
config ?: manager.getTunnelConfig(this@ObservableTunnel)
|
||||
}
|
||||
|
||||
fun setConfigAsync(config: Config): CompletionStage<Config> = if (config != this.config)
|
||||
manager.setTunnelConfig(this, config)
|
||||
else
|
||||
CompletableFuture.completedFuture(this.config)
|
||||
suspend fun setConfigAsync(config: Config): Config = withContext(Dispatchers.Main.immediate) {
|
||||
this@ObservableTunnel.config.let {
|
||||
if (config != it)
|
||||
manager.setTunnelConfig(this@ObservableTunnel, config)
|
||||
else
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
fun onConfigChanged(config: Config?): Config? {
|
||||
this.config = config
|
||||
@@ -93,17 +108,27 @@ class ObservableTunnel internal constructor(
|
||||
@get:Bindable
|
||||
var statistics: Statistics? = null
|
||||
get() {
|
||||
if (field == null || field!!.isStale)
|
||||
manager.getTunnelStatistics(this).whenComplete(ExceptionLoggers.E)
|
||||
if (field == null || field?.isStale != false)
|
||||
// Opportunistically fetch this if we don't have a cached one, and rely on data bindings to update it eventually
|
||||
applicationScope.launch {
|
||||
try {
|
||||
manager.getTunnelStatistics(this@ObservableTunnel)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
}
|
||||
return field
|
||||
}
|
||||
private set
|
||||
|
||||
val statisticsAsync: CompletionStage<Statistics>
|
||||
get() = if (statistics == null || statistics!!.isStale)
|
||||
manager.getTunnelStatistics(this)
|
||||
else
|
||||
CompletableFuture.completedFuture(statistics)
|
||||
suspend fun getStatisticsAsync(): Statistics = withContext(Dispatchers.Main.immediate) {
|
||||
statistics.let {
|
||||
if (it == null || it.isStale)
|
||||
manager.getTunnelStatistics(this@ObservableTunnel)
|
||||
else
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
fun onStatisticsChanged(statistics: Statistics?): Statistics? {
|
||||
this.statistics = statistics
|
||||
@@ -112,5 +137,10 @@ class ObservableTunnel internal constructor(
|
||||
}
|
||||
|
||||
|
||||
fun delete(): CompletionStage<Void> = manager.delete(this)
|
||||
suspend fun deleteAsync() = manager.delete(this)
|
||||
|
||||
|
||||
companion object {
|
||||
private const val TAG = "WireGuard/ObservableTunnel"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,17 +4,15 @@
|
||||
*/
|
||||
package com.wireguard.android.model
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.databinding.BaseObservable
|
||||
import androidx.databinding.Bindable
|
||||
import com.wireguard.android.Application.Companion.get
|
||||
import com.wireguard.android.Application.Companion.getAsyncWorker
|
||||
import com.wireguard.android.Application.Companion.getBackend
|
||||
import com.wireguard.android.Application.Companion.getSharedPreferences
|
||||
import com.wireguard.android.Application.Companion.getTunnelManager
|
||||
import com.wireguard.android.BR
|
||||
import com.wireguard.android.R
|
||||
@@ -22,145 +20,147 @@ import com.wireguard.android.backend.Statistics
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.configStore.ConfigStore
|
||||
import com.wireguard.android.databinding.ObservableSortedKeyedArrayList
|
||||
import com.wireguard.android.util.ExceptionLoggers
|
||||
import com.wireguard.android.util.UserKnobs
|
||||
import com.wireguard.android.util.applicationScope
|
||||
import com.wireguard.config.Config
|
||||
import java9.util.concurrent.CompletableFuture
|
||||
import java9.util.concurrent.CompletionStage
|
||||
import java.util.ArrayList
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Maintains and mediates changes to the set of available WireGuard tunnels,
|
||||
*/
|
||||
class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
|
||||
val tunnels = CompletableFuture<ObservableSortedKeyedArrayList<String, ObservableTunnel>>()
|
||||
private val tunnels = CompletableDeferred<ObservableSortedKeyedArrayList<String, ObservableTunnel>>()
|
||||
private val context: Context = get()
|
||||
private val delayedLoadRestoreTunnels = ArrayList<CompletableFuture<Void>>()
|
||||
private val tunnelMap: ObservableSortedKeyedArrayList<String, ObservableTunnel> = ObservableSortedKeyedArrayList(TunnelComparator)
|
||||
private var haveLoaded = false
|
||||
|
||||
private fun addToList(name: String, config: Config?, state: Tunnel.State): ObservableTunnel? {
|
||||
private fun addToList(name: String, config: Config?, state: Tunnel.State): ObservableTunnel {
|
||||
val tunnel = ObservableTunnel(this, name, config, state)
|
||||
tunnelMap.add(tunnel)
|
||||
return tunnel
|
||||
}
|
||||
|
||||
fun create(name: String, config: Config?): CompletionStage<ObservableTunnel> {
|
||||
suspend fun getTunnels(): ObservableSortedKeyedArrayList<String, ObservableTunnel> = tunnels.await()
|
||||
|
||||
suspend fun create(name: String, config: Config?): ObservableTunnel = withContext(Dispatchers.Main.immediate) {
|
||||
if (Tunnel.isNameInvalid(name))
|
||||
return CompletableFuture.failedFuture(IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)))
|
||||
throw IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name))
|
||||
if (tunnelMap.containsKey(name))
|
||||
return CompletableFuture.failedFuture(IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name)))
|
||||
return getAsyncWorker().supplyAsync { configStore.create(name, config!!) }.thenApply { addToList(name, it, Tunnel.State.DOWN) }
|
||||
throw IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name))
|
||||
addToList(name, withContext(Dispatchers.IO) { configStore.create(name, config!!) }, Tunnel.State.DOWN)
|
||||
}
|
||||
|
||||
fun delete(tunnel: ObservableTunnel): CompletionStage<Void> {
|
||||
suspend fun delete(tunnel: ObservableTunnel) = withContext(Dispatchers.Main.immediate) {
|
||||
val originalState = tunnel.state
|
||||
val wasLastUsed = tunnel == lastUsedTunnel
|
||||
// Make sure nothing touches the tunnel.
|
||||
if (wasLastUsed)
|
||||
lastUsedTunnel = null
|
||||
tunnelMap.remove(tunnel)
|
||||
return getAsyncWorker().runAsync {
|
||||
try {
|
||||
if (originalState == Tunnel.State.UP)
|
||||
getBackend().setState(tunnel, Tunnel.State.DOWN, null)
|
||||
withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.DOWN, null) }
|
||||
try {
|
||||
configStore.delete(tunnel.name)
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.IO) { configStore.delete(tunnel.name) }
|
||||
} catch (e: Throwable) {
|
||||
if (originalState == Tunnel.State.UP)
|
||||
getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config)
|
||||
withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config) }
|
||||
throw e
|
||||
}
|
||||
}.whenComplete { _, e ->
|
||||
if (e == null)
|
||||
return@whenComplete
|
||||
} catch (e: Throwable) {
|
||||
// Failure, put the tunnel back.
|
||||
tunnelMap.add(tunnel)
|
||||
if (wasLastUsed)
|
||||
lastUsedTunnel = tunnel
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@get:Bindable
|
||||
@SuppressLint("ApplySharedPref")
|
||||
var lastUsedTunnel: ObservableTunnel? = null
|
||||
private set(value) {
|
||||
if (value == field) return
|
||||
field = value
|
||||
notifyPropertyChanged(BR.lastUsedTunnel)
|
||||
if (value != null)
|
||||
getSharedPreferences().edit().putString(KEY_LAST_USED_TUNNEL, value.name).commit()
|
||||
else
|
||||
getSharedPreferences().edit().remove(KEY_LAST_USED_TUNNEL).commit()
|
||||
applicationScope.launch { UserKnobs.setLastUsedTunnel(value?.name) }
|
||||
}
|
||||
|
||||
fun getTunnelConfig(tunnel: ObservableTunnel): CompletionStage<Config> = getAsyncWorker()
|
||||
.supplyAsync { configStore.load(tunnel.name) }.thenApply(tunnel::onConfigChanged)
|
||||
|
||||
suspend fun getTunnelConfig(tunnel: ObservableTunnel): Config = withContext(Dispatchers.Main.immediate) {
|
||||
tunnel.onConfigChanged(withContext(Dispatchers.IO) { configStore.load(tunnel.name) })!!
|
||||
}
|
||||
|
||||
fun onCreate() {
|
||||
getAsyncWorker().supplyAsync { configStore.enumerate() }
|
||||
.thenAcceptBoth(getAsyncWorker().supplyAsync { getBackend().runningTunnelNames }, this::onTunnelsLoaded)
|
||||
.whenComplete(ExceptionLoggers.E)
|
||||
applicationScope.launch {
|
||||
try {
|
||||
onTunnelsLoaded(withContext(Dispatchers.IO) { configStore.enumerate() }, withContext(Dispatchers.IO) { getBackend().runningTunnelNames })
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTunnelsLoaded(present: Iterable<String>, running: Collection<String>) {
|
||||
for (name in present)
|
||||
addToList(name, null, if (running.contains(name)) Tunnel.State.UP else Tunnel.State.DOWN)
|
||||
val lastUsedName = getSharedPreferences().getString(KEY_LAST_USED_TUNNEL, null)
|
||||
if (lastUsedName != null)
|
||||
lastUsedTunnel = tunnelMap[lastUsedName]
|
||||
var toComplete: Array<CompletableFuture<Void>>
|
||||
synchronized(delayedLoadRestoreTunnels) {
|
||||
applicationScope.launch {
|
||||
val lastUsedName = UserKnobs.lastUsedTunnel.first()
|
||||
if (lastUsedName != null)
|
||||
lastUsedTunnel = tunnelMap[lastUsedName]
|
||||
haveLoaded = true
|
||||
toComplete = delayedLoadRestoreTunnels.toTypedArray()
|
||||
delayedLoadRestoreTunnels.clear()
|
||||
restoreState(true)
|
||||
tunnels.complete(tunnelMap)
|
||||
}
|
||||
restoreState(true).whenComplete { v: Void?, t: Throwable? ->
|
||||
for (f in toComplete) {
|
||||
if (t == null)
|
||||
f.complete(v)
|
||||
else
|
||||
f.completeExceptionally(t)
|
||||
}
|
||||
|
||||
private fun refreshTunnelStates() {
|
||||
applicationScope.launch {
|
||||
try {
|
||||
val running = withContext(Dispatchers.IO) { getBackend().runningTunnelNames }
|
||||
for (tunnel in tunnelMap)
|
||||
tunnel.onStateChanged(if (running.contains(tunnel.name)) Tunnel.State.UP else Tunnel.State.DOWN)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
}
|
||||
tunnels.complete(tunnelMap)
|
||||
}
|
||||
|
||||
fun refreshTunnelStates() {
|
||||
getAsyncWorker().supplyAsync { getBackend().runningTunnelNames }
|
||||
.thenAccept { running: Set<String> -> for (tunnel in tunnelMap) tunnel.onStateChanged(if (running.contains(tunnel.name)) Tunnel.State.UP else Tunnel.State.DOWN) }
|
||||
.whenComplete(ExceptionLoggers.E)
|
||||
}
|
||||
|
||||
fun restoreState(force: Boolean): CompletionStage<Void> {
|
||||
if (!force && !getSharedPreferences().getBoolean(KEY_RESTORE_ON_BOOT, false))
|
||||
return CompletableFuture.completedFuture(null)
|
||||
synchronized(delayedLoadRestoreTunnels) {
|
||||
if (!haveLoaded) {
|
||||
val f = CompletableFuture<Void>()
|
||||
delayedLoadRestoreTunnels.add(f)
|
||||
return f
|
||||
suspend fun restoreState(force: Boolean) {
|
||||
if (!haveLoaded || (!force && !UserKnobs.restoreOnBoot.first()))
|
||||
return
|
||||
val previouslyRunning = UserKnobs.runningTunnels.first()
|
||||
if (previouslyRunning.isEmpty()) return
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
tunnelMap.filter { previouslyRunning.contains(it.name) }.map { async(Dispatchers.IO + SupervisorJob()) { setTunnelState(it, Tunnel.State.UP) } }.awaitAll()
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
}
|
||||
val previouslyRunning = getSharedPreferences().getStringSet(KEY_RUNNING_TUNNELS, null)
|
||||
?: return CompletableFuture.completedFuture(null)
|
||||
return CompletableFuture.allOf(*tunnelMap.filter { previouslyRunning.contains(it.name) }.map { setTunnelState(it, Tunnel.State.UP).toCompletableFuture() }.toTypedArray())
|
||||
}
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
fun saveState() {
|
||||
getSharedPreferences().edit().putStringSet(KEY_RUNNING_TUNNELS, tunnelMap.filter { it.state == Tunnel.State.UP }.map { it.name }.toSet()).commit()
|
||||
suspend fun saveState() {
|
||||
UserKnobs.setRunningTunnels(tunnelMap.filter { it.state == Tunnel.State.UP }.map { it.name }.toSet())
|
||||
}
|
||||
|
||||
fun setTunnelConfig(tunnel: ObservableTunnel, config: Config): CompletionStage<Config> = getAsyncWorker().supplyAsync {
|
||||
getBackend().setState(tunnel, tunnel.state, config)
|
||||
configStore.save(tunnel.name, config)
|
||||
}.thenApply { tunnel.onConfigChanged(it) }
|
||||
suspend fun setTunnelConfig(tunnel: ObservableTunnel, config: Config): Config = withContext(Dispatchers.Main.immediate) {
|
||||
tunnel.onConfigChanged(withContext(Dispatchers.IO) {
|
||||
getBackend().setState(tunnel, tunnel.state, config)
|
||||
configStore.save(tunnel.name, config)
|
||||
})!!
|
||||
}
|
||||
|
||||
fun setTunnelName(tunnel: ObservableTunnel, name: String): CompletionStage<String> {
|
||||
suspend fun setTunnelName(tunnel: ObservableTunnel, name: String): String = withContext(Dispatchers.Main.immediate) {
|
||||
if (Tunnel.isNameInvalid(name))
|
||||
return CompletableFuture.failedFuture(IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)))
|
||||
throw IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name))
|
||||
if (tunnelMap.containsKey(name)) {
|
||||
return CompletableFuture.failedFuture(IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name)))
|
||||
throw IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name))
|
||||
}
|
||||
val originalState = tunnel.state
|
||||
val wasLastUsed = tunnel == lastUsedTunnel
|
||||
@@ -168,69 +168,81 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
|
||||
if (wasLastUsed)
|
||||
lastUsedTunnel = null
|
||||
tunnelMap.remove(tunnel)
|
||||
return getAsyncWorker().supplyAsync {
|
||||
var throwable: Throwable? = null
|
||||
var newName: String? = null
|
||||
try {
|
||||
if (originalState == Tunnel.State.UP)
|
||||
getBackend().setState(tunnel, Tunnel.State.DOWN, null)
|
||||
configStore.rename(tunnel.name, name)
|
||||
val newName = tunnel.onNameChanged(name)
|
||||
withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.DOWN, null) }
|
||||
withContext(Dispatchers.IO) { configStore.rename(tunnel.name, name) }
|
||||
newName = tunnel.onNameChanged(name)
|
||||
if (originalState == Tunnel.State.UP)
|
||||
getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config)
|
||||
newName
|
||||
}.whenComplete { _, e ->
|
||||
withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config) }
|
||||
} catch (e: Throwable) {
|
||||
throwable = e
|
||||
// On failure, we don't know what state the tunnel might be in. Fix that.
|
||||
if (e != null)
|
||||
getTunnelState(tunnel)
|
||||
// Add the tunnel back to the manager, under whatever name it thinks it has.
|
||||
tunnelMap.add(tunnel)
|
||||
if (wasLastUsed)
|
||||
lastUsedTunnel = tunnel
|
||||
getTunnelState(tunnel)
|
||||
}
|
||||
// Add the tunnel back to the manager, under whatever name it thinks it has.
|
||||
tunnelMap.add(tunnel)
|
||||
if (wasLastUsed)
|
||||
lastUsedTunnel = tunnel
|
||||
if (throwable != null)
|
||||
throw throwable
|
||||
newName!!
|
||||
}
|
||||
|
||||
fun setTunnelState(tunnel: ObservableTunnel, state: Tunnel.State): CompletionStage<Tunnel.State> = tunnel.configAsync
|
||||
.thenCompose { getAsyncWorker().supplyAsync { getBackend().setState(tunnel, state, it) } }
|
||||
.whenComplete { newState, e ->
|
||||
// Ensure onStateChanged is always called (failure or not), and with the correct state.
|
||||
tunnel.onStateChanged(if (e == null) newState else tunnel.state)
|
||||
if (e == null && newState == Tunnel.State.UP)
|
||||
lastUsedTunnel = tunnel
|
||||
saveState()
|
||||
}
|
||||
suspend fun setTunnelState(tunnel: ObservableTunnel, state: Tunnel.State): Tunnel.State = withContext(Dispatchers.Main.immediate) {
|
||||
var newState = tunnel.state
|
||||
var throwable: Throwable? = null
|
||||
try {
|
||||
newState = withContext(Dispatchers.IO) { getBackend().setState(tunnel, state, tunnel.getConfigAsync()) }
|
||||
if (newState == Tunnel.State.UP)
|
||||
lastUsedTunnel = tunnel
|
||||
} catch (e: Throwable) {
|
||||
throwable = e
|
||||
}
|
||||
tunnel.onStateChanged(newState)
|
||||
saveState()
|
||||
if (throwable != null)
|
||||
throw throwable
|
||||
newState
|
||||
}
|
||||
|
||||
class IntentReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
val manager = getTunnelManager()
|
||||
if (intent == null) return
|
||||
val action = intent.action ?: return
|
||||
if ("com.wireguard.android.action.REFRESH_TUNNEL_STATES" == action) {
|
||||
manager.refreshTunnelStates()
|
||||
return
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || !getSharedPreferences().getBoolean("allow_remote_control_intents", false))
|
||||
return
|
||||
val state: Tunnel.State
|
||||
state = when (action) {
|
||||
"com.wireguard.android.action.SET_TUNNEL_UP" -> Tunnel.State.UP
|
||||
"com.wireguard.android.action.SET_TUNNEL_DOWN" -> Tunnel.State.DOWN
|
||||
else -> return
|
||||
}
|
||||
val tunnelName = intent.getStringExtra("tunnel") ?: return
|
||||
manager.tunnels.thenAccept {
|
||||
val tunnel = it[tunnelName] ?: return@thenAccept
|
||||
applicationScope.launch {
|
||||
val manager = getTunnelManager()
|
||||
if (intent == null) return@launch
|
||||
val action = intent.action ?: return@launch
|
||||
if ("com.wireguard.android.action.REFRESH_TUNNEL_STATES" == action) {
|
||||
manager.refreshTunnelStates()
|
||||
return@launch
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || !UserKnobs.allowRemoteControlIntents.first())
|
||||
return@launch
|
||||
val state: Tunnel.State
|
||||
state = when (action) {
|
||||
"com.wireguard.android.action.SET_TUNNEL_UP" -> Tunnel.State.UP
|
||||
"com.wireguard.android.action.SET_TUNNEL_DOWN" -> Tunnel.State.DOWN
|
||||
else -> return@launch
|
||||
}
|
||||
val tunnelName = intent.getStringExtra("tunnel") ?: return@launch
|
||||
val tunnels = manager.getTunnels()
|
||||
val tunnel = tunnels[tunnelName] ?: return@launch
|
||||
manager.setTunnelState(tunnel, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getTunnelState(tunnel: ObservableTunnel): CompletionStage<Tunnel.State> = getAsyncWorker()
|
||||
.supplyAsync { getBackend().getState(tunnel) }.thenApply(tunnel::onStateChanged)
|
||||
suspend fun getTunnelState(tunnel: ObservableTunnel): Tunnel.State = withContext(Dispatchers.Main.immediate) {
|
||||
tunnel.onStateChanged(withContext(Dispatchers.IO) { getBackend().getState(tunnel) })
|
||||
}
|
||||
|
||||
fun getTunnelStatistics(tunnel: ObservableTunnel): CompletionStage<Statistics> = getAsyncWorker()
|
||||
.supplyAsync { getBackend().getStatistics(tunnel) }.thenApply(tunnel::onStatisticsChanged)
|
||||
suspend fun getTunnelStatistics(tunnel: ObservableTunnel): Statistics = withContext(Dispatchers.Main.immediate) {
|
||||
tunnel.onStatisticsChanged(withContext(Dispatchers.IO) { getBackend().getStatistics(tunnel) })!!
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_LAST_USED_TUNNEL = "last_used_tunnel"
|
||||
private const val KEY_RESTORE_ON_BOOT = "restore_on_boot"
|
||||
private const val KEY_RUNNING_TUNNELS = "enabled_configs"
|
||||
private const val TAG = "WireGuard/TunnelManager"
|
||||
}
|
||||
}
|
||||
|
||||
+48
-25
@@ -4,46 +4,63 @@
|
||||
*/
|
||||
package com.wireguard.android.preference
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import androidx.preference.Preference
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.activity.SettingsActivity
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import java9.util.concurrent.CompletableFuture
|
||||
import com.wireguard.android.util.UserKnobs
|
||||
import com.wireguard.android.util.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class KernelModuleDisablerPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
||||
private var state = if (Application.getBackend() is WgQuickBackend) State.ENABLED else State.DISABLED
|
||||
private var state = State.UNKNOWN
|
||||
|
||||
override fun getSummary() = context.getString(state.summaryResourceId)
|
||||
|
||||
override fun getTitle() = context.getString(state.titleResourceId)
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
override fun onClick() {
|
||||
if (state == State.DISABLED) {
|
||||
setState(State.ENABLING)
|
||||
Application.getSharedPreferences().edit().putBoolean("disable_kernel_module", false).commit()
|
||||
} else if (state == State.ENABLED) {
|
||||
setState(State.DISABLING)
|
||||
Application.getSharedPreferences().edit().putBoolean("disable_kernel_module", true).commit()
|
||||
init {
|
||||
isVisible = false
|
||||
lifecycleScope.launch {
|
||||
setState(if (Application.getBackend() is WgQuickBackend) State.ENABLED else State.DISABLED)
|
||||
}
|
||||
Application.getAsyncWorker().runAsync {
|
||||
Application.getTunnelManager().tunnels.thenApply { observableTunnels ->
|
||||
val downings = observableTunnels.map { it.setStateAsync(Tunnel.State.DOWN).toCompletableFuture() }.toTypedArray()
|
||||
CompletableFuture.allOf(*downings).thenRun {
|
||||
}
|
||||
|
||||
override fun getSummary() = if (state == State.UNKNOWN) "" else context.getString(state.summaryResourceId)
|
||||
|
||||
override fun getTitle() = if (state == State.UNKNOWN) "" else context.getString(state.titleResourceId)
|
||||
|
||||
override fun onClick() {
|
||||
lifecycleScope.launch {
|
||||
if (state == State.DISABLED) {
|
||||
setState(State.ENABLING)
|
||||
UserKnobs.setDisableKernelModule(false)
|
||||
} else if (state == State.ENABLED) {
|
||||
setState(State.DISABLING)
|
||||
UserKnobs.setDisableKernelModule(true)
|
||||
}
|
||||
val observableTunnels = Application.getTunnelManager().getTunnels()
|
||||
val downings = observableTunnels.map { async(SupervisorJob()) { it.setStateAsync(Tunnel.State.DOWN) } }
|
||||
try {
|
||||
downings.awaitAll()
|
||||
withContext(Dispatchers.IO) {
|
||||
val restartIntent = Intent(context, SettingsActivity::class.java)
|
||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
Application.get().startActivity(restartIntent)
|
||||
exitProcess(0)
|
||||
}
|
||||
}.join()
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,13 +68,19 @@ class KernelModuleDisablerPreference(context: Context, attrs: AttributeSet?) : P
|
||||
if (this.state == state) return
|
||||
this.state = state
|
||||
if (isEnabled != state.shouldEnableView) isEnabled = state.shouldEnableView
|
||||
if (isVisible != state.visible) isVisible = state.visible
|
||||
notifyChanged()
|
||||
}
|
||||
|
||||
private enum class State(val titleResourceId: Int, val summaryResourceId: Int, val shouldEnableView: Boolean) {
|
||||
ENABLED(R.string.module_disabler_enabled_title, R.string.module_disabler_enabled_summary, true),
|
||||
DISABLED(R.string.module_disabler_disabled_title, R.string.module_disabler_disabled_summary, true),
|
||||
ENABLING(R.string.module_disabler_disabled_title, R.string.success_application_will_restart, false),
|
||||
DISABLING(R.string.module_disabler_enabled_title, R.string.success_application_will_restart, false);
|
||||
private enum class State(val titleResourceId: Int, val summaryResourceId: Int, val shouldEnableView: Boolean, val visible: Boolean) {
|
||||
UNKNOWN(0, 0, false, false),
|
||||
ENABLED(R.string.module_disabler_enabled_title, R.string.module_disabler_enabled_summary, true, true),
|
||||
DISABLED(R.string.module_disabler_disabled_title, R.string.module_disabler_disabled_summary, true, true),
|
||||
ENABLING(R.string.module_disabler_disabled_title, R.string.success_application_will_restart, false, true),
|
||||
DISABLING(R.string.module_disabler_enabled_title, R.string.success_application_will_restart, false, true);
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "WireGuard/KernelModuleDisablerPreference"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
package com.wireguard.android.preference
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.system.OsConstants
|
||||
@@ -15,40 +14,42 @@ import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.activity.SettingsActivity
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import com.wireguard.android.util.UserKnobs
|
||||
import com.wireguard.android.util.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class ModuleDownloaderPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
||||
private var state = State.INITIAL
|
||||
|
||||
override fun getSummary() = context.getString(state.messageResourceId)
|
||||
|
||||
override fun getTitle() = context.getString(R.string.module_installer_title)
|
||||
|
||||
override fun onClick() {
|
||||
setState(State.WORKING)
|
||||
Application.getAsyncWorker().supplyAsync(Application.getModuleLoader()::download).whenComplete(this::onDownloadResult)
|
||||
}
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
private fun onDownloadResult(result: Int, throwable: Throwable?) {
|
||||
when {
|
||||
throwable != null -> {
|
||||
setState(State.FAILURE)
|
||||
Toast.makeText(context, ErrorMessages[throwable], Toast.LENGTH_LONG).show()
|
||||
}
|
||||
result == OsConstants.ENOENT -> setState(State.NOTFOUND)
|
||||
result == OsConstants.EXIT_SUCCESS -> {
|
||||
setState(State.SUCCESS)
|
||||
Application.getSharedPreferences().edit().remove("disable_kernel_module").commit()
|
||||
Application.getAsyncWorker().runAsync {
|
||||
val restartIntent = Intent(context, SettingsActivity::class.java)
|
||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
Application.get().startActivity(restartIntent)
|
||||
exitProcess(0)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
when (withContext(Dispatchers.IO) { Application.getModuleLoader().download() }) {
|
||||
OsConstants.ENOENT -> setState(State.NOTFOUND)
|
||||
OsConstants.EXIT_SUCCESS -> {
|
||||
setState(State.SUCCESS)
|
||||
UserKnobs.setDisableKernelModule(null)
|
||||
withContext(Dispatchers.IO) {
|
||||
val restartIntent = Intent(context, SettingsActivity::class.java)
|
||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
Application.get().startActivity(restartIntent)
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
||||
else -> setState(State.FAILURE)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
setState(State.FAILURE)
|
||||
Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_LONG).show()
|
||||
}
|
||||
else -> setState(State.FAILURE)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.preference
|
||||
|
||||
import androidx.datastore.DataStore
|
||||
import androidx.datastore.preferences.Preferences
|
||||
import androidx.datastore.preferences.edit
|
||||
import androidx.datastore.preferences.preferencesKey
|
||||
import androidx.datastore.preferences.preferencesSetKey
|
||||
import androidx.datastore.preferences.remove
|
||||
import androidx.preference.PreferenceDataStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class PreferencesPreferenceDataStore(private val coroutineScope: CoroutineScope, private val dataStore: DataStore<Preferences>) : PreferenceDataStore() {
|
||||
override fun putString(key: String?, value: String?) {
|
||||
if (key == null) return
|
||||
val pk = preferencesKey<String>(key)
|
||||
coroutineScope.launch {
|
||||
dataStore.edit {
|
||||
if (value == null) it.remove(pk)
|
||||
else it[pk] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun putStringSet(key: String?, values: Set<String?>?) {
|
||||
if (key == null) return
|
||||
val pk = preferencesSetKey<String>(key)
|
||||
val filteredValues = values?.filterNotNull()?.toSet()
|
||||
coroutineScope.launch {
|
||||
dataStore.edit {
|
||||
if (filteredValues == null || filteredValues.isEmpty()) it.remove(pk)
|
||||
else it[pk] = filteredValues
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun putInt(key: String?, value: Int) {
|
||||
if (key == null) return
|
||||
val pk = preferencesKey<Int>(key)
|
||||
coroutineScope.launch {
|
||||
dataStore.edit {
|
||||
it[pk] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun putLong(key: String?, value: Long) {
|
||||
if (key == null) return
|
||||
val pk = preferencesKey<Long>(key)
|
||||
coroutineScope.launch {
|
||||
dataStore.edit {
|
||||
it[pk] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun putFloat(key: String?, value: Float) {
|
||||
if (key == null) return
|
||||
val pk = preferencesKey<Float>(key)
|
||||
coroutineScope.launch {
|
||||
dataStore.edit {
|
||||
it[pk] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun putBoolean(key: String?, value: Boolean) {
|
||||
if (key == null) return
|
||||
val pk = preferencesKey<Boolean>(key)
|
||||
coroutineScope.launch {
|
||||
dataStore.edit {
|
||||
it[pk] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getString(key: String?, defValue: String?): String? {
|
||||
if (key == null) return defValue
|
||||
val pk = preferencesKey<String>(key)
|
||||
return runBlocking {
|
||||
dataStore.data.map { it[pk] ?: defValue }.first()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getStringSet(key: String?, defValues: Set<String?>?): Set<String?>? {
|
||||
if (key == null) return defValues
|
||||
val pk = preferencesSetKey<String>(key)
|
||||
return runBlocking {
|
||||
dataStore.data.map { it[pk] ?: defValues }.first()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getInt(key: String?, defValue: Int): Int {
|
||||
if (key == null) return defValue
|
||||
val pk = preferencesKey<Int>(key)
|
||||
return runBlocking {
|
||||
dataStore.data.map { it[pk] ?: defValue }.first()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLong(key: String?, defValue: Long): Long {
|
||||
if (key == null) return defValue
|
||||
val pk = preferencesKey<Long>(key)
|
||||
return runBlocking {
|
||||
dataStore.data.map { it[pk] ?: defValue }.first()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFloat(key: String?, defValue: Float): Float {
|
||||
if (key == null) return defValue
|
||||
val pk = preferencesKey<Float>(key)
|
||||
return runBlocking {
|
||||
dataStore.data.map { it[pk] ?: defValue }.first()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
|
||||
if (key == null) return defValue
|
||||
val pk = preferencesKey<Boolean>(key)
|
||||
return runBlocking {
|
||||
dataStore.data.map { it[pk] ?: defValue }.first()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,10 @@ import androidx.preference.Preference
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.util.ToolsInstaller
|
||||
import com.wireguard.android.util.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Preference implementing a button that asynchronously runs `ToolsInstaller` and displays the
|
||||
@@ -17,37 +21,41 @@ import com.wireguard.android.util.ToolsInstaller
|
||||
*/
|
||||
class ToolsInstallerPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
||||
private var state = State.INITIAL
|
||||
|
||||
override fun getSummary() = context.getString(state.messageResourceId)
|
||||
|
||||
override fun getTitle() = context.getString(R.string.tools_installer_title)
|
||||
|
||||
override fun onAttached() {
|
||||
super.onAttached()
|
||||
Application.getAsyncWorker().supplyAsync(Application.getToolsInstaller()::areInstalled).whenComplete(this::onCheckResult)
|
||||
}
|
||||
|
||||
private fun onCheckResult(state: Int, throwable: Throwable?) {
|
||||
when {
|
||||
throwable != null || state == ToolsInstaller.ERROR -> setState(State.INITIAL)
|
||||
state and ToolsInstaller.YES == ToolsInstaller.YES -> setState(State.ALREADY)
|
||||
state and (ToolsInstaller.MAGISK or ToolsInstaller.NO) == ToolsInstaller.MAGISK or ToolsInstaller.NO -> setState(State.INITIAL_MAGISK)
|
||||
state and (ToolsInstaller.SYSTEM or ToolsInstaller.NO) == ToolsInstaller.SYSTEM or ToolsInstaller.NO -> setState(State.INITIAL_SYSTEM)
|
||||
else -> setState(State.INITIAL)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val state = withContext(Dispatchers.IO) { Application.getToolsInstaller().areInstalled() }
|
||||
when {
|
||||
state == ToolsInstaller.ERROR -> setState(State.INITIAL)
|
||||
state and ToolsInstaller.YES == ToolsInstaller.YES -> setState(State.ALREADY)
|
||||
state and (ToolsInstaller.MAGISK or ToolsInstaller.NO) == ToolsInstaller.MAGISK or ToolsInstaller.NO -> setState(State.INITIAL_MAGISK)
|
||||
state and (ToolsInstaller.SYSTEM or ToolsInstaller.NO) == ToolsInstaller.SYSTEM or ToolsInstaller.NO -> setState(State.INITIAL_SYSTEM)
|
||||
else -> setState(State.INITIAL)
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
setState(State.INITIAL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
setState(State.WORKING)
|
||||
Application.getAsyncWorker().supplyAsync { Application.getToolsInstaller().install() }.whenComplete { result: Int, throwable: Throwable? -> onInstallResult(result, throwable) }
|
||||
}
|
||||
|
||||
private fun onInstallResult(result: Int, throwable: Throwable?) {
|
||||
when {
|
||||
throwable != null -> setState(State.FAILURE)
|
||||
result and (ToolsInstaller.YES or ToolsInstaller.MAGISK) == ToolsInstaller.YES or ToolsInstaller.MAGISK -> setState(State.SUCCESS_MAGISK)
|
||||
result and (ToolsInstaller.YES or ToolsInstaller.SYSTEM) == ToolsInstaller.YES or ToolsInstaller.SYSTEM -> setState(State.SUCCESS_SYSTEM)
|
||||
else -> setState(State.FAILURE)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val result = withContext(Dispatchers.IO) { Application.getToolsInstaller().install() }
|
||||
when {
|
||||
result and (ToolsInstaller.YES or ToolsInstaller.MAGISK) == ToolsInstaller.YES or ToolsInstaller.MAGISK -> setState(State.SUCCESS_MAGISK)
|
||||
result and (ToolsInstaller.YES or ToolsInstaller.SYSTEM) == ToolsInstaller.YES or ToolsInstaller.SYSTEM -> setState(State.SUCCESS_SYSTEM)
|
||||
else -> setState(State.FAILURE)
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
setState(State.FAILURE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ import com.wireguard.android.R
|
||||
import com.wireguard.android.backend.Backend
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.wireguard.android.util.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Locale
|
||||
|
||||
class VersionPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
||||
@@ -43,15 +47,16 @@ class VersionPreference(context: Context, attrs: AttributeSet?) : Preference(con
|
||||
}
|
||||
|
||||
init {
|
||||
Application.getBackendAsync().thenAccept { backend ->
|
||||
lifecycleScope.launch {
|
||||
val backend = Application.getBackend()
|
||||
versionSummary = getContext().getString(R.string.version_summary_checking, getBackendPrettyName(context, backend).toLowerCase(Locale.ENGLISH))
|
||||
Application.getAsyncWorker().supplyAsync(backend::getVersion).whenComplete { version, exception ->
|
||||
versionSummary = if (exception == null)
|
||||
getContext().getString(R.string.version_summary, getBackendPrettyName(context, backend), version)
|
||||
else
|
||||
getContext().getString(R.string.version_summary_unknown, getBackendPrettyName(context, backend).toLowerCase(Locale.ENGLISH))
|
||||
notifyChanged()
|
||||
notifyChanged()
|
||||
versionSummary = try {
|
||||
getContext().getString(R.string.version_summary, getBackendPrettyName(context, backend), withContext(Dispatchers.IO) { backend.version })
|
||||
} catch (_: Throwable) {
|
||||
getContext().getString(R.string.version_summary_unknown, getBackendPrettyName(context, backend).toLowerCase(Locale.ENGLISH))
|
||||
}
|
||||
notifyChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,21 +4,25 @@
|
||||
*/
|
||||
package com.wireguard.android.preference
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import androidx.preference.Preference
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.model.ObservableTunnel
|
||||
import com.wireguard.android.util.AdminKnobs
|
||||
import com.wireguard.android.util.BiometricAuthenticator
|
||||
import com.wireguard.android.util.DownloadsFileSaver
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import com.wireguard.android.util.FragmentUtils
|
||||
import java9.util.concurrent.CompletableFuture
|
||||
import com.wireguard.android.util.activity
|
||||
import com.wireguard.android.util.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
@@ -28,52 +32,46 @@ import java.util.zip.ZipOutputStream
|
||||
*/
|
||||
class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
||||
private var exportedFilePath: String? = null
|
||||
|
||||
private fun exportZip() {
|
||||
Application.getTunnelManager().tunnels.thenAccept(this::exportZip)
|
||||
}
|
||||
|
||||
private fun exportZip(tunnels: List<ObservableTunnel>) {
|
||||
val futureConfigs = tunnels.map { it.configAsync.toCompletableFuture() }.toTypedArray()
|
||||
if (futureConfigs.isEmpty()) {
|
||||
exportZipComplete(null, IllegalArgumentException(
|
||||
context.getString(R.string.no_tunnels_error)))
|
||||
return
|
||||
}
|
||||
CompletableFuture.allOf(*futureConfigs)
|
||||
.whenComplete { _, exception ->
|
||||
Application.getAsyncWorker().supplyAsync {
|
||||
if (exception != null) throw exception
|
||||
val outputFile = DownloadsFileSaver.save(context, "wireguard-export.zip", "application/zip", true)
|
||||
try {
|
||||
ZipOutputStream(outputFile.outputStream).use { zip ->
|
||||
for (i in futureConfigs.indices) {
|
||||
zip.putNextEntry(ZipEntry(tunnels[i].name + ".conf"))
|
||||
zip.write(futureConfigs[i].getNow(null)!!.toWgQuickString().toByteArray(StandardCharsets.UTF_8))
|
||||
}
|
||||
zip.closeEntry()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
outputFile.delete()
|
||||
throw e
|
||||
lifecycleScope.launch {
|
||||
val tunnels = Application.getTunnelManager().getTunnels()
|
||||
try {
|
||||
exportedFilePath = withContext(Dispatchers.IO) {
|
||||
val configs = tunnels.map { async(SupervisorJob()) { it.getConfigAsync() } }.awaitAll()
|
||||
if (configs.isEmpty()) {
|
||||
throw IllegalArgumentException(context.getString(R.string.no_tunnels_error))
|
||||
}
|
||||
val outputFile = DownloadsFileSaver.save(activity, "wireguard-export.zip", "application/zip", true)
|
||||
if (outputFile == null) {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
isEnabled = true
|
||||
}
|
||||
outputFile.fileName
|
||||
}.whenComplete(this::exportZipComplete)
|
||||
return@withContext null
|
||||
}
|
||||
try {
|
||||
ZipOutputStream(outputFile.outputStream).use { zip ->
|
||||
for (i in configs.indices) {
|
||||
zip.putNextEntry(ZipEntry(tunnels[i].name + ".conf"))
|
||||
zip.write(configs[i].toWgQuickString().toByteArray(StandardCharsets.UTF_8))
|
||||
}
|
||||
zip.closeEntry()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
outputFile.delete()
|
||||
throw e
|
||||
}
|
||||
outputFile.fileName
|
||||
}
|
||||
}
|
||||
|
||||
private fun exportZipComplete(filePath: String?, throwable: Throwable?) {
|
||||
if (throwable != null) {
|
||||
val error = ErrorMessages[throwable]
|
||||
val message = context.getString(R.string.zip_export_error, error)
|
||||
Log.e(TAG, message, throwable)
|
||||
Snackbar.make(
|
||||
FragmentUtils.getPrefActivity(this).findViewById(android.R.id.content),
|
||||
message, Snackbar.LENGTH_LONG).show()
|
||||
isEnabled = true
|
||||
} else {
|
||||
exportedFilePath = filePath
|
||||
notifyChanged()
|
||||
notifyChanged()
|
||||
} catch (e: Throwable) {
|
||||
val error = ErrorMessages[e]
|
||||
val message = context.getString(R.string.zip_export_error, error)
|
||||
Log.e(TAG, message, e)
|
||||
Snackbar.make(
|
||||
activity.findViewById(android.R.id.content),
|
||||
message, Snackbar.LENGTH_LONG).show()
|
||||
isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,22 +80,18 @@ class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference
|
||||
override fun getTitle() = context.getString(R.string.zip_export_title)
|
||||
|
||||
override fun onClick() {
|
||||
val prefActivity = FragmentUtils.getPrefActivity(this)
|
||||
val fragment = prefActivity.supportFragmentManager.fragments.first()
|
||||
if (AdminKnobs.disableConfigExport) return
|
||||
val fragment = activity.supportFragmentManager.fragments.first()
|
||||
BiometricAuthenticator.authenticate(R.string.biometric_prompt_zip_exporter_title, fragment) {
|
||||
when (it) {
|
||||
// When we have successful authentication, or when there is no biometric hardware available.
|
||||
is BiometricAuthenticator.Result.Success, is BiometricAuthenticator.Result.HardwareUnavailableOrDisabled -> {
|
||||
prefActivity.ensurePermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, grantResults ->
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
isEnabled = false
|
||||
exportZip()
|
||||
}
|
||||
}
|
||||
isEnabled = false
|
||||
exportZip()
|
||||
}
|
||||
is BiometricAuthenticator.Result.Failure -> {
|
||||
Snackbar.make(
|
||||
prefActivity.findViewById(android.R.id.content),
|
||||
activity.findViewById(android.R.id.content),
|
||||
it.message,
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util
|
||||
|
||||
import android.content.RestrictionsManager
|
||||
import androidx.core.content.getSystemService
|
||||
import com.wireguard.android.Application
|
||||
|
||||
object AdminKnobs {
|
||||
private val restrictions: RestrictionsManager? = Application.get().getSystemService()
|
||||
val disableConfigExport: Boolean
|
||||
get() = restrictions?.applicationRestrictions?.getBoolean("disable_config_export", false)
|
||||
?: false
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.util
|
||||
|
||||
import android.os.Handler
|
||||
import java9.util.concurrent.CompletableFuture
|
||||
import java9.util.concurrent.CompletionStage
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
/**
|
||||
* Helper class for running asynchronous tasks and ensuring they are completed on the main thread.
|
||||
*/
|
||||
|
||||
class AsyncWorker(private val executor: Executor, private val handler: Handler) {
|
||||
|
||||
fun runAsync(run: () -> Unit): CompletionStage<Void> {
|
||||
val future = CompletableFuture<Void>()
|
||||
executor.execute {
|
||||
try {
|
||||
run()
|
||||
handler.post { future.complete(null) }
|
||||
} catch (t: Throwable) {
|
||||
handler.post { future.completeExceptionally(t) }
|
||||
}
|
||||
}
|
||||
return future
|
||||
}
|
||||
|
||||
fun <T> supplyAsync(get: () -> T?): CompletionStage<T> {
|
||||
val future = CompletableFuture<T>()
|
||||
executor.execute {
|
||||
try {
|
||||
val result = get()
|
||||
handler.post { future.complete(result) }
|
||||
} catch (t: Throwable) {
|
||||
handler.post { future.completeExceptionally(t) }
|
||||
}
|
||||
}
|
||||
return future
|
||||
}
|
||||
}
|
||||
@@ -5,24 +5,24 @@
|
||||
|
||||
package com.wireguard.android.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.biometric.BiometricConstants
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricManager.Authenticators
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.wireguard.android.R
|
||||
|
||||
|
||||
object BiometricAuthenticator {
|
||||
private const val TAG = "WireGuard/BiometricAuthenticator"
|
||||
private val handler = Handler()
|
||||
|
||||
// Not all devices support strong biometric auth so we're allowing both device credentials as
|
||||
// well as weak biometrics.
|
||||
private const val allowedAuthenticators = Authenticators.DEVICE_CREDENTIAL or Authenticators.BIOMETRIC_WEAK
|
||||
|
||||
sealed class Result {
|
||||
data class Success(val cryptoObject: BiometricPrompt.CryptoObject?) : Result()
|
||||
@@ -31,20 +31,6 @@ object BiometricAuthenticator {
|
||||
object Cancelled : Result()
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
private fun isPinEnabled(context: Context): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
||||
return context.getSystemService<KeyguardManager>()!!.isDeviceSecure
|
||||
return try {
|
||||
val lockUtilsClass = Class.forName("com.android.internal.widget.LockPatternUtils")
|
||||
val lockUtils = lockUtilsClass.getConstructor(Context::class.java).newInstance(context)
|
||||
val method = lockUtilsClass.getMethod("isLockScreenDisabled")
|
||||
!(method.invoke(lockUtils) as Boolean)
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun authenticate(
|
||||
@StringRes dialogTitleRes: Int,
|
||||
fragment: Fragment,
|
||||
@@ -77,12 +63,12 @@ object BiometricAuthenticator {
|
||||
callback(Result.Success(result.cryptoObject))
|
||||
}
|
||||
}
|
||||
val biometricPrompt = BiometricPrompt(fragment, { handler.post(it) }, authCallback)
|
||||
val biometricPrompt = BiometricPrompt(fragment, { Handler(Looper.getMainLooper()).post(it) }, authCallback)
|
||||
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(fragment.getString(dialogTitleRes))
|
||||
.setDeviceCredentialAllowed(true)
|
||||
.setAllowedAuthenticators(allowedAuthenticators)
|
||||
.build()
|
||||
if (BiometricManager.from(fragment.requireContext()).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS || isPinEnabled(fragment.requireContext())) {
|
||||
if (BiometricManager.from(fragment.requireContext()).canAuthenticate(allowedAuthenticators) == BiometricManager.BIOMETRIC_SUCCESS) {
|
||||
biometricPrompt.authenticate(promptInfo)
|
||||
} else {
|
||||
callback(Result.HardwareUnavailableOrDisabled)
|
||||
|
||||
@@ -4,14 +4,22 @@
|
||||
*/
|
||||
package com.wireguard.android.util
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.provider.MediaStore.MediaColumns
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.wireguard.android.R
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
@@ -19,50 +27,65 @@ import java.io.OutputStream
|
||||
|
||||
object DownloadsFileSaver {
|
||||
@Throws(Exception::class)
|
||||
fun save(context: Context, name: String, mimeType: String?, overwriteExisting: Boolean) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val contentResolver = context.contentResolver
|
||||
if (overwriteExisting)
|
||||
contentResolver.delete(MediaStore.Downloads.EXTERNAL_CONTENT_URI, String.format("%s = ?", MediaColumns.DISPLAY_NAME), arrayOf(name))
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(MediaColumns.DISPLAY_NAME, name)
|
||||
contentValues.put(MediaColumns.MIME_TYPE, mimeType)
|
||||
val contentUri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
||||
?: throw IOException(context.getString(R.string.create_downloads_file_error))
|
||||
val contentStream = contentResolver.openOutputStream(contentUri)
|
||||
?: throw IOException(context.getString(R.string.create_downloads_file_error))
|
||||
@Suppress("DEPRECATION") var cursor = contentResolver.query(contentUri, arrayOf(MediaColumns.DATA), null, null, null)
|
||||
var path: String? = null
|
||||
if (cursor != null) {
|
||||
try {
|
||||
if (cursor.moveToFirst())
|
||||
path = cursor.getString(0)
|
||||
} finally {
|
||||
cursor.close()
|
||||
}
|
||||
}
|
||||
if (path == null) {
|
||||
path = "Download/"
|
||||
cursor = contentResolver.query(contentUri, arrayOf(MediaColumns.DISPLAY_NAME), null, null, null)
|
||||
suspend fun save(context: ComponentActivity, name: String, mimeType: String?, overwriteExisting: Boolean) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val contentResolver = context.contentResolver
|
||||
if (overwriteExisting)
|
||||
contentResolver.delete(MediaStore.Downloads.EXTERNAL_CONTENT_URI, String.format("%s = ?", MediaColumns.DISPLAY_NAME), arrayOf(name))
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(MediaColumns.DISPLAY_NAME, name)
|
||||
contentValues.put(MediaColumns.MIME_TYPE, mimeType)
|
||||
val contentUri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
||||
?: throw IOException(context.getString(R.string.create_downloads_file_error))
|
||||
val contentStream = contentResolver.openOutputStream(contentUri)
|
||||
?: throw IOException(context.getString(R.string.create_downloads_file_error))
|
||||
@Suppress("DEPRECATION") var cursor = contentResolver.query(contentUri, arrayOf(MediaColumns.DATA), null, null, null)
|
||||
var path: String? = null
|
||||
if (cursor != null) {
|
||||
try {
|
||||
if (cursor.moveToFirst())
|
||||
path += cursor.getString(0)
|
||||
path = cursor.getString(0)
|
||||
} finally {
|
||||
cursor.close()
|
||||
}
|
||||
}
|
||||
if (path == null) {
|
||||
path = "Download/"
|
||||
cursor = contentResolver.query(contentUri, arrayOf(MediaColumns.DISPLAY_NAME), null, null, null)
|
||||
if (cursor != null) {
|
||||
try {
|
||||
if (cursor.moveToFirst())
|
||||
path += cursor.getString(0)
|
||||
} finally {
|
||||
cursor.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
DownloadsFile(context, contentStream, path, contentUri)
|
||||
}
|
||||
DownloadsFile(context, contentStream, path, contentUri)
|
||||
} else {
|
||||
@Suppress("DEPRECATION") val path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
val file = File(path, name)
|
||||
if (!path.isDirectory && !path.mkdirs())
|
||||
throw IOException(context.getString(R.string.create_output_dir_error))
|
||||
DownloadsFile(context, FileOutputStream(file), file.absolutePath, null)
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
val futureGrant = CompletableDeferred<Boolean>()
|
||||
val activityResult = context.registerForActivityResult(ActivityResultContracts.RequestPermission(), futureGrant::complete)
|
||||
activityResult.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
val granted = futureGrant.await()
|
||||
activityResult.unregister()
|
||||
if (!granted)
|
||||
return@withContext null
|
||||
}
|
||||
@Suppress("DEPRECATION") val path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
withContext(Dispatchers.IO) {
|
||||
val file = File(path, name)
|
||||
if (!path.isDirectory && !path.mkdirs())
|
||||
throw IOException(context.getString(R.string.create_output_dir_error))
|
||||
DownloadsFile(context, FileOutputStream(file), file.absolutePath, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadsFile(private val context: Context, val outputStream: OutputStream, val fileName: String, private val uri: Uri?) {
|
||||
fun delete() {
|
||||
suspend fun delete() = withContext(Dispatchers.IO) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||
context.contentResolver.delete(uri!!, null, null)
|
||||
else
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.util
|
||||
|
||||
import android.util.Log
|
||||
import java9.util.function.BiConsumer
|
||||
|
||||
/**
|
||||
* Helpers for logging exceptions from asynchronous tasks. These can be passed to
|
||||
* `CompletionStage.whenComplete()` at the end of an asynchronous future chain.
|
||||
*/
|
||||
enum class ExceptionLoggers(private val priority: Int) : BiConsumer<Any?, Throwable?> {
|
||||
D(Log.DEBUG), E(Log.ERROR);
|
||||
|
||||
override fun accept(result: Any?, throwable: Throwable?) {
|
||||
if (throwable != null)
|
||||
Log.println(Log.ERROR, TAG, Log.getStackTraceString(throwable))
|
||||
else if (priority <= Log.DEBUG)
|
||||
Log.println(priority, TAG, "Future completed successfully")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "WireGuard/ExceptionLoggers"
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,24 @@ package com.wireguard.android.util
|
||||
import android.content.Context
|
||||
import android.util.TypedValue
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.Preference
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.activity.SettingsActivity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
fun Context.resolveAttribute(@AttrRes attrRes: Int): Int {
|
||||
val typedValue = TypedValue()
|
||||
theme.resolveAttribute(attrRes, typedValue, true)
|
||||
return typedValue.data
|
||||
}
|
||||
|
||||
val Any.applicationScope: CoroutineScope
|
||||
get() = Application.getCoroutineScope()
|
||||
|
||||
val Preference.activity: SettingsActivity
|
||||
get() = context as? SettingsActivity
|
||||
?: throw IllegalStateException("Failed to resolve SettingsActivity")
|
||||
|
||||
val Preference.lifecycleScope: CoroutineScope
|
||||
get() = activity.lifecycleScope
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.util
|
||||
|
||||
import android.view.ContextThemeWrapper
|
||||
import androidx.preference.Preference
|
||||
import com.wireguard.android.activity.SettingsActivity
|
||||
|
||||
object FragmentUtils {
|
||||
fun getPrefActivity(preference: Preference): SettingsActivity {
|
||||
val context = preference.context
|
||||
if (context is ContextThemeWrapper) {
|
||||
if (context is SettingsActivity) {
|
||||
return context
|
||||
}
|
||||
}
|
||||
throw IllegalStateException("Failed to resolve SettingsActivity")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util
|
||||
|
||||
import androidx.datastore.preferences.edit
|
||||
import androidx.datastore.preferences.preferencesKey
|
||||
import androidx.datastore.preferences.preferencesSetKey
|
||||
import androidx.datastore.preferences.remove
|
||||
import com.wireguard.android.Application
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
object UserKnobs {
|
||||
private val DISABLE_KERNEL_MODULE = preferencesKey<Boolean>("disable_kernel_module")
|
||||
val disableKernelModule: Flow<Boolean>
|
||||
get() = Application.getPreferencesDataStore().data.map {
|
||||
it[DISABLE_KERNEL_MODULE] ?: false
|
||||
}
|
||||
|
||||
suspend fun setDisableKernelModule(disable: Boolean?) {
|
||||
Application.getPreferencesDataStore().edit {
|
||||
if (disable == null)
|
||||
it.remove(DISABLE_KERNEL_MODULE)
|
||||
else
|
||||
it[DISABLE_KERNEL_MODULE] = disable
|
||||
}
|
||||
}
|
||||
|
||||
private val MULTIPLE_TUNNELS = preferencesKey<Boolean>("multiple_tunnels")
|
||||
val multipleTunnels: Flow<Boolean>
|
||||
get() = Application.getPreferencesDataStore().data.map {
|
||||
it[MULTIPLE_TUNNELS] ?: false
|
||||
}
|
||||
|
||||
private val DARK_THEME = preferencesKey<Boolean>("dark_theme")
|
||||
val darkTheme: Flow<Boolean>
|
||||
get() = Application.getPreferencesDataStore().data.map {
|
||||
it[DARK_THEME] ?: false
|
||||
}
|
||||
|
||||
private val ALLOW_REMOTE_CONTROL_INTENTS = preferencesKey<Boolean>("allow_remote_control_intents")
|
||||
val allowRemoteControlIntents: Flow<Boolean>
|
||||
get() = Application.getPreferencesDataStore().data.map {
|
||||
it[ALLOW_REMOTE_CONTROL_INTENTS] ?: false
|
||||
}
|
||||
|
||||
private val RESTORE_ON_BOOT = preferencesKey<Boolean>("restore_on_boot")
|
||||
val restoreOnBoot: Flow<Boolean>
|
||||
get() = Application.getPreferencesDataStore().data.map {
|
||||
it[RESTORE_ON_BOOT] ?: false
|
||||
}
|
||||
|
||||
private val LAST_USED_TUNNEL = preferencesKey<String>("last_used_tunnel")
|
||||
val lastUsedTunnel: Flow<String?>
|
||||
get() = Application.getPreferencesDataStore().data.map {
|
||||
it[LAST_USED_TUNNEL]
|
||||
}
|
||||
|
||||
suspend fun setLastUsedTunnel(lastUsedTunnel: String?) {
|
||||
Application.getPreferencesDataStore().edit {
|
||||
if (lastUsedTunnel == null)
|
||||
it.remove(LAST_USED_TUNNEL)
|
||||
else
|
||||
it[LAST_USED_TUNNEL] = lastUsedTunnel
|
||||
}
|
||||
}
|
||||
|
||||
private val RUNNING_TUNNELS = preferencesSetKey<String>("enabled_configs")
|
||||
val runningTunnels: Flow<Set<String>>
|
||||
get() = Application.getPreferencesDataStore().data.map {
|
||||
it[RUNNING_TUNNELS] ?: emptySet()
|
||||
}
|
||||
|
||||
suspend fun setRunningTunnels(runningTunnels: Set<String>) {
|
||||
Application.getPreferencesDataStore().edit {
|
||||
if (runningTunnels.isEmpty())
|
||||
it.remove(RUNNING_TUNNELS)
|
||||
else
|
||||
it[RUNNING_TUNNELS] = runningTunnels
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,9 @@ class InterfaceProxy : BaseObservable, Parcelable {
|
||||
@get:Bindable
|
||||
val excludedApplications: ObservableList<String> = ObservableArrayList()
|
||||
|
||||
@get:Bindable
|
||||
val includedApplications: ObservableList<String> = ObservableArrayList()
|
||||
|
||||
@get:Bindable
|
||||
var addresses: String = ""
|
||||
set(value) {
|
||||
@@ -70,6 +73,7 @@ class InterfaceProxy : BaseObservable, Parcelable {
|
||||
addresses = parcel.readString() ?: ""
|
||||
dnsServers = parcel.readString() ?: ""
|
||||
parcel.readStringList(excludedApplications)
|
||||
parcel.readStringList(includedApplications)
|
||||
listenPort = parcel.readString() ?: ""
|
||||
mtu = parcel.readString() ?: ""
|
||||
privateKey = parcel.readString() ?: ""
|
||||
@@ -80,6 +84,7 @@ class InterfaceProxy : BaseObservable, Parcelable {
|
||||
val dnsServerStrings = other.dnsServers.map { it.hostAddress }
|
||||
dnsServers = Attribute.join(dnsServerStrings)
|
||||
excludedApplications.addAll(other.excludedApplications)
|
||||
includedApplications.addAll(other.includedApplications)
|
||||
listenPort = other.listenPort.map { it.toString() }.orElse("")
|
||||
mtu = other.mtu.map { it.toString() }.orElse("")
|
||||
val keyPair = other.keyPair
|
||||
@@ -103,6 +108,7 @@ class InterfaceProxy : BaseObservable, Parcelable {
|
||||
if (addresses.isNotEmpty()) builder.parseAddresses(addresses)
|
||||
if (dnsServers.isNotEmpty()) builder.parseDnsServers(dnsServers)
|
||||
if (excludedApplications.isNotEmpty()) builder.excludeApplications(excludedApplications)
|
||||
if (includedApplications.isNotEmpty()) builder.includeApplications(includedApplications)
|
||||
if (listenPort.isNotEmpty()) builder.parseListenPort(listenPort)
|
||||
if (mtu.isNotEmpty()) builder.parseMtu(mtu)
|
||||
if (privateKey.isNotEmpty()) builder.parsePrivateKey(privateKey)
|
||||
@@ -113,6 +119,7 @@ class InterfaceProxy : BaseObservable, Parcelable {
|
||||
dest.writeString(addresses)
|
||||
dest.writeString(dnsServers)
|
||||
dest.writeStringList(excludedApplications)
|
||||
dest.writeStringList(includedApplications)
|
||||
dest.writeString(listenPort)
|
||||
dest.writeString(mtu)
|
||||
dest.writeString(privateKey)
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.widget
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.marginBottom
|
||||
import androidx.core.view.marginLeft
|
||||
import androidx.core.view.marginRight
|
||||
import androidx.core.view.marginTop
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
|
||||
/**
|
||||
* A utility for edge-to-edge display. It provides several features needed to make the app
|
||||
* displayed edge-to-edge on Android Q with gestural navigation.
|
||||
*/
|
||||
|
||||
object EdgeToEdge {
|
||||
@JvmStatic
|
||||
fun setUpRoot(root: ViewGroup) {
|
||||
root.systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setUpScrollingContent(scrollingContent: ViewGroup, fab: ExtendedFloatingActionButton?) {
|
||||
val originalPaddingLeft = scrollingContent.paddingLeft
|
||||
val originalPaddingRight = scrollingContent.paddingRight
|
||||
val originalPaddingBottom = scrollingContent.paddingBottom
|
||||
|
||||
val fabPaddingBottom = fab?.height ?: 0
|
||||
|
||||
val originalMarginTop = scrollingContent.marginTop
|
||||
|
||||
scrollingContent.setOnApplyWindowInsetsListener { _, windowInsets ->
|
||||
scrollingContent.updatePadding(
|
||||
left = originalPaddingLeft + windowInsets.systemWindowInsetLeft,
|
||||
right = originalPaddingRight + windowInsets.systemWindowInsetRight,
|
||||
bottom = originalPaddingBottom + fabPaddingBottom + windowInsets.systemWindowInsetBottom
|
||||
)
|
||||
scrollingContent.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = originalMarginTop + windowInsets.systemWindowInsetTop
|
||||
}
|
||||
windowInsets
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setUpFAB(fab: ExtendedFloatingActionButton) {
|
||||
val originalMarginLeft = fab.marginLeft
|
||||
val originalMarginRight = fab.marginRight
|
||||
val originalMarginBottom = fab.marginBottom
|
||||
fab.setOnApplyWindowInsetsListener { _, windowInsets ->
|
||||
fab.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
leftMargin = originalMarginLeft + windowInsets.systemWindowInsetLeft
|
||||
rightMargin = originalMarginRight + windowInsets.systemWindowInsetRight
|
||||
bottomMargin = originalMarginBottom + windowInsets.systemWindowInsetBottom
|
||||
}
|
||||
windowInsets
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.text.Editable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.util.AttributeSet
|
||||
import com.google.android.material.R
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
|
||||
class MonkeyedTextInputEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = R.attr.editTextStyle) : TextInputEditText(context, attrs, defStyleAttr) {
|
||||
@Override
|
||||
override fun getText(): Editable? {
|
||||
val text = super.getText()
|
||||
if (!text.isNullOrEmpty())
|
||||
return text
|
||||
/* We want this expression in TextInputLayout.java to be true if there's a hint set:
|
||||
* final boolean hasText = editText != null && !TextUtils.isEmpty(editText.getText());
|
||||
* But for everyone else it should return the real value, so we check the caller.
|
||||
*/
|
||||
if (!hint.isNullOrEmpty() && Thread.currentThread().stackTrace[3].className == TextInputLayout::class.qualifiedName)
|
||||
return SpannableStringBuilder(hint)
|
||||
return text
|
||||
}
|
||||
}
|
||||
@@ -10,14 +10,14 @@ import android.view.View
|
||||
import android.widget.RelativeLayout
|
||||
import com.wireguard.android.R
|
||||
|
||||
class MultiselectableRelativeLayout : RelativeLayout {
|
||||
class MultiselectableRelativeLayout @JvmOverloads constructor(
|
||||
context: Context? = null,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0,
|
||||
defStyleRes: Int = 0
|
||||
) : RelativeLayout(context, attrs, defStyleAttr, defStyleRes) {
|
||||
private var multiselected = false
|
||||
|
||||
constructor(context: Context?) : super(context) {}
|
||||
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {}
|
||||
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}
|
||||
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {}
|
||||
|
||||
override fun onCreateDrawableState(extraSpace: Int): IntArray {
|
||||
if (multiselected) {
|
||||
val drawableState = super.onCreateDrawableState(extraSpace + 1)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<scale
|
||||
android:duration="300"
|
||||
android:fromXScale="1.0"
|
||||
android:fromYScale="1.0"
|
||||
android:interpolator="@android:anim/linear_interpolator"
|
||||
android:pivotX="50%"
|
||||
android:pivotY="50%"
|
||||
android:toXScale="0"
|
||||
android:toYScale="0" />
|
||||
</set>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<scale
|
||||
android:duration="300"
|
||||
android:fromXScale="0"
|
||||
android:fromYScale="0"
|
||||
android:interpolator="@android:anim/linear_interpolator"
|
||||
android:pivotX="50%"
|
||||
android:pivotY="50%"
|
||||
android:toXScale="1.0"
|
||||
android:toYScale="1.0" />
|
||||
</set>
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/main_activity_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".activity.MainActivity">
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
android:layout_marginLeft="@dimen/normal_margin"
|
||||
android:layout_marginEnd="@dimen/normal_margin"
|
||||
android:layout_marginRight="@dimen/normal_margin"
|
||||
android:nextFocusDown="@id/create_from_qrcode"
|
||||
android:nextFocusForward="@id/create_from_qrcode"
|
||||
android:text="@string/create_from_file"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
@@ -36,6 +38,9 @@
|
||||
android:layout_marginLeft="@dimen/normal_margin"
|
||||
android:layout_marginEnd="@dimen/normal_margin"
|
||||
android:layout_marginRight="@dimen/normal_margin"
|
||||
android:nextFocusUp="@id/create_from_file"
|
||||
android:nextFocusDown="@id/create_empty"
|
||||
android:nextFocusForward="@id/create_empty"
|
||||
android:text="@string/create_from_qr_code"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
@@ -57,6 +62,7 @@
|
||||
android:layout_marginLeft="@dimen/normal_margin"
|
||||
android:layout_marginEnd="@dimen/normal_margin"
|
||||
android:layout_marginRight="@dimen/normal_margin"
|
||||
android:nextFocusUp="@id/create_from_qrcode"
|
||||
android:text="@string/create_empty"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
|
||||
@@ -18,30 +18,50 @@
|
||||
type="com.wireguard.android.databinding.ObservableKeyedArrayList<String, ApplicationData>" />
|
||||
</data>
|
||||
|
||||
<FrameLayout
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:minHeight="200dp">
|
||||
android:orientation="vertical">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true"
|
||||
android:visibility="@{appData.isEmpty() ? View.VISIBLE : View.GONE}"
|
||||
tools:visibility="gone" />
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabs"
|
||||
style="@style/Widget.MaterialComponents.TabLayout.Colored"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/app_list"
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/exclude_from_tunnel" />
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/include_in_tunnel" />
|
||||
</com.google.android.material.tabs.TabLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:items="@{appData}"
|
||||
app:layout="@{@layout/app_list_item}"
|
||||
tools:itemCount="10"
|
||||
tools:listitem="@layout/app_list_item" />
|
||||
|
||||
</FrameLayout>
|
||||
android:minHeight="200dp">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true"
|
||||
android:visibility="@{appData.isEmpty() ? View.VISIBLE : View.GONE}"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/app_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:items="@{appData}"
|
||||
app:layout="@{@layout/app_list_item}"
|
||||
tools:itemCount="10"
|
||||
tools:listitem="@layout/app_list_item" />
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
</layout>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/list_item_background"
|
||||
android:gravity="center_vertical"
|
||||
android:onClick="@{(view) -> item.setExcludedFromTunnel(!item.excludedFromTunnel)}"
|
||||
android:onClick="@{(view) -> item.setSelected(!item.selected)}"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
@@ -51,10 +51,10 @@
|
||||
tools:text="@tools:sample/full_names" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/excluded_checkbox"
|
||||
android:id="@+id/selected_checkbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="@={item.excludedFromTunnel}"
|
||||
android:checked="@={item.selected}"
|
||||
tools:checked="true" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -17,11 +17,12 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<EditText
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/tunnel_name_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/tunnel_name"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textNoSuggestions|textVisiblePassword"
|
||||
app:filter="@{NameInputFilter.newInstance()}" />
|
||||
|
||||
@@ -29,5 +30,4 @@
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
</layout>
|
||||
|
||||
@@ -16,13 +16,12 @@
|
||||
tools:itemCount="20"
|
||||
tools:listitem="@layout/log_viewer_entry" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/share_fab"
|
||||
style="@style/Widget.MaterialComponents.ExtendedFloatingActionButton.Icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="@dimen/fab_margin"
|
||||
app:icon="@drawable/ic_action_share_white" />
|
||||
app:srcCompat="@drawable/ic_action_share_white" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/master_detail_wrapper"
|
||||
android:id="@+id/main_activity_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
|
||||
@@ -22,131 +22,266 @@
|
||||
type="com.wireguard.config.Config" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:orientation="vertical">
|
||||
android:focusable="true">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/tunnel_detail_card"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:cardBackgroundColor="?attr/elevationOverlayColor"
|
||||
app:cardCornerRadius="0dp"
|
||||
app:cardElevation="2dp"
|
||||
app:contentPadding="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/tunnel_detail_card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/interface_title"
|
||||
style="@style/SectionText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/interface_title"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.wireguard.android.widget.ToggleSwitch
|
||||
android:id="@+id/tunnel_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:checked="@{tunnel.state == State.UP}"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/interface_title"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:onBeforeCheckedChanged="@{fragment::setTunnelState}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interface_name_label"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/interface_name_text"
|
||||
android:text="@string/name"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/interface_title" />
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interface_name_text"
|
||||
style="@style/DetailText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{tunnel.name}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/interface_name_label"
|
||||
tools:text="wg0" />
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/interface_title"
|
||||
style="@style/SectionText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/interface_title"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/public_key_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/public_key_text"
|
||||
android:text="@string/public_key"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/interface_name_text" />
|
||||
<com.wireguard.android.widget.ToggleSwitch
|
||||
android:id="@+id/tunnel_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:nextFocusDown="@id/interface_name_text"
|
||||
android:nextFocusForward="@id/interface_name_text"
|
||||
app:checked="@{tunnel.state == State.UP}"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/interface_title"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:onBeforeCheckedChanged="@{fragment::setTunnelState}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/public_key_text"
|
||||
style="@style/DetailText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/public_key_description"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
android:singleLine="true"
|
||||
android:text="@{config.interface.keyPair.publicKey.toBase64}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/public_key_label"
|
||||
tools:text="wOs2eguFEohqIZxlSJ1CAT9584tc6ejj9hfGFsoBVkA=" />
|
||||
<TextView
|
||||
android:id="@+id/interface_name_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/interface_name_text"
|
||||
android:text="@string/name"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/interface_title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/addresses_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/addresses_text"
|
||||
android:text="@string/addresses"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/public_key_text" />
|
||||
<TextView
|
||||
android:id="@+id/interface_name_text"
|
||||
style="@style/DetailText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/name"
|
||||
android:nextFocusUp="@id/tunnel_switch"
|
||||
android:nextFocusDown="@id/public_key_text"
|
||||
android:nextFocusForward="@id/public_key_text"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
android:text="@{tunnel.name}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/interface_name_label"
|
||||
tools:text="wg0" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/addresses_text"
|
||||
style="@style/DetailText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/addresses"
|
||||
android:text="@{config.interface.addresses}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/addresses_label"
|
||||
tools:text="fc00:bbbb:bbbb:bb11::3:368b/128" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
<TextView
|
||||
android:id="@+id/public_key_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/public_key_text"
|
||||
android:text="@string/public_key"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/interface_name_text" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/peers_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:divider="@null"
|
||||
android:orientation="vertical"
|
||||
app:items="@{config.peers}"
|
||||
app:layout="@{@layout/tunnel_detail_peer}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tunnel_detail_card"
|
||||
tools:ignore="UselessLeaf" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<TextView
|
||||
android:id="@+id/public_key_text"
|
||||
style="@style/DetailText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/public_key"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:nextFocusUp="@id/interface_name_text"
|
||||
android:nextFocusDown="@id/addresses_text"
|
||||
android:nextFocusForward="@id/addresses_text"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
android:singleLine="true"
|
||||
android:text="@{config.interface.keyPair.publicKey.toBase64}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/public_key_label"
|
||||
tools:text="wOs2eguFEohqIZxlSJ1CAT9584tc6ejj9hfGFsoBVkA=" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/addresses_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/addresses_text"
|
||||
android:text="@string/addresses"
|
||||
android:visibility="@{config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/public_key_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/addresses_text"
|
||||
style="@style/DetailText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/addresses"
|
||||
android:nextFocusUp="@id/public_key_text"
|
||||
android:nextFocusDown="@id/dns_servers_text"
|
||||
android:nextFocusForward="@id/dns_servers_text"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
android:text="@{config.interface.addresses}"
|
||||
android:visibility="@{config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/addresses_label"
|
||||
tools:text="fc00:bbbb:bbbb:bb11::3:368b/128" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dns_servers_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/dns_servers_text"
|
||||
android:text="@string/dns_servers"
|
||||
android:visibility="@{config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/addresses_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dns_servers_text"
|
||||
style="@style/DetailText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/dns_servers"
|
||||
android:nextFocusUp="@id/addresses_text"
|
||||
android:nextFocusDown="@id/listen_port_text"
|
||||
android:nextFocusForward="@id/listen_port_text"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
android:text="@{config.interface.dnsServers}"
|
||||
android:visibility="@{config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/dns_servers_label"
|
||||
tools:text="8.8.8.8, 8.8.4.4" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/listen_port_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/listen_port_text"
|
||||
android:text="@string/listen_port"
|
||||
android:visibility="@{!config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintEnd_toStartOf="@id/mtu_label"
|
||||
app:layout_constraintHorizontal_weight="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/dns_servers_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/listen_port_text"
|
||||
style="@style/DetailText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/listen_port"
|
||||
android:nextFocusRight="@id/mtu_text"
|
||||
android:nextFocusUp="@id/dns_servers_text"
|
||||
android:nextFocusDown="@id/applications_text"
|
||||
android:nextFocusForward="@id/mtu_text"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
android:text="@{config.interface.listenPort}"
|
||||
android:visibility="@{!config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintEnd_toStartOf="@id/mtu_label"
|
||||
app:layout_constraintHorizontal_weight="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/listen_port_label"
|
||||
tools:text="51820" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mtu_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/mtu_text"
|
||||
android:text="@string/mtu"
|
||||
android:visibility="@{!config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_weight="0.5"
|
||||
app:layout_constraintLeft_toRightOf="@id/listen_port_label"
|
||||
app:layout_constraintStart_toEndOf="@id/listen_port_label"
|
||||
app:layout_constraintTop_toBottomOf="@id/dns_servers_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mtu_text"
|
||||
style="@style/DetailText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/mtu"
|
||||
android:nextFocusLeft="@id/listen_port_text"
|
||||
android:nextFocusUp="@id/dns_servers_text"
|
||||
android:nextFocusForward="@id/applications_text"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
android:text="@{config.interface.mtu}"
|
||||
android:visibility="@{!config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_weight="0.5"
|
||||
app:layout_constraintStart_toEndOf="@id/listen_port_label"
|
||||
app:layout_constraintStart_toStartOf="@+id/mtu_label"
|
||||
app:layout_constraintTop_toBottomOf="@+id/mtu_label"
|
||||
tools:text="1500" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/applications_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/applications_text"
|
||||
android:text="@string/applications"
|
||||
android:visibility="@{config.interface.includedApplications.isEmpty() && config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/listen_port_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/applications_text"
|
||||
style="@style/DetailText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/applications"
|
||||
android:nextFocusUp="@id/mtu_text"
|
||||
android:nextFocusDown="@id/peers_layout"
|
||||
android:nextFocusForward="@id/peers_layout"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
android:text="@{config.interface.includedApplications.isEmpty() ? @plurals/n_excluded_applications(config.interface.excludedApplications.size(), config.interface.excludedApplications.size()) : @plurals/n_included_applications(config.interface.includedApplications.size(), config.interface.includedApplications.size())}"
|
||||
android:visibility="@{config.interface.includedApplications.isEmpty() && config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/applications_label"
|
||||
tools:text="8 excluded" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/peers_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:divider="@null"
|
||||
android:orientation="vertical"
|
||||
app:items="@{config.peers}"
|
||||
app:layout="@{@layout/tunnel_detail_peer}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tunnel_detail_card"
|
||||
tools:ignore="UselessLeaf" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
</layout>
|
||||
|
||||
@@ -15,11 +15,7 @@
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
app:cardBackgroundColor="?attr/elevationOverlayColor"
|
||||
app:cardCornerRadius="0dp"
|
||||
app:cardElevation="2dp"
|
||||
app:contentPadding="8dp">
|
||||
android:layout_margin="8dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -49,9 +45,11 @@
|
||||
style="@style/DetailText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/public_key_description"
|
||||
android:contentDescription="@string/public_key"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:nextFocusDown="@id/pre_shared_key_text"
|
||||
android:nextFocusForward="@id/pre_shared_key_text"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
android:singleLine="true"
|
||||
android:text="@{item.publicKey.toBase64}"
|
||||
@@ -59,6 +57,35 @@
|
||||
app:layout_constraintTop_toBottomOf="@+id/public_key_label"
|
||||
tools:text="wOs2eguFEohqIZxlSJ1CAT9584tc6ejj9hfGFsoBVkA=" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pre_shared_key_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/pre_shared_key_text"
|
||||
android:text="@string/pre_shared_key"
|
||||
android:visibility="@{!item.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/public_key_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pre_shared_key_text"
|
||||
style="@style/DetailText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/pre_shared_key"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:nextFocusUp="@id/public_key_text"
|
||||
android:nextFocusDown="@id/allowed_ips_text"
|
||||
android:nextFocusForward="@id/allowed_ips_text"
|
||||
android:singleLine="true"
|
||||
android:text="@string/pre_shared_key_enabled"
|
||||
android:visibility="@{!item.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/pre_shared_key_label"
|
||||
tools:text="8VyS8W8XeMcBWfKp1GuG3/fZlnUQFkqMNbrdmZtVQIM=" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/allowed_ips_label"
|
||||
android:layout_width="match_parent"
|
||||
@@ -66,15 +93,22 @@
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/allowed_ips_text"
|
||||
android:text="@string/allowed_ips"
|
||||
android:visibility="@{item.allowedIps.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/public_key_text" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/pre_shared_key_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/allowed_ips_text"
|
||||
style="@style/DetailText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/allowed_ips"
|
||||
android:nextFocusUp="@id/pre_shared_key_text"
|
||||
android:nextFocusDown="@id/endpoint_text"
|
||||
android:nextFocusForward="@id/endpoint_text"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
android:text="@{item.allowedIps}"
|
||||
android:visibility="@{item.allowedIps.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/allowed_ips_label"
|
||||
tools:text="0.0.0.0/5, 8.0.0.0/7, 11.0.0.0/8, 12.0.0.0/6, 16.0.0.0/4, 32.0.0.0/3" />
|
||||
@@ -86,6 +120,7 @@
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/endpoint_text"
|
||||
android:text="@string/endpoint"
|
||||
android:visibility="@{!item.endpoint.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/allowed_ips_text" />
|
||||
|
||||
@@ -94,11 +129,44 @@
|
||||
style="@style/DetailText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/endpoint"
|
||||
android:nextFocusUp="@id/allowed_ips_text"
|
||||
android:nextFocusDown="@id/persistent_keepalive_text"
|
||||
android:nextFocusForward="@id/persistent_keepalive_text"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
android:text="@{item.endpoint}"
|
||||
android:visibility="@{!item.endpoint.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/endpoint_label"
|
||||
tools:text="192.168.0.1:51820" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/persistent_keepalive_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/persistent_keepalive_text"
|
||||
android:text="@string/persistent_keepalive"
|
||||
android:visibility="@{!item.persistentKeepalive.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/endpoint_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/persistent_keepalive_text"
|
||||
style="@style/DetailText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/persistent_keepalive"
|
||||
android:nextFocusUp="@id/endpoint_text"
|
||||
android:nextFocusDown="@id/transfer_text"
|
||||
android:nextFocusForward="@id/transfer_text"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
android:text="@{@plurals/persistent_keepalive_seconds_unit(item.persistentKeepalive.orElse(0), item.persistentKeepalive.orElse(0))}"
|
||||
android:visibility="@{!item.persistentKeepalive.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/persistent_keepalive_label"
|
||||
tools:text="every 3 seconds" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/transfer_label"
|
||||
android:layout_width="match_parent"
|
||||
@@ -109,7 +177,7 @@
|
||||
android:text="@string/transfer"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/endpoint_text"
|
||||
app:layout_constraintTop_toBottomOf="@+id/persistent_keepalive_text"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
@@ -118,6 +186,9 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/transfer_label"
|
||||
android:contentDescription="@string/transfer"
|
||||
android:nextFocusUp="@id/persistent_keepalive_text"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/transfer_label"
|
||||
|
||||
@@ -45,11 +45,7 @@
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardBackgroundColor="?attr/elevationOverlayColor"
|
||||
app:cardCornerRadius="0dp"
|
||||
app:cardElevation="2dp"
|
||||
app:contentPadding="8dp">
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -80,7 +76,10 @@
|
||||
android:id="@+id/interface_name_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="textNoSuggestions|textVisiblePassword"
|
||||
android:nextFocusDown="@id/private_key_text"
|
||||
android:nextFocusForward="@id/private_key_text"
|
||||
android:text="@={name}"
|
||||
app:filter="@{NameInputFilter.newInstance()}" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
@@ -102,7 +101,11 @@
|
||||
android:id="@+id/private_key_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="textNoSuggestions|textPassword"
|
||||
android:nextFocusUp="@id/interface_name_text"
|
||||
android:nextFocusDown="@id/public_key_text"
|
||||
android:nextFocusForward="@id/public_key_text"
|
||||
android:onClick="@{fragment::onKeyClick}"
|
||||
android:text="@={config.interface.privateKey}"
|
||||
app:filter="@{KeyInputFilter.newInstance()}"
|
||||
@@ -115,11 +118,12 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="4dp"
|
||||
android:hint="@string/public_key"
|
||||
app:expandedHintEnabled="false"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/private_key_text_layout">
|
||||
|
||||
<com.wireguard.android.widget.MonkeyedTextInputEditText
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/public_key_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -127,6 +131,10 @@
|
||||
android:ellipsize="end"
|
||||
android:focusable="false"
|
||||
android:hint="@string/hint_generated"
|
||||
android:imeOptions="actionNext"
|
||||
android:nextFocusUp="@id/private_key_text"
|
||||
android:nextFocusDown="@id/addresses_label_text"
|
||||
android:nextFocusForward="@id/addresses_label_text"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
android:singleLine="true"
|
||||
android:text="@{config.interface.publicKey}" />
|
||||
@@ -148,7 +156,11 @@
|
||||
android:id="@+id/addresses_label_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="textNoSuggestions|textVisiblePassword"
|
||||
android:nextFocusUp="@id/public_key_text"
|
||||
android:nextFocusDown="@id/dns_servers_text"
|
||||
android:nextFocusForward="@id/listen_port_text"
|
||||
android:text="@={config.interface.addresses}" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
@@ -158,16 +170,22 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="4dp"
|
||||
android:hint="@string/listen_port"
|
||||
app:expandedHintEnabled="false"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_weight="0.3"
|
||||
app:layout_constraintStart_toEndOf="@id/addresses_label_layout"
|
||||
app:layout_constraintTop_toBottomOf="@id/public_key_label_layout">
|
||||
|
||||
<com.wireguard.android.widget.MonkeyedTextInputEditText
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/listen_port_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/hint_random"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="number"
|
||||
android:nextFocusUp="@id/public_key_text"
|
||||
android:nextFocusDown="@id/mtu_text"
|
||||
android:nextFocusForward="@id/dns_servers_text"
|
||||
android:text="@={config.interface.listenPort}"
|
||||
android:textAlignment="center" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
@@ -189,7 +207,11 @@
|
||||
android:id="@+id/dns_servers_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="textNoSuggestions|textVisiblePassword"
|
||||
android:nextFocusUp="@id/addresses_label_text"
|
||||
android:nextFocusDown="@id/set_excluded_applications"
|
||||
android:nextFocusForward="@id/mtu_text"
|
||||
android:text="@={config.interface.dnsServers}" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
@@ -199,17 +221,22 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="4dp"
|
||||
android:hint="@string/mtu"
|
||||
app:expandedHintEnabled="false"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_weight="0.3"
|
||||
app:layout_constraintStart_toEndOf="@id/dns_servers_label_layout"
|
||||
app:layout_constraintTop_toBottomOf="@id/addresses_label_layout">
|
||||
|
||||
<com.wireguard.android.widget.MonkeyedTextInputEditText
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/mtu_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/hint_automatic"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="number"
|
||||
android:nextFocusUp="@id/listen_port_text"
|
||||
android:nextFocusDown="@id/set_excluded_applications"
|
||||
android:nextFocusForward="@id/set_excluded_applications"
|
||||
android:text="@={config.interface.mtu}"
|
||||
android:textAlignment="center" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
@@ -220,18 +247,23 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="4dp"
|
||||
android:onClick="@{fragment::onRequestSetExcludedApplications}"
|
||||
android:text="@{@plurals/set_excluded_applications(config.interface.excludedApplications.size, config.interface.excludedApplications.size)}"
|
||||
android:nextFocusUp="@id/dns_servers_text"
|
||||
android:nextFocusDown="@id/peers_layout"
|
||||
android:nextFocusForward="@id/peers_layout"
|
||||
android:onClick="@{fragment::onRequestSetExcludedIncludedApplications}"
|
||||
android:text="@{config.interface.includedApplications.size > 0 ? @plurals/set_included_applications(config.interface.includedApplications.size, config.interface.includedApplications.size) : config.interface.excludedApplications.size > 0 ? @plurals/set_excluded_applications(config.interface.excludedApplications.size, config.interface.excludedApplications.size) : @string/all_applications}"
|
||||
android:textColor="?attr/colorSecondary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/mtu_label_layout"
|
||||
app:rippleColor="?attr/colorSecondary" />
|
||||
app:rippleColor="?attr/colorSecondary"
|
||||
tools:text="4 excluded applications" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/peers_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:divider="@null"
|
||||
@@ -242,6 +274,7 @@
|
||||
tools:ignore="UselessLeaf" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/add_peer_button"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
<import type="com.wireguard.android.widget.KeyInputFilter" />
|
||||
|
||||
<import type="com.wireguard.android.databinding.BindingAdapters" />
|
||||
|
||||
<variable
|
||||
name="collection"
|
||||
type="androidx.databinding.ObservableList<com.wireguard.android.viewmodel.PeerProxy>" />
|
||||
@@ -27,11 +29,7 @@
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
app:cardBackgroundColor="?attr/elevationOverlayColor"
|
||||
app:cardCornerRadius="0dp"
|
||||
app:cardElevation="2dp"
|
||||
app:contentPadding="8dp">
|
||||
android:layout_marginBottom="4dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -53,6 +51,8 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@null"
|
||||
android:nextFocusDown="@id/public_key_text"
|
||||
android:nextFocusForward="@id/public_key_text"
|
||||
android:onClick="@{() -> item.unbind()}"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_action_delete"
|
||||
@@ -75,7 +75,11 @@
|
||||
android:id="@+id/public_key_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="textNoSuggestions|textVisiblePassword"
|
||||
android:nextFocusUp="@id/delete"
|
||||
android:nextFocusDown="@id/pre_shared_key_text"
|
||||
android:nextFocusForward="@id/pre_shared_key_text"
|
||||
android:text="@={item.publicKey}"
|
||||
app:filter="@{KeyInputFilter.newInstance()}" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
@@ -86,16 +90,21 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="4dp"
|
||||
android:hint="@string/pre_shared_key"
|
||||
app:expandedHintEnabled="false"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/public_key_label_layout">
|
||||
|
||||
<com.wireguard.android.widget.MonkeyedTextInputEditText
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/pre_shared_key_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/hint_optional"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="textNoSuggestions|textPassword"
|
||||
android:nextFocusUp="@id/public_key_text"
|
||||
android:nextFocusDown="@id/persistent_keepalive_text"
|
||||
android:nextFocusForward="@id/persistent_keepalive_text"
|
||||
android:onClick="@{fragment::onKeyClick}"
|
||||
android:text="@={item.preSharedKey}"
|
||||
app:filter="@{KeyInputFilter.newInstance()}"
|
||||
@@ -108,16 +117,22 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="4dp"
|
||||
android:hint="@string/persistent_keepalive"
|
||||
app:expandedHintEnabled="false"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/pre_shared_key_label_layout">
|
||||
app:layout_constraintTop_toBottomOf="@id/pre_shared_key_label_layout"
|
||||
app:suffixText="@{@plurals/persistent_keepalive_seconds_suffix(BindingAdapters.tryParseInt(item.persistentKeepalive))}">
|
||||
|
||||
<com.wireguard.android.widget.MonkeyedTextInputEditText
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/persistent_keepalive_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/hint_optional_discouraged"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="number"
|
||||
android:nextFocusUp="@id/persistent_keepalive_text"
|
||||
android:nextFocusDown="@id/endpoint_text"
|
||||
android:nextFocusForward="@id/endpoint_text"
|
||||
android:text="@={item.persistentKeepalive}" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
@@ -135,7 +150,11 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="textNoSuggestions|textVisiblePassword"
|
||||
android:nextFocusUp="@id/persistent_keepalive_text"
|
||||
android:nextFocusDown="@id/allowed_ips_text"
|
||||
android:nextFocusForward="@id/allowed_ips_text"
|
||||
android:text="@={item.endpoint}" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
@@ -154,17 +173,23 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textNoSuggestions|textVisiblePassword"
|
||||
android:nextFocusUp="@id/endpoint_text"
|
||||
android:nextFocusDown="@id/selected_checkbox"
|
||||
android:nextFocusForward="@id/selected_checkbox"
|
||||
android:text="@={item.allowedIps}" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/excluded_checkbox"
|
||||
android:id="@+id/selected_checkbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginTop="0dp"
|
||||
android:checked="@={item.excludingPrivateIps}"
|
||||
android:nextFocusDown="@id/add_peer_button"
|
||||
android:nextFocusForward="@id/add_peer_button"
|
||||
android:text="@string/exclude_private_ips"
|
||||
android:visibility="@{item.ableToExcludePrivateIps ? View.VISIBLE : View.GONE}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
android:layout_height="match_parent"
|
||||
android:choiceMode="multipleChoiceModal"
|
||||
android:clipToPadding="false"
|
||||
android:nextFocusDown="@id/create_fab"
|
||||
android:nextFocusForward="@id/create_fab"
|
||||
android:paddingBottom="@{@dimen/design_fab_size_normal * 1.1f}"
|
||||
android:visibility="@{tunnels.size() > 0 ? android.view.View.VISIBLE : android.view.View.GONE}"
|
||||
app:configurationHandler="@{rowConfigurationHandler}"
|
||||
@@ -66,14 +68,14 @@
|
||||
android:textSize="20sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/create_fab"
|
||||
style="@style/Widget.MaterialComponents.ExtendedFloatingActionButton.Icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="@dimen/fab_margin"
|
||||
app:icon="@drawable/ic_action_add_white" />
|
||||
android:nextFocusUp="@id/tunnel_list"
|
||||
app:srcCompat="@drawable/ic_action_add_white" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</layout>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -24,14 +24,47 @@
|
||||
<item quantity="one">%d ausgeschlossene Anwendung</item>
|
||||
<item quantity="other">%d ausgeschlossene Anwendungen</item>
|
||||
</plurals>
|
||||
<plurals name="set_included_applications">
|
||||
<item quantity="one">%d einbezogene Anwendung</item>
|
||||
<item quantity="other">%d einbezogene Anwendungen</item>
|
||||
</plurals>
|
||||
<plurals name="n_excluded_applications">
|
||||
<item quantity="one">%d ausgeschlossen</item>
|
||||
<item quantity="other">%d ausgeschlossen</item>
|
||||
</plurals>
|
||||
<plurals name="n_included_applications">
|
||||
<item quantity="one">%d einbezogen</item>
|
||||
<item quantity="other">%d einbezogen</item>
|
||||
</plurals>
|
||||
<string name="all_applications">Alle Anwendungen</string>
|
||||
<string name="exclude_from_tunnel">Ausschließen</string>
|
||||
<string name="include_in_tunnel">Nur einbeziehen</string>
|
||||
<plurals name="include_n_applications">
|
||||
<item quantity="one">%d App einbeziehen</item>
|
||||
<item quantity="other">%d App einbeziehen</item>
|
||||
</plurals>
|
||||
<plurals name="exclude_n_applications">
|
||||
<item quantity="one">%d App ausschließen</item>
|
||||
<item quantity="other">%d App ausschließen</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_unit">
|
||||
<item quantity="one">jede Sekunde</item>
|
||||
<item quantity="other">alle %d Sekunden</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_suffix">
|
||||
<item quantity="one">Sekunde</item>
|
||||
<item quantity="other">Sekunden</item>
|
||||
</plurals>
|
||||
<string name="use_all_applications">Alle Apps verwenden</string>
|
||||
<string name="add_peer">Teilnehmer hinzufügen</string>
|
||||
<string name="addresses">Adressen</string>
|
||||
<string name="applications">Anwendungen</string>
|
||||
<string name="allow_remote_control_intents_summary_off">Externe Apps dürfen keine Tunnel umschalten (empfohlen)</string>
|
||||
<string name="allow_remote_control_intents_summary_on">Externe Apps dürfen Tunnel umschalten (erweitert)</string>
|
||||
<string name="allow_remote_control_intents_title">Erlaube Steuerung über externe Apps</string>
|
||||
<string name="allowed_ips">Erlaubte IPs</string>
|
||||
<string name="app_name">WireGuard</string>
|
||||
<string name="bad_config_context">%1$ss %2$s</string>
|
||||
<string name="bad_config_context">%2$s des %1$s</string>
|
||||
<string name="bad_config_context_top_level">%s</string>
|
||||
<string name="bad_config_error">%1$s in %2$s</string>
|
||||
<string name="bad_config_explanation_pka">: Muss positiv sein und nicht größer als 65535</string>
|
||||
@@ -40,8 +73,8 @@
|
||||
<string name="bad_config_reason_invalid_key">Ungültiger Schlüssel</string>
|
||||
<string name="bad_config_reason_invalid_number">Ungültige Zahl</string>
|
||||
<string name="bad_config_reason_invalid_value">Ungültiger Wert</string>
|
||||
<string name="bad_config_reason_missing_attribute">Attribut fehlt</string>
|
||||
<string name="bad_config_reason_missing_section">Fehlende Sektion</string>
|
||||
<string name="bad_config_reason_missing_attribute">Fehlendes Attribut</string>
|
||||
<string name="bad_config_reason_missing_section">Fehlender Abschnitt</string>
|
||||
<string name="bad_config_reason_syntax_error">Syntaxfehler</string>
|
||||
<string name="bad_config_reason_unknown_attribute">Unbekanntes Attribut</string>
|
||||
<string name="bad_config_reason_unknown_section">Unbekannter Abschnitt</string>
|
||||
@@ -56,27 +89,26 @@
|
||||
<string name="config_save_error">Konfiguration für „%1$s“ kann nicht gespeichert werden: %2$s</string>
|
||||
<string name="config_save_success">Konfiguration für „%s ” erfolgreich gespeichert</string>
|
||||
<string name="create_activity_title">WireGuard-Tunnel erstellen</string>
|
||||
<string name="create_bin_dir_error">Lokales Anwendungsverzeichnis konnte nicht erstellt werden</string>
|
||||
<string name="create_downloads_file_error">Datei konnte im Download-Verzeichnis nicht erstellt werden</string>
|
||||
<string name="create_bin_dir_error">Lokales Anwendungsverzeichnis kann nicht erstellt werden</string>
|
||||
<string name="create_downloads_file_error">Datei kann im Download-Verzeichnis nicht erstellt werden</string>
|
||||
<string name="create_empty">Neu erstellen</string>
|
||||
<string name="create_from_file">Aus Datei oder Archiv importieren</string>
|
||||
<string name="create_from_qr_code">Vom QR-Code scannen</string>
|
||||
<string name="create_from_qr_code">Von QR-Code scannen</string>
|
||||
<string name="create_output_dir_error">Konnte das Ausgabeverzeichnis nicht erstellen</string>
|
||||
<string name="create_temp_dir_error">Lokales temporäres Verzeichnis konnte nicht erstellt werden</string>
|
||||
<string name="create_temp_dir_error">Lokales temporäres Verzeichnis kann nicht erstellt werden</string>
|
||||
<string name="create_tunnel">Tunnel erstellen</string>
|
||||
<string name="dark_theme_summary_off">Helles (Tag) Design in Verwendung</string>
|
||||
<string name="dark_theme_summary_on">Dunkles (Nacht) Design in Verwendung</string>
|
||||
<string name="dark_theme_summary_off">Verwende helles Design (Tag)</string>
|
||||
<string name="dark_theme_summary_on">Verwende dunkles Design (Nacht)</string>
|
||||
<string name="dark_theme_title">Dunkles Design verwenden</string>
|
||||
<string name="delete">Entfernen</string>
|
||||
<string name="dns_servers">DNS-Server</string>
|
||||
<string name="edit">Bearbeiten</string>
|
||||
<string name="endpoint">Endpunkt</string>
|
||||
<string name="error_down">Fehler beim Herunterfahren des Tunnels: %s</string>
|
||||
<string name="error_down">Fehler beim Abschalten des Tunnels: %s</string>
|
||||
<string name="error_fetching_apps">Fehler beim Abrufen der App-Liste: %s</string>
|
||||
<string name="error_root">Bitte root-Zugriff anfordern und erneut versuchen</string>
|
||||
<string name="error_up">Fehler beim Starten des Tunnels: %s</string>
|
||||
<string name="exclude_private_ips">Private IPs ausschließen</string>
|
||||
<string name="excluded_applications">Ausgeschlossene Anwendungen</string>
|
||||
<string name="generate_new_private_key">Neuen privaten Schlüssel generieren</string>
|
||||
<string name="generic_error">Unbekannter „%s“ Fehler</string>
|
||||
<string name="hint_automatic">(auto)</string>
|
||||
@@ -88,38 +120,38 @@
|
||||
<string name="import_error">Kann Tunnel nicht importieren: %s</string>
|
||||
<string name="import_from_qr_code">Tunnel aus QR-Code importieren</string>
|
||||
<string name="import_success">„%s” importiert</string>
|
||||
<string name="interface_title">Schnittstelle</string>
|
||||
<string name="key_contents_error">Falsche Zeichen in Schlüssel</string>
|
||||
<string name="interface_title">Interface</string>
|
||||
<string name="key_contents_error">Unzulässige Zeichen im Schlüssel</string>
|
||||
<string name="key_length_error">Falsche Schlüssellänge</string>
|
||||
<string name="key_length_explanation_base64">: WireGuard base64-Schlüssel müssen 44 Zeichen enthalten (32 Bytes)</string>
|
||||
<string name="key_length_explanation_binary">: WireGuard-Schlüssel müssen 32 Bytes groß sein</string>
|
||||
<string name="key_length_explanation_hex">: WireGuard Hex-Schlüssel müssen 64 Zeichen (32 Bytes) groß sein</string>
|
||||
<string name="listen_port">Eingangs-Port</string>
|
||||
<string name="log_export_error">Fehler beim Exportieren des Logs: %s</string>
|
||||
<string name="log_export_subject">WireGuard Android Log Datei</string>
|
||||
<string name="log_export_error">Konnte Protokoll nicht exportieren: %s</string>
|
||||
<string name="log_export_subject">WireGuard Android Protokolldatei</string>
|
||||
<string name="log_export_success">Gespeichert in „%s”</string>
|
||||
<string name="log_export_title">Logdatei exportieren</string>
|
||||
<string name="log_saver_activity_label">Log speichern</string>
|
||||
<string name="log_viewer_pref_summary">Logs können beim Debuggen helfen</string>
|
||||
<string name="log_viewer_pref_title">Protokoll der Anwendung anzeigen</string>
|
||||
<string name="log_viewer_title">Loggen</string>
|
||||
<string name="log_saver_activity_label">Protokoll speichern</string>
|
||||
<string name="log_viewer_pref_summary">Protokolle können beim Debuggen helfen</string>
|
||||
<string name="log_viewer_pref_title">Anwendungs-Protokoll anzeigen</string>
|
||||
<string name="log_viewer_title">Protokoll</string>
|
||||
<string name="logcat_error">Konnte logcat nicht ausführen: </string>
|
||||
<string name="module_disabler_disabled_summary">Das experimentelle Kernelmodul kann die Leistung verbessern</string>
|
||||
<string name="module_disabler_disabled_title">Kernelmodul-Backend aktivieren</string>
|
||||
<string name="module_disabler_enabled_summary">Das langsamere Userspace-Backend kann die Stabilität verbessern</string>
|
||||
<string name="module_disabler_enabled_title">Kernelmodul-Backend deaktivieren</string>
|
||||
<string name="module_installer_error">Es ist etwas schief gelaufen. Bitte versuchen Sie es später erneut</string>
|
||||
<string name="module_installer_error">Es ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut</string>
|
||||
<string name="module_installer_initial">Das experimentelle Kernelmodul kann die Leistung verbessern</string>
|
||||
<string name="module_installer_not_found">Für Ihr Gerät sind keine Module verfügbar</string>
|
||||
<string name="module_installer_title">Kernelmodul herunterladen und installieren</string>
|
||||
<string name="module_installer_working">Herunterladen und Installieren…</string>
|
||||
<string name="module_installer_working">Lade herunter und installiere…</string>
|
||||
<string name="module_version_error">Konnte Version des Kernel-Moduls nicht ermitteln</string>
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="multiple_tunnels_summary_off">Einschalten eines Tunnels wird andere ausschalten</string>
|
||||
<string name="multiple_tunnels_summary_on">Mehrere Tunnel können gleichzeitig aktiviert werden</string>
|
||||
<string name="multiple_tunnels_title">Erlaube mehrere Tunnel gleichzeitig</string>
|
||||
<string name="multiple_tunnels_summary_off">Das einschalten eines Tunnels wird andere ausschalten</string>
|
||||
<string name="multiple_tunnels_summary_on">Mehrere Tunnel können gleichzeitig aktiviert sein</string>
|
||||
<string name="multiple_tunnels_title">Erlaube mehrere gleichzeitige Tunnel</string>
|
||||
<string name="name">Name</string>
|
||||
<string name="no_config_error">Versucht einen Tunnel ohne Konfiguration zu starten</string>
|
||||
<string name="no_config_error">Es wurde versucht, einen Tunnel ohne Konfiguration zu starten</string>
|
||||
<string name="no_configs_error">Keine Konfigurationen gefunden</string>
|
||||
<string name="no_tunnels_error">Keine Tunnel vorhanden</string>
|
||||
<string name="parse_error_generic">String (Zeichenkette)</string>
|
||||
@@ -133,16 +165,15 @@
|
||||
<string name="permission_label">WireGuard-Tunnel steuern</string>
|
||||
<string name="persistent_keepalive">Dauerhafte Erhaltung</string>
|
||||
<string name="pre_shared_key">Vorab geteilter Schlüssel</string>
|
||||
<string name="pre_shared_key_enabled">aktiviert</string>
|
||||
<string name="private_key">Privater Schlüssel</string>
|
||||
<string name="public_key">Öffentlicher Schlüssel</string>
|
||||
<string name="public_key_description">Öffentlicher Schlüssel</string>
|
||||
<string name="qr_code_hint">Tipp: Mit `qrencode -t ansiutf8 < tunnel.conf` generieren.</string>
|
||||
<string name="restore_on_boot_summary_off">Wird aktivierte Tunnel beim Systemstart nicht automatisch starten</string>
|
||||
<string name="restore_on_boot_summary_on">Wird aktivierte Tunnel beim Systemstart automatisch wieder starten</string>
|
||||
<string name="restore_on_boot_summary_off">Aktivierte Tunnel beim Systemstart nicht automatisch starten</string>
|
||||
<string name="restore_on_boot_summary_on">Aktivierte Tunnel beim Systemstart automatisch wieder starten</string>
|
||||
<string name="restore_on_boot_title">Beim Neustart wiederherstellen</string>
|
||||
<string name="save">Speichern</string>
|
||||
<string name="select_all">Alle auswählen</string>
|
||||
<string name="set_exclusions">Ausnahmen festlegen</string>
|
||||
<string name="settings">Einstellungen</string>
|
||||
<string name="shell_exit_status_read_error">Shell kann den Exit-Status nicht lesen</string>
|
||||
<string name="shell_marker_count_error">Die Shell erwartete 4 Marker, erhielt aber %d</string>
|
||||
@@ -191,4 +222,8 @@
|
||||
<string name="zip_export_success">Gespeichert unter “%s”</string>
|
||||
<string name="zip_export_summary">Zip-Datei wird im Download-Verzeichnis gespeichert</string>
|
||||
<string name="zip_export_title">Tunnel als Zip-Datei exportieren</string>
|
||||
<string name="biometric_prompt_zip_exporter_title">Authentifizieren, um Tunnel zu exportieren</string>
|
||||
<string name="biometric_prompt_private_key_title">Authentifizieren, um privaten Schlüssel anzuzeigen</string>
|
||||
<string name="biometric_auth_error">Authentifizierungsfehler</string>
|
||||
<string name="biometric_auth_error_reason">Authentifizierungsfehler: %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="delete_error">
|
||||
<item quantity="one">Impossible de supprimer %d tunnel : %s</item>
|
||||
<item quantity="other">Impossible de supprimer %d tunnels : %s</item>
|
||||
</plurals>
|
||||
<plurals name="delete_success">
|
||||
<item quantity="one">Supprimé avec succès %d tunnel</item>
|
||||
<item quantity="other">Supprimé avec succès %d tunnels</item>
|
||||
</plurals>
|
||||
<plurals name="delete_title">
|
||||
<item quantity="one">%d tunnel sélectionné</item>
|
||||
<item quantity="other">%d tunnels sélectionnés</item>
|
||||
</plurals>
|
||||
<plurals name="import_partial_success">
|
||||
<item quantity="one">%1$d de %2$d tunnels importés</item>
|
||||
<item quantity="other">%1$d de %2$d tunnels importés</item>
|
||||
</plurals>
|
||||
<plurals name="import_total_success">
|
||||
<item quantity="one">%d tunnel importé</item>
|
||||
<item quantity="other">%d tunnels importés</item>
|
||||
</plurals>
|
||||
<plurals name="set_excluded_applications">
|
||||
<item quantity="one">%d application exclue</item>
|
||||
<item quantity="other">%d applications exclues</item>
|
||||
</plurals>
|
||||
<plurals name="set_included_applications">
|
||||
<item quantity="one">%d application incluse</item>
|
||||
<item quantity="other">%d applications incluses</item>
|
||||
</plurals>
|
||||
<plurals name="n_excluded_applications">
|
||||
<item quantity="one">%d exclue</item>
|
||||
<item quantity="other">%d exclues</item>
|
||||
</plurals>
|
||||
<plurals name="n_included_applications">
|
||||
<item quantity="one">%d incluse</item>
|
||||
<item quantity="other">%d incluses</item>
|
||||
</plurals>
|
||||
<string name="all_applications">Toutes les Applications</string>
|
||||
<string name="exclude_from_tunnel">Exclure</string>
|
||||
<string name="include_in_tunnel">Inclure uniquement</string>
|
||||
<plurals name="include_n_applications">
|
||||
<item quantity="one">Inclure %d application</item>
|
||||
<item quantity="other">Inclure %d applications</item>
|
||||
</plurals>
|
||||
<plurals name="exclude_n_applications">
|
||||
<item quantity="one">Exclure %d application</item>
|
||||
<item quantity="other">Exclure %d applications</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_unit">
|
||||
<item quantity="one">chaque seconde</item>
|
||||
<item quantity="other">tous les %d secondes</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_suffix">
|
||||
<item quantity="one">seconde</item>
|
||||
<item quantity="other">secondes</item>
|
||||
</plurals>
|
||||
<string name="use_all_applications">Utiliser toutes les applications</string>
|
||||
<string name="add_peer">Ajouter un pair</string>
|
||||
<string name="addresses">Adresses</string>
|
||||
<string name="applications">Applications</string>
|
||||
<string name="allow_remote_control_intents_summary_off">Les applications externes ne peuvent pas activer les tunnels (recommandé)</string>
|
||||
<string name="allow_remote_control_intents_summary_on">Les applications externes peuvent activer les tunnels (avancés)</string>
|
||||
<string name="allow_remote_control_intents_title">Autoriser les applications de contrôle à distance</string>
|
||||
<string name="allowed_ips">Adresses IP autorisées</string>
|
||||
<string name="app_name">WireGuard</string>
|
||||
<string name="bad_config_context">%1$s de %2$s</string>
|
||||
<string name="bad_config_context_top_level">%s</string>
|
||||
<string name="bad_config_error">%1$s dans %2$s</string>
|
||||
<string name="bad_config_explanation_pka">: Doit être positif et ne pas dépasser 65535</string>
|
||||
<string name="bad_config_explanation_positive_number">: Doit être positif</string>
|
||||
<string name="bad_config_explanation_udp_port">: Doit être un numéro de port UDP valide</string>
|
||||
<string name="bad_config_reason_invalid_key">Clef invalide</string>
|
||||
<string name="bad_config_reason_invalid_number">Numéro invalide</string>
|
||||
<string name="bad_config_reason_invalid_value">Valeur invalide</string>
|
||||
<string name="bad_config_reason_missing_attribute">Attribut manquant</string>
|
||||
<string name="bad_config_reason_missing_section">Section manquante</string>
|
||||
<string name="bad_config_reason_syntax_error">Erreur de syntaxe</string>
|
||||
<string name="bad_config_reason_unknown_attribute">Attribut inconnu</string>
|
||||
<string name="bad_config_reason_unknown_section">Section inconnue</string>
|
||||
<string name="bad_config_reason_value_out_of_range">Valeur hors limite</string>
|
||||
<string name="bad_extension_error">Le fichier doit être .conf ou .zip</string>
|
||||
<string name="cancel">Annuler</string>
|
||||
<string name="config_delete_error">Impossible de supprimer le fichier de configuration %s</string>
|
||||
<string name="config_exists_error">La configuration de « %s » existe déjà</string>
|
||||
<string name="config_file_exists_error">Le fichier de configuration « %s » existe déjà</string>
|
||||
<string name="config_not_found_error">Fichier de configuration «%s» introuvable</string>
|
||||
<string name="config_rename_error">Impossible de renommer le fichier de configuration «%s»</string>
|
||||
<string name="config_save_error">Impossible d’enregistrer la configuration pour «%1$s» : %2$s</string>
|
||||
<string name="config_save_success">Configuration enregistrée avec succès pour “%s”</string>
|
||||
<string name="create_activity_title">Créer un tunnel WireGuard</string>
|
||||
<string name="create_bin_dir_error">Impossible de créer le répertoire binaire local</string>
|
||||
<string name="create_downloads_file_error">Impossible de créer le fichier dans le répertoire des téléchargements</string>
|
||||
<string name="create_empty">Créer à partir de zéro</string>
|
||||
<string name="create_from_file">Importer depuis un fichier ou une archive</string>
|
||||
<string name="create_from_qr_code">Créer avec un scan de QR code</string>
|
||||
<string name="create_output_dir_error">Impossible de créer le répertoire de sortie</string>
|
||||
<string name="create_temp_dir_error">Impossible de créer le répertoire temporaire local</string>
|
||||
<string name="create_tunnel">Créer un tunnel</string>
|
||||
<string name="dark_theme_summary_off">Utilisation en cours du thème clair (jour)</string>
|
||||
<string name="dark_theme_summary_on">Utilisation en cours du thème sombre (nuit)</string>
|
||||
<string name="dark_theme_title">Utilisez le thème sombre</string>
|
||||
<string name="delete">Supprimer</string>
|
||||
<string name="dns_servers">Serveurs DNS</string>
|
||||
<string name="edit">Modifier</string>
|
||||
<string name="endpoint">Point de terminaison</string>
|
||||
<string name="error_down">Erreur lors de la désactivation du tunnel : %s</string>
|
||||
<string name="error_fetching_apps">Erreur lors de la récupération de la liste d\'applications : %s</string>
|
||||
<string name="error_root">Veuillez obtenir l\'accès root et essayez à nouveau</string>
|
||||
<string name="error_up">Erreur lors de la mise en place du tunnel : %s</string>
|
||||
<string name="exclude_private_ips">Exclure les IPs privées</string>
|
||||
<string name="generate_new_private_key">Générer une nouvelle clé privée</string>
|
||||
<string name="generic_error">Erreur inconnue « %s »</string>
|
||||
<string name="hint_automatic">(auto)</string>
|
||||
<string name="hint_generated">(généré)</string>
|
||||
<string name="hint_optional">(facultatif)</string>
|
||||
<string name="hint_optional_discouraged">(facultatif, non recommandé)</string>
|
||||
<string name="hint_random">(aléatoire)</string>
|
||||
<string name="illegal_filename_error">Nom de fichier invalide “%s”</string>
|
||||
<string name="import_error">Impossible d\'importer le tunnel : %s</string>
|
||||
<string name="import_from_qr_code">Importer un tunnel depuis un QR-code</string>
|
||||
<string name="import_success">Importé «%s »</string>
|
||||
<string name="interface_title">Interface</string>
|
||||
<string name="key_contents_error">Mauvais caractères dans la clé</string>
|
||||
<string name="key_length_error">Longueur de la clé incorrecte</string>
|
||||
<string name="key_length_explanation_base64">: Les clés base64 WireGuard doivent comporter 44 caractères (32 octets)</string>
|
||||
<string name="key_length_explanation_binary">: Les clés WireGuard doivent comporter 32 octets</string>
|
||||
<string name="key_length_explanation_hex">: Les clés hexadécimales WireGuard doivent comporter 64 caractères (32 octets)</string>
|
||||
<string name="listen_port">Port d\'écoute</string>
|
||||
<string name="log_export_error">Impossible d\'exporter le journal : %s</string>
|
||||
<string name="log_export_subject">Fichier journal d\'Android WireGuard</string>
|
||||
<string name="log_export_success">Enregistré dans « %s »</string>
|
||||
<string name="log_export_title">Exporter le fichier journal</string>
|
||||
<string name="log_saver_activity_label">Enregistrer le journal</string>
|
||||
<string name="log_viewer_pref_summary">Les journaux peuvent aider au débogage</string>
|
||||
<string name="log_viewer_pref_title">Afficher le journal de l\'application</string>
|
||||
<string name="log_viewer_title">Journal</string>
|
||||
<string name="logcat_error">Impossible d\'exécuter logcat : </string>
|
||||
<string name="module_disabler_disabled_summary">Le module expérimental du noyau peut améliorer les performances</string>
|
||||
<string name="module_disabler_disabled_title">Activer le backend du module du noyau</string>
|
||||
<string name="module_disabler_enabled_summary">Le backend plus lent de l\'espace utilisateur peut améliorer la stabilité</string>
|
||||
<string name="module_disabler_enabled_title">Désactiver le backend du module du noyau</string>
|
||||
<string name="module_installer_error">Une erreur est survenue. Veuillez réessayer</string>
|
||||
<string name="module_installer_initial">Le module expérimental du noyau peut améliorer les performances</string>
|
||||
<string name="module_installer_not_found">Aucun module n\'est disponible pour votre appareil</string>
|
||||
<string name="module_installer_title">Télécharger et installer le module du noyau</string>
|
||||
<string name="module_installer_working">Téléchargement et installation en cours…</string>
|
||||
<string name="module_version_error">Impossible de déterminer la version du module noyau</string>
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="multiple_tunnels_summary_off">Activer un tunnel éteindra les autres</string>
|
||||
<string name="multiple_tunnels_summary_on">Plusieurs tunnels peuvent être activés simultanément</string>
|
||||
<string name="multiple_tunnels_title">Autoriser plusieurs tunnels simultanés</string>
|
||||
<string name="name">Nom</string>
|
||||
<string name="no_config_error">Tentative d\'établir un tunnel sans configuration</string>
|
||||
<string name="no_configs_error">Aucune configuration trouvée</string>
|
||||
<string name="no_tunnels_error">Aucun tunnel existant</string>
|
||||
<string name="parse_error_generic">chaîne de caractères</string>
|
||||
<string name="parse_error_inet_address">Adresse IP</string>
|
||||
<string name="parse_error_inet_endpoint">point de terminaison</string>
|
||||
<string name="parse_error_inet_network">Réseau IP</string>
|
||||
<string name="parse_error_integer">nombre</string>
|
||||
<string name="parse_error_reason">Impossible d\'analyser %1$s “%2$s”</string>
|
||||
<string name="peer">Pair</string>
|
||||
<string name="permission_description">contrôler les tunnels WireGuard, activer et désactiver les tunnels à volonté, potentiellement détourner le trafic Internet</string>
|
||||
<string name="permission_label">contrôler les tunnels WireGuard</string>
|
||||
<string name="persistent_keepalive">Maintien de connexion permanente</string>
|
||||
<string name="pre_shared_key">Clé pré-partagée</string>
|
||||
<string name="pre_shared_key_enabled">activée</string>
|
||||
<string name="private_key">Clé privée</string>
|
||||
<string name="public_key">Clé publique</string>
|
||||
<string name="qr_code_hint">Astuce : générez avec \"qrencode -t ansiutf8 < tunnel.conf\".</string>
|
||||
<string name="restore_on_boot_summary_off">N\'affichera pas les tunnels activés au démarrage</string>
|
||||
<string name="restore_on_boot_summary_on">Les tunnels activés seront affichés au démarrage</string>
|
||||
<string name="restore_on_boot_title">Restaurer au démarrage</string>
|
||||
<string name="save">Enregistrer</string>
|
||||
<string name="select_all">Tout sélectionner</string>
|
||||
<string name="settings">Paramètres</string>
|
||||
<string name="shell_exit_status_read_error">L\'interpréteur de commande ne peut pas lire l\'état de sortie</string>
|
||||
<string name="shell_marker_count_error">L\'interpréteur de commandes attendait 4 marqueurs, %d reçus</string>
|
||||
<string name="shell_start_error">L\'interpréteur de commande n\'a pas pu démarrer : %d</string>
|
||||
<string name="success_application_will_restart">Succès. L\'application va redémarrer maintenant…</string>
|
||||
<string name="toggle_all">Tout basculer</string>
|
||||
<string name="toggle_error">Erreur lors de l\'activation du tunnel WireGuard : %s</string>
|
||||
<string name="tools_installer_already">wg et wg-quick sont déjà installés</string>
|
||||
<string name="tools_installer_failure">Impossible d\'installer les outils en ligne de commande (pas de root?)</string>
|
||||
<string name="tools_installer_initial">Installer les outils optionnels pour le script</string>
|
||||
<string name="tools_installer_initial_magisk">Installer les outils optionnels pour le script en tant que module Magisk</string>
|
||||
<string name="tools_installer_initial_system">Installer les outils optionnels pour le script dans la partition système</string>
|
||||
<string name="tools_installer_success_magisk">wg et wg-quick installés en tant que module Magisk (redémarrage requis)</string>
|
||||
<string name="tools_installer_success_system">wg et wg-quick installés dans la partition système</string>
|
||||
<string name="tools_installer_title">Installer les outils de ligne de commande</string>
|
||||
<string name="tools_installer_working">Installation de wg et wg-quick</string>
|
||||
<string name="tools_unavailable_error">Outils requis indisponibles</string>
|
||||
<string name="transfer">Transférer</string>
|
||||
<string name="transfer_bytes">%d Octets</string>
|
||||
<string name="transfer_gibibytes">%.2f Go</string>
|
||||
<string name="transfer_kibibytes">%.2f Ko</string>
|
||||
<string name="transfer_mibibytes">%.2f Mo</string>
|
||||
<string name="transfer_rx_tx">rx: %1$s, tx: %2$s</string>
|
||||
<string name="transfer_tibibytes">%.2f To</string>
|
||||
<string name="tun_create_error">Impossible de créer le périphérique tun</string>
|
||||
<string name="tunnel_config_error">Impossible de configurer le tunnel (wg-quick a retourné %d)</string>
|
||||
<string name="tunnel_create_error">Impossible de créer le tunnel : %s</string>
|
||||
<string name="tunnel_create_success">Tunnel «%s » créé avec succès</string>
|
||||
<string name="tunnel_error_already_exists">Le tunnel « %s » existe déjà</string>
|
||||
<string name="tunnel_error_invalid_name">Nom invalide</string>
|
||||
<string name="tunnel_list_placeholder">Ajouter un tunnel en utilisant le bouton bleu</string>
|
||||
<string name="tunnel_name">Nom du tunnel</string>
|
||||
<string name="tunnel_on_error">Impossible d\'activer le tunnel (wgTurnOn a retourné %d)</string>
|
||||
<string name="tunnel_rename_error">Impossible de renommer le tunnel : %s</string>
|
||||
<string name="tunnel_rename_success">Tunnel renommé avec succès en «%s »</string>
|
||||
<string name="type_name_go_userspace">Nom de l\'espace utilisateur</string>
|
||||
<string name="type_name_kernel_module">Module noyau</string>
|
||||
<string name="unknown_error">Erreur inconnue</string>
|
||||
<string name="version_summary">%1$s backend v%2$s</string>
|
||||
<string name="version_summary_checking">Vérification de la version %s du backend</string>
|
||||
<string name="version_summary_unknown">Version %s inconnue</string>
|
||||
<string name="version_title">WireGuard pour Android v%s</string>
|
||||
<string name="vpn_not_authorized_error">Service VPN non autorisé par l\'utilisateur</string>
|
||||
<string name="vpn_start_error">Impossible de démarrer le service VPN Android</string>
|
||||
<string name="zip_export_error">Impossible d\'exporter les tunnels : %s</string>
|
||||
<string name="zip_export_success">Enregistré dans « %s »</string>
|
||||
<string name="zip_export_summary">L\'archive zip sera sauvegardée dans le dossier de téléchargement</string>
|
||||
<string name="zip_export_title">Exporter les tunnels vers le fichier zip</string>
|
||||
<string name="biometric_prompt_zip_exporter_title">S\'authentifier pour exporter des tunnels</string>
|
||||
<string name="biometric_prompt_private_key_title">S\'authentifier pour voir la clé privée</string>
|
||||
<string name="biometric_auth_error">Échec de l\'authentification</string>
|
||||
<string name="biometric_auth_error_reason">Échec de l\'authentification : %s</string>
|
||||
</resources>
|
||||
@@ -72,7 +72,6 @@
|
||||
<string name="error_root">कृपया रूट एक्सेस प्राप्त करें और पुनः प्रयास करें</string>
|
||||
<string name="error_up">टनल को लाने में त्रुटि: %s</string>
|
||||
<string name="exclude_private_ips">निजी आईपी को छोड़ दें</string>
|
||||
<string name="excluded_applications">निकाले गए ऐप्स</string>
|
||||
<string name="generic_error">अज्ञात “%s” त्रुटि</string>
|
||||
<string name="hint_automatic">(ऑटो)</string>
|
||||
<string name="hint_generated">(उत्पन्न)</string>
|
||||
@@ -121,14 +120,12 @@
|
||||
<string name="pre_shared_key">प्री-शेयर्ड कीस</string>
|
||||
<string name="private_key">निजी कीस</string>
|
||||
<string name="public_key">सार्वजनिक कीस</string>
|
||||
<string name="public_key_description">सार्वजनिक कीस</string>
|
||||
<string name="qr_code_hint">सुझाव: `qrencode -t ansiutf8 < tunnel.conf` के साथ उत्पन्न करो</string>
|
||||
<string name="restore_on_boot_summary_on">बूट पर सक्षम टनलस को लाएगा</string>
|
||||
<string name="restore_on_boot_summary_off">बूट पर सक्षम टनलस को नहीं लाएगा</string>
|
||||
<string name="restore_on_boot_title">बूट पर पुनर्स्थापित करें</string>
|
||||
<string name="save">सहेजें</string>
|
||||
<string name="select_all">सभी का चयन करे</string>
|
||||
<string name="set_exclusions">बहिष्करण सेट करें</string>
|
||||
<string name="settings">सेटिंग्स</string>
|
||||
<string name="shell_exit_status_read_error">शेल बाहर निकलने की स्थिति नहीं पढ़ सकता</string>
|
||||
<string name="shell_marker_count_error">शेल ने 4 मार्करों की अपेक्षा की, %d प्राप्त किया</string>
|
||||
|
||||
@@ -1,112 +1,143 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="delete_error">
|
||||
<item quantity="one">Tidak dapat menghapus %d tunel: %s</item>
|
||||
<item quantity="other">Tidak dapat menghapus %d tunel: %s</item>
|
||||
<item quantity="other">Tidak dapat menghapus %d terowongan: %s</item>
|
||||
</plurals>
|
||||
<plurals name="delete_success">
|
||||
<item quantity="one">Berhasil menghapus tunel %d</item>
|
||||
<item quantity="other">Berhasil menghapus tunel %d</item>
|
||||
<item quantity="other">Berhasil menghapus terowongan %d</item>
|
||||
</plurals>
|
||||
<plurals name="delete_title">
|
||||
<item quantity="one">%d tunnel dipilih</item>
|
||||
<item quantity="other">%d tunel dipilih</item>
|
||||
<item quantity="other">%d terowongan dipilih</item>
|
||||
</plurals>
|
||||
<plurals name="import_partial_success">
|
||||
<item quantity="one">Mengimpor tunel %d dari %d</item>
|
||||
<item quantity="other">Mengimpor tunel %d dari %d</item>
|
||||
<item quantity="other">Mengimpor %1$d terowongan dari %2$d</item>
|
||||
</plurals>
|
||||
<plurals name="import_total_success">
|
||||
<item quantity="one">Mengimpor tunel %d</item>
|
||||
<item quantity="other">Mengimpor tunel %d</item>
|
||||
<item quantity="other">Mengimpor %d terowongan</item>
|
||||
</plurals>
|
||||
<plurals name="set_excluded_applications">
|
||||
<item quantity="one">%d Aplikasi dikecualikan</item>
|
||||
<item quantity="other">%d Aplikasi dikecualikan</item>
|
||||
<item quantity="other">%d Aplikasi Dikecualikan</item>
|
||||
</plurals>
|
||||
<string name="add_peer">Tambahkan peer</string>
|
||||
<plurals name="set_included_applications">
|
||||
<item quantity="other">%d Aplikasi Dikecualikan</item>
|
||||
</plurals>
|
||||
<plurals name="n_excluded_applications">
|
||||
<item quantity="other">%d dikecualikan</item>
|
||||
</plurals>
|
||||
<plurals name="n_included_applications">
|
||||
<item quantity="other">%d termasuk</item>
|
||||
</plurals>
|
||||
<string name="all_applications">Semua aplikasi</string>
|
||||
<string name="exclude_from_tunnel">Kecualikan</string>
|
||||
<string name="include_in_tunnel">Berlaku hanya untuk</string>
|
||||
<plurals name="include_n_applications">
|
||||
<item quantity="other">Aplikasi %d yang disertakan</item>
|
||||
</plurals>
|
||||
<plurals name="exclude_n_applications">
|
||||
<item quantity="other">Aplikasi %d yang disertakan</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_unit">
|
||||
<item quantity="other">setiap 10 detik</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_suffix">
|
||||
<item quantity="other">detik</item>
|
||||
</plurals>
|
||||
<string name="use_all_applications">Gunakan semua aplikasi</string>
|
||||
<string name="add_peer">Tambahkan rekan</string>
|
||||
<string name="addresses">Alamat</string>
|
||||
<string name="applications">Aplikasi</string>
|
||||
<string name="allow_remote_control_intents_summary_off">Apl eksternal tidak dapat mengalihkan tunnel (disarankan)</string>
|
||||
<string name="allow_remote_control_intents_summary_on">Apl eksternal dapat mengalihkan tunnel (lanjutan)</string>
|
||||
<string name="allow_remote_control_intents_title">Izinkan apl kendali jarak jauh</string>
|
||||
<string name="allowed_ips">IP diizinkan</string>
|
||||
<string name="app_name">WireGuard</string>
|
||||
<string name="bad_config_context">%1$s\'s %2$s</string>
|
||||
<string name="bad_config_context">%1$s %2$s</string>
|
||||
<string name="bad_config_context_top_level">%s</string>
|
||||
<string name="bad_config_error">%1$s di %2$s</string>
|
||||
<string name="bad_config_explanation_pka">: Harus positif dan tidak lebih dari 65535</string>
|
||||
<string name="bad_config_explanation_positive_number">: Angka harus positif</string>
|
||||
<string name="bad_config_explanation_udp_port">: Harus nomor port UDP yang valid</string>
|
||||
<string name="bad_config_reason_invalid_key">Kunci tidak valid</string>
|
||||
<string name="bad_config_reason_invalid_number">Angka tidak valid</string>
|
||||
<string name="bad_config_reason_invalid_value">Nilai tidak valid</string>
|
||||
<string name="bad_config_reason_missing_attribute">Atribut yang hilang</string>
|
||||
<string name="bad_config_reason_missing_section">Bagian yang hilang</string>
|
||||
<string name="bad_config_reason_syntax_error">Kesalahan sintaks</string>
|
||||
<string name="bad_config_reason_unknown_attribute">Atribut tidak diketahui</string>
|
||||
<string name="bad_config_reason_unknown_section">Bagian tidak diketahui</string>
|
||||
<string name="bad_config_reason_value_out_of_range">Nilai di luar jangkauan</string>
|
||||
<string name="bad_extension_error">File harus .conf atau .zip</string>
|
||||
<string name="cancel">Kembali</string>
|
||||
<string name="config_delete_error">Tidak dapat menghapus file konfigurasi %s</string>
|
||||
<string name="config_exists_error">Konfigurasi untuk “%s” sudah ada</string>
|
||||
<string name="config_file_exists_error">Konfigurasi file “%s” sudah ada</string>
|
||||
<string name="config_not_found_error">Konfigurasi file “%s” tidak ditemukan</string>
|
||||
<string name="config_rename_error">Nama konfigurasi “%s” Tidak bisa diganti</string>
|
||||
<string name="bad_config_explanation_positive_number">: Harus positif</string>
|
||||
<string name="bad_config_explanation_udp_port">: Harus nomor porta UDP yang sah</string>
|
||||
<string name="bad_config_reason_invalid_key">Kunci tidak sah</string>
|
||||
<string name="bad_config_reason_invalid_number">Bilngan tidak sah</string>
|
||||
<string name="bad_config_reason_invalid_value">Nilai tidak sah</string>
|
||||
<string name="bad_config_reason_missing_attribute">Atribut hilang</string>
|
||||
<string name="bad_config_reason_missing_section">Bagian hilang</string>
|
||||
<string name="bad_config_reason_syntax_error">Galat sintaks</string>
|
||||
<string name="bad_config_reason_unknown_attribute">Atribut tak diketahui</string>
|
||||
<string name="bad_config_reason_unknown_section">Bagian tak diketahui</string>
|
||||
<string name="bad_config_reason_value_out_of_range">Nilai di luar rentang</string>
|
||||
<string name="bad_extension_error">Berkas harus .conf atau .zip</string>
|
||||
<string name="cancel">Batalkan</string>
|
||||
<string name="config_delete_error">Tidak dapat menghapus berkas konfigurasi %s</string>
|
||||
<string name="config_exists_error">Sudah ada konfigurasi untuk “%s”</string>
|
||||
<string name="config_file_exists_error">Sudah ada konfigurasi berkas “%s”</string>
|
||||
<string name="config_not_found_error">Tidak menemukan konfigurasi berkas “%s”</string>
|
||||
<string name="config_rename_error">Tidak bisa mengganti nama konfigurasi “%s”</string>
|
||||
<string name="config_save_error">Konfigurasi “%1$s”: %2$s tidak bisa disimpan</string>
|
||||
<string name="config_save_success">Konfigurasi “%s” berhasil disimpan</string>
|
||||
<string name="create_activity_title">Buat tunel WireGuard</string>
|
||||
<string name="create_bin_dir_error">Tidak dapat membuat direktori biner lokal</string>
|
||||
<string name="create_empty">Buat dari awal</string>
|
||||
<string name="create_output_dir_error">Tidak dapat membuat direktori keluaran</string>
|
||||
<string name="create_downloads_file_error">Tidak dapat membuat file di direktori download</string>
|
||||
<string name="create_empty">Buat dari awal</string>
|
||||
<string name="create_from_file">Impor dari berkas atau arsip</string>
|
||||
<string name="create_from_qr_code">Pindai dari kode QR</string>
|
||||
<string name="create_output_dir_error">Tidak dapat membuat direktori keluaran</string>
|
||||
<string name="create_temp_dir_error">Tidak dapat membuat direktori lokal sementara</string>
|
||||
<string name="create_tunnel">Buat tunel</string>
|
||||
<string name="dark_theme_summary_off">Saat ini menggunakan tema terang (siang)</string>
|
||||
<string name="dark_theme_summary_on">Saat ini menggunakan tema gelap (malam)</string>
|
||||
<string name="dark_theme_title">Gunakan tema gelap</string>
|
||||
<string name="delete">Hapus</string>
|
||||
<string name="toggle_all">Matikan semua</string>
|
||||
<string name="dns_servers">Server DNS</string>
|
||||
<string name="edit">Edit</string>
|
||||
<string name="endpoint">Titik akhir</string>
|
||||
<string name="error_down">Kesalahan mengurangi tunel: %s</string>
|
||||
<string name="endpoint">Endpoint</string>
|
||||
<string name="error_down">Kesalahan pada tunel: %s</string>
|
||||
<string name="error_fetching_apps">Kesalahan mengambil daftar aplikasi: %s</string>
|
||||
<string name="error_root">Izinkan akses root dan coba lagi</string>
|
||||
<string name="error_up">Kesalahan menambahkan tunel: %s</string>
|
||||
<string name="error_up">Kesalahan pada tunel: %s</string>
|
||||
<string name="exclude_private_ips">Kecualikan IP pribadi</string>
|
||||
<string name="excluded_applications">Kecualikan aplikasi</string>
|
||||
<string name="generate_new_private_key">Buat kunci privat baru</string>
|
||||
<string name="generic_error">Eror “%s” Tidak diketahui</string>
|
||||
<string name="hint_automatic">(otomatis)</string>
|
||||
<string name="hint_generated">(generate)</string>
|
||||
<string name="hint_optional">(pilihan)</string>
|
||||
<string name="hint_optional_discouraged">(opsional, tidak disarankan)</string>
|
||||
<string name="hint_random">(acak)</string>
|
||||
<string name="illegal_filename_error">Nama file “%s” ilegal</string>
|
||||
<string name="import_error">Tunel %s tidak bisa diimpor</string>
|
||||
<string name="import_from_qr_code">Mengimpor tunel dari kode QR</string>
|
||||
<string name="import_success">“%s” Sudah diimpor</string>
|
||||
<string name="interface_title">Antarmuka</string>
|
||||
<string name="key_contents_error">Karakter buruk pada kunci</string>
|
||||
<string name="key_length_error">Panjang kunci salah</string>
|
||||
<string name="key_length_explanation_base64">: Kunci WireGuard base64 harus terdiri dari 44 karakter (32 bit)</string>
|
||||
<string name="key_length_explanation_binary">: Kunci WireGuard harus terdiri dari 32 bit</string>
|
||||
<string name="key_length_explanation_hex">: Kunci hex WireGuard Harus terdiri dari 64 karakter (32 bit)</string>
|
||||
<string name="listen_port">Isi port</string>
|
||||
<string name="log_export_error">Log tidak bisa diekspor: %s</string>
|
||||
<string name="log_export_subject">Berkas Log WireGuard Android</string>
|
||||
<string name="log_export_success">Simpan ke “%s”</string>
|
||||
<string name="log_export_title">Ekspor file log</string>
|
||||
<string name="log_saver_activity_label">Simpan log</string>
|
||||
<string name="log_viewer_pref_summary">Log dapat membantu dengan pengawakutuan</string>
|
||||
<string name="log_viewer_pref_title">Lihat log aplikasi</string>
|
||||
<string name="log_viewer_title">Log</string>
|
||||
<string name="logcat_error">Tidak bisa menjalankan logcat: </string>
|
||||
<string name="module_version_error">Tidak dapat menentukan versi modul kernel</string>
|
||||
<string name="module_installer_not_found">Tidak tersedia modul untuk perangkat anda</string>
|
||||
<string name="module_disabler_disabled_summary">Modul kernel eksperimental dapat meningkatkan kinerja</string>
|
||||
<string name="module_disabler_disabled_title">Aktifkan backend modul kernel</string>
|
||||
<string name="module_disabler_enabled_summary">Backend userspace yang lebih lambat dapat meningkatkan stabilitas</string>
|
||||
<string name="module_disabler_enabled_title">Nonaktifkan backend modul kernel</string>
|
||||
<string name="module_installer_error">Ada yang salah. Silakan coba lagi</string>
|
||||
<string name="module_installer_initial">Modul kernel eksperimental dapat meningkatkan kinerja</string>
|
||||
<string name="module_installer_not_found">Tidak tersedia modul untuk perangkat anda</string>
|
||||
<string name="module_installer_title">Unduh dan pasang modul kernel</string>
|
||||
<string name="module_installer_working">Mengunduh and memasang…</string>
|
||||
<string name="module_installer_error">Ada yang salah. Silakan coba lagi</string>
|
||||
<string name="module_disabler_disabled_title">Aktifkan backend modul kernel</string>
|
||||
<string name="module_disabler_disabled_summary">Modul kernel eksperimental dapat meningkatkan kinerja</string>
|
||||
<string name="module_disabler_enabled_title">Nonaktifkan backend modul kernel</string>
|
||||
<string name="module_disabler_enabled_summary">Backend userspace yang lebih lambat dapat meningkatkan stabilitas</string>
|
||||
<string name="success_application_will_restart">Berhasil. Sekarang aplikasi akan memulai ulang…</string>
|
||||
<string name="module_version_error">Tidak dapat menentukan versi modul kernel</string>
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="multiple_tunnels_title">Izinkan beberapa sekaligus tunel</string>
|
||||
<string name="multiple_tunnels_summary_on">Beberapa tunel dapat dihidupkan secara bersamaan</string>
|
||||
<string name="multiple_tunnels_summary_off">Menyalakan satu tunel akan mematikan semuanya</string>
|
||||
<string name="name">Name</string>
|
||||
<string name="multiple_tunnels_summary_on">Beberapa tunel dapat dihidupkan secara bersamaan</string>
|
||||
<string name="multiple_tunnels_title">Izinkan beberapa sekaligus tunel</string>
|
||||
<string name="name">Nama</string>
|
||||
<string name="no_config_error">Lihat tunel tanpa konfigurasi</string>
|
||||
<string name="no_configs_error">Tidak ditemukan konfigurasi</string>
|
||||
<string name="no_tunnels_error">Tidak ada tunel</string>
|
||||
@@ -116,24 +147,26 @@
|
||||
<string name="parse_error_inet_network">Jaringan IP</string>
|
||||
<string name="parse_error_integer">angka</string>
|
||||
<string name="parse_error_reason">%1$s “%2$s” Tidak dapat diuraikan</string>
|
||||
<string name="peer">Peer</string>
|
||||
<string name="peer">Rekan</string>
|
||||
<string name="permission_description">mengontrol terowongan WireGuard, mengaktifkan dan menonaktifkannya sesuka hati, berpotensi salah melalu lintas Internet</string>
|
||||
<string name="permission_label">Kontrol tunel WireGuard</string>
|
||||
<string name="persistent_keepalive">Keepalive persisten</string>
|
||||
<string name="pre_shared_key">Kunci Pra-bersama</string>
|
||||
<string name="pre_shared_key_enabled">diaktifkan</string>
|
||||
<string name="private_key">Kunci pribadi</string>
|
||||
<string name="public_key">Kunci publik</string>
|
||||
<string name="public_key_description">Kunci publik</string>
|
||||
<string name="qr_code_hint">Tips: generate dengan `qrencode -t ansiutf8 < tunnel.conf`.</string>
|
||||
<string name="restore_on_boot_summary_on">Tunel yang diaktifkan akan dimunculkan saat boot</string>
|
||||
<string name="restore_on_boot_summary_off">Tunel yang diaktifkan tidak akan ditampilkan saat boot</string>
|
||||
<string name="restore_on_boot_summary_on">Tunel yang diaktifkan akan dimunculkan saat boot</string>
|
||||
<string name="restore_on_boot_title">Pulihkan saat boot</string>
|
||||
<string name="save">Simpan</string>
|
||||
<string name="select_all">Pilih semua</string>
|
||||
<string name="set_exclusions">Tetapkan pengecualian</string>
|
||||
<string name="settings">Pengaturan</string>
|
||||
<string name="shell_exit_status_read_error">Shell tidak dapat membaca status keluar</string>
|
||||
<string name="shell_marker_count_error">Shell diharapkan 4 nilai, diterima %d</string>
|
||||
<string name="shell_start_error">Gagal memulai shell: %d</string>
|
||||
<string name="success_application_will_restart">Berhasil. Sekarang aplikasi akan memulai ulang…</string>
|
||||
<string name="toggle_all">Matikan semua</string>
|
||||
<string name="toggle_error">Tunel WireGuard %s gagal dialihkan</string>
|
||||
<string name="tools_installer_already">wg and wg-quick sudah terpasang</string>
|
||||
<string name="tools_installer_failure">Tidak dapat memasang alat command-line (tidak root?)</string>
|
||||
@@ -146,11 +179,11 @@
|
||||
<string name="tools_installer_working">Memasang wg and wg-quick</string>
|
||||
<string name="tools_unavailable_error">Alat yang diperlukan tidak tersedia</string>
|
||||
<string name="transfer">Transfer</string>
|
||||
<string name="transfer_rx_tx">rx: %1$s, tx: %2$s</string>
|
||||
<string name="transfer_bytes">%d B</string>
|
||||
<string name="transfer_gibibytes">%.2f GiB</string>
|
||||
<string name="transfer_kibibytes">%.2f KiB</string>
|
||||
<string name="transfer_mibibytes">%.2f MiB</string>
|
||||
<string name="transfer_gibibytes">%.2f GiB</string>
|
||||
<string name="transfer_rx_tx">trm: %1$s, krm: %2$s</string>
|
||||
<string name="transfer_tibibytes">%.2f TiB</string>
|
||||
<string name="tun_create_error">Tidak dapat membuat perangkat tun</string>
|
||||
<string name="tunnel_config_error">Tidak bisa mengkonfigurasikan tunel (wg-quick %d dikembalikan)</string>
|
||||
@@ -176,6 +209,8 @@
|
||||
<string name="zip_export_success">Disimpan ke “%s”</string>
|
||||
<string name="zip_export_summary">File Zip akan disimpan di folder download</string>
|
||||
<string name="zip_export_title">Ekspor tunel ke file zip</string>
|
||||
<string name="key_length_error">Panjang kunci salah</string>
|
||||
<string name="key_contents_error">Karakter buruk pada kunci</string>
|
||||
<string name="biometric_prompt_zip_exporter_title">Otentikasi untuk mengekspor terowongan</string>
|
||||
<string name="biometric_prompt_private_key_title">Otentikasi untuk melihat kunci privat</string>
|
||||
<string name="biometric_auth_error">Otentikasi gagal</string>
|
||||
<string name="biometric_auth_error_reason">Otentikasi gagal: %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -24,8 +24,41 @@
|
||||
<item quantity="one">%d applicazione esclusa</item>
|
||||
<item quantity="other">%d applicazioni escluse</item>
|
||||
</plurals>
|
||||
<plurals name="set_included_applications">
|
||||
<item quantity="one">%d applicazione esclusa</item>
|
||||
<item quantity="other">%d applicazioni incluse</item>
|
||||
</plurals>
|
||||
<plurals name="n_excluded_applications">
|
||||
<item quantity="one">%d escluse</item>
|
||||
<item quantity="other">%d escluse</item>
|
||||
</plurals>
|
||||
<plurals name="n_included_applications">
|
||||
<item quantity="one">%d escluse</item>
|
||||
<item quantity="other">%d incluse</item>
|
||||
</plurals>
|
||||
<string name="all_applications">Tutte le applicazioni</string>
|
||||
<string name="exclude_from_tunnel">Escludi</string>
|
||||
<string name="include_in_tunnel">Includi solo</string>
|
||||
<plurals name="include_n_applications">
|
||||
<item quantity="one">Includi %d applicazione</item>
|
||||
<item quantity="other">Includi %d applicazioni</item>
|
||||
</plurals>
|
||||
<plurals name="exclude_n_applications">
|
||||
<item quantity="one">Escludi %d applicazione</item>
|
||||
<item quantity="other">Escludi %d applicazioni</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_unit">
|
||||
<item quantity="one">ogni secondo</item>
|
||||
<item quantity="other">ogni %d secondi</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_suffix">
|
||||
<item quantity="one">secondo</item>
|
||||
<item quantity="other">secondi</item>
|
||||
</plurals>
|
||||
<string name="use_all_applications">Utilizza tutte le applicazioni</string>
|
||||
<string name="add_peer">Aggiungi peer</string>
|
||||
<string name="addresses">Indirizzi</string>
|
||||
<string name="applications">Applicazioni</string>
|
||||
<string name="allow_remote_control_intents_summary_off">Le applicazioni esterne non possono attivare tunnel (consigliato)</string>
|
||||
<string name="allow_remote_control_intents_summary_on">Le applicazioni esterne possono attivare tunnel (avanzato)</string>
|
||||
<string name="allow_remote_control_intents_title">Consenti applicazioni di controllo remoto</string>
|
||||
@@ -76,7 +109,6 @@
|
||||
<string name="error_root">Accedi come root e riprova</string>
|
||||
<string name="error_up">Errore di attivazione del tunnel: %s</string>
|
||||
<string name="exclude_private_ips">Escludi IP privati</string>
|
||||
<string name="excluded_applications">Applicazioni escluse</string>
|
||||
<string name="generate_new_private_key">Genera nuova chiave privata</string>
|
||||
<string name="generic_error">Errore “%s” sconosciuto</string>
|
||||
<string name="hint_automatic">(auto)</string>
|
||||
@@ -133,16 +165,15 @@
|
||||
<string name="permission_label">controlla tunnel WireGuard</string>
|
||||
<string name="persistent_keepalive">Tieni sempre attivo</string>
|
||||
<string name="pre_shared_key">Chiave condivisa (PSK)</string>
|
||||
<string name="pre_shared_key_enabled">abilitata</string>
|
||||
<string name="private_key">Chiave privata</string>
|
||||
<string name="public_key">Chiave pubblica</string>
|
||||
<string name="public_key_description">La chiave pubblica</string>
|
||||
<string name="qr_code_hint">Suggerimento: genera con `qrencode -t ansiutf8 < tunnel.conf`.</string>
|
||||
<string name="restore_on_boot_summary_off">Non attiverà i tunnel configurati all\'avvio</string>
|
||||
<string name="restore_on_boot_summary_on">Attiverà i tunnel configurati all\'avvio</string>
|
||||
<string name="restore_on_boot_title">Ripristina all\'avvio</string>
|
||||
<string name="save">Salva</string>
|
||||
<string name="select_all">Seleziona tutto</string>
|
||||
<string name="set_exclusions">Imposta esclusioni</string>
|
||||
<string name="settings">Impostazioni</string>
|
||||
<string name="shell_exit_status_read_error">La shell non riesce a leggere lo stato di uscita</string>
|
||||
<string name="shell_marker_count_error">La shell si aspettava 4 marker, ne ha ricevuti %d</string>
|
||||
@@ -191,4 +222,8 @@
|
||||
<string name="zip_export_success">Salvato in “%s”</string>
|
||||
<string name="zip_export_summary">Il file zip verrà salvato nella cartella di download</string>
|
||||
<string name="zip_export_title">Esporta i tunnel in un file zip</string>
|
||||
<string name="biometric_prompt_zip_exporter_title">Autenticati per esportare le chiavi i tunnel</string>
|
||||
<string name="biometric_prompt_private_key_title">Autenticati per visualizzare le chiavi private</string>
|
||||
<string name="biometric_auth_error">Autenticazione non riuscita</string>
|
||||
<string name="biometric_auth_error_reason">Autenticazione non riuscita: %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -18,8 +18,34 @@
|
||||
<plurals name="set_excluded_applications">
|
||||
<item quantity="other">除外アプリ %d 個</item>
|
||||
</plurals>
|
||||
<plurals name="set_included_applications">
|
||||
<item quantity="other">トンネリング対象アプリ %d 個</item>
|
||||
</plurals>
|
||||
<plurals name="n_excluded_applications">
|
||||
<item quantity="other">対象外アプリ %d 個</item>
|
||||
</plurals>
|
||||
<plurals name="n_included_applications">
|
||||
<item quantity="other">トンネル対象アプリ %d 個</item>
|
||||
</plurals>
|
||||
<string name="all_applications">すべてのアプリがトンネル対象</string>
|
||||
<string name="exclude_from_tunnel">トンネルしない</string>
|
||||
<string name="include_in_tunnel">トンネルする</string>
|
||||
<plurals name="include_n_applications">
|
||||
<item quantity="other">%d アプリが対象</item>
|
||||
</plurals>
|
||||
<plurals name="exclude_n_applications">
|
||||
<item quantity="other">%d アプリを除外</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_unit">
|
||||
<item quantity="other">%d 秒ごと</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_suffix">
|
||||
<item quantity="other">秒</item>
|
||||
</plurals>
|
||||
<string name="use_all_applications">全アプリをトンネル対象にする</string>
|
||||
<string name="add_peer">ピアを追加する</string>
|
||||
<string name="addresses">アドレス</string>
|
||||
<string name="applications">アプリ</string>
|
||||
<string name="allow_remote_control_intents_summary_off">外部アプリはトンネルを制御できない(推奨)</string>
|
||||
<string name="allow_remote_control_intents_summary_on">外部アプリにトンネルの制御を許可(上級者向け)</string>
|
||||
<string name="allow_remote_control_intents_title">外部アプリからの制御</string>
|
||||
@@ -70,7 +96,6 @@
|
||||
<string name="error_root">root 権限を取得して再試行してください</string>
|
||||
<string name="error_up">トンネル起動時エラー: %s</string>
|
||||
<string name="exclude_private_ips">プライベート IP アドレスを除外</string>
|
||||
<string name="excluded_applications">対象外とするアプリケーション</string>
|
||||
<string name="generate_new_private_key">新しい秘密鍵を生成する</string>
|
||||
<string name="generic_error">未知の “%s” エラー</string>
|
||||
<string name="hint_automatic">(自動)</string>
|
||||
@@ -127,22 +152,21 @@
|
||||
<string name="permission_label">WireGuard トンネルの制御</string>
|
||||
<string name="persistent_keepalive">持続的キープアライブ</string>
|
||||
<string name="pre_shared_key">事前共有鍵</string>
|
||||
<string name="pre_shared_key_enabled">有効</string>
|
||||
<string name="private_key">秘密鍵</string>
|
||||
<string name="public_key">公開鍵</string>
|
||||
<string name="public_key_description">公開鍵</string>
|
||||
<string name="qr_code_hint">Tip: `qrencode -t ansiutf8 < tunnel.conf` で生成できます</string>
|
||||
<string name="restore_on_boot_summary_off">起動時にトンネルを有効化しない</string>
|
||||
<string name="restore_on_boot_summary_on">起動時に、前回有効だったトンネルを有効化する</string>
|
||||
<string name="restore_on_boot_title">起動時に復元</string>
|
||||
<string name="save">保存</string>
|
||||
<string name="select_all">すべて選択</string>
|
||||
<string name="set_exclusions">対象外アプリを設定</string>
|
||||
<string name="settings">設定</string>
|
||||
<string name="shell_exit_status_read_error">シェルは終了ステータスを取得できません</string>
|
||||
<string name="shell_marker_count_error">シェルは 4 マーカーを期待していますが、 %d マーカーを受け取りました</string>
|
||||
<string name="shell_start_error">シェルの起動に失敗しました: %d</string>
|
||||
<string name="success_application_will_restart">成功。アプリケーションは再起動します…</string>
|
||||
<string name="toggle_all">すべてのトンネルの状態を切り替え</string>
|
||||
<string name="toggle_all">すべての状態を切り替え</string>
|
||||
<string name="toggle_error">WireGuard トンネルの状態切り替え時にエラー: %s</string>
|
||||
<string name="tools_installer_already">wg および wg-quick はインストール済みです</string>
|
||||
<string name="tools_installer_failure">コマンドラインツールをインストールできません(root権限がない?)</string>
|
||||
@@ -185,4 +209,8 @@
|
||||
<string name="zip_export_success">“%s” に保存</string>
|
||||
<string name="zip_export_summary">Zip ファイルはダウンロードフォルダに保存されます</string>
|
||||
<string name="zip_export_title">トンネル設定を zip ファイルにエクスポート</string>
|
||||
<string name="biometric_prompt_zip_exporter_title">トンネル設定をエクスポートするために認証を行います</string>
|
||||
<string name="biometric_prompt_private_key_title">秘密鍵を表示するために認証を行います</string>
|
||||
<string name="biometric_auth_error">認証に失敗</string>
|
||||
<string name="biometric_auth_error_reason">認証に失敗: %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
<!-- Theme variables -->
|
||||
<color name="list_multiselect_background">#1aeeeeee</color>
|
||||
<color name="status_bar_color">#21242424</color>
|
||||
<color name="navigation_bar_color">#aa242424</color>
|
||||
<color name="status_bar_color">@color/primary_color</color>
|
||||
<color name="navigation_bar_color">#aa212121</color>
|
||||
|
||||
<!-- Log viewer tag colors -->
|
||||
<color name="debug_tag_color">#aaaaaa</color>
|
||||
|
||||
@@ -2,31 +2,31 @@
|
||||
<resources>
|
||||
<plurals name="delete_error">
|
||||
<item quantity="one">Не удалось удалить %d туннель: %s</item>
|
||||
<item quantity="few">Не удалось удалить %d туннелей: %s</item>
|
||||
<item quantity="few">Не удалось удалить %d туннеля: %s</item>
|
||||
<item quantity="many">Не удалось удалить %d туннелей: %s</item>
|
||||
<item quantity="other">Не удалось удалить %d туннелей: %s</item>
|
||||
</plurals>
|
||||
<plurals name="delete_success">
|
||||
<item quantity="one">Успешно удален %d туннель</item>
|
||||
<item quantity="few">Успешно удалено %d туннелей</item>
|
||||
<item quantity="few">Успешно удалено %d туннеля</item>
|
||||
<item quantity="many">Успешно удалено %d туннелей</item>
|
||||
<item quantity="other">Успешно удалено %d туннелей</item>
|
||||
</plurals>
|
||||
<plurals name="delete_title">
|
||||
<item quantity="one">%d туннель выбран</item>
|
||||
<item quantity="few">%d туннелей выбрано</item>
|
||||
<item quantity="few">%d туннеля выбрано</item>
|
||||
<item quantity="many">%d туннелей выбрано</item>
|
||||
<item quantity="other">%d туннелей выбрано</item>
|
||||
</plurals>
|
||||
<plurals name="import_partial_success">
|
||||
<item quantity="one">Импортирован %1$d из %2$d туннелей</item>
|
||||
<item quantity="few">Импортировано %1$d из %2$d туннелей</item>
|
||||
<item quantity="few">Импортировано %1$d из %2$d туннеля</item>
|
||||
<item quantity="many">Импортировано %1$d из %2$d туннелей</item>
|
||||
<item quantity="other">Импортировано %1$d из %2$d туннелей</item>
|
||||
</plurals>
|
||||
<plurals name="import_total_success">
|
||||
<item quantity="one">Импортированный %d туннель</item>
|
||||
<item quantity="few">Импортировано %d туннелей</item>
|
||||
<item quantity="few">Импортировано %d туннеля</item>
|
||||
<item quantity="many">Импортировано %d туннелей</item>
|
||||
<item quantity="other">Импортировано %d туннелей</item>
|
||||
</plurals>
|
||||
@@ -36,8 +36,55 @@
|
||||
<item quantity="many">%d Исключенных приложений</item>
|
||||
<item quantity="other">%d Исключенных приложений</item>
|
||||
</plurals>
|
||||
<plurals name="set_included_applications">
|
||||
<item quantity="one">%d Включенное приложение</item>
|
||||
<item quantity="few">%d Включенных приложений</item>
|
||||
<item quantity="many">%d Включенных приложений</item>
|
||||
<item quantity="other">%d Включенных приложений</item>
|
||||
</plurals>
|
||||
<plurals name="n_excluded_applications">
|
||||
<item quantity="one">%d исключен</item>
|
||||
<item quantity="few">%d исключено</item>
|
||||
<item quantity="many">%d исключено</item>
|
||||
<item quantity="other">%d исключено</item>
|
||||
</plurals>
|
||||
<plurals name="n_included_applications">
|
||||
<item quantity="one">%d влючен</item>
|
||||
<item quantity="few">%d включено</item>
|
||||
<item quantity="many">%d включено</item>
|
||||
<item quantity="other">%d включено</item>
|
||||
</plurals>
|
||||
<string name="all_applications">Все приложения</string>
|
||||
<string name="exclude_from_tunnel">Исключить</string>
|
||||
<string name="include_in_tunnel">Только включить</string>
|
||||
<plurals name="include_n_applications">
|
||||
<item quantity="one">Включить %d приложение</item>
|
||||
<item quantity="few">Включить %d приложения</item>
|
||||
<item quantity="many">Включить %d приложений</item>
|
||||
<item quantity="other">Включить %d приложений</item>
|
||||
</plurals>
|
||||
<plurals name="exclude_n_applications">
|
||||
<item quantity="one">Исключить %d приложение</item>
|
||||
<item quantity="few">Исключить %d приложения</item>
|
||||
<item quantity="many">Исключить %d приложений</item>
|
||||
<item quantity="other">Исключить %d приложений</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_unit">
|
||||
<item quantity="one">каждую %d секунду</item>
|
||||
<item quantity="few">каждые %d секунды</item>
|
||||
<item quantity="many">каждые %d секунд</item>
|
||||
<item quantity="other">каждые %d секунд</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_suffix">
|
||||
<item quantity="one">секунда</item>
|
||||
<item quantity="few">секунд</item>
|
||||
<item quantity="many">секунд</item>
|
||||
<item quantity="other">секунд</item>
|
||||
</plurals>
|
||||
<string name="use_all_applications">Использовать все приложения</string>
|
||||
<string name="add_peer">Добавить пира</string>
|
||||
<string name="addresses">IP-адреса</string>
|
||||
<string name="applications">Приложения</string>
|
||||
<string name="allow_remote_control_intents_summary_off">Сторонние приложения не могут переключать туннели (рекомендуется)</string>
|
||||
<string name="allow_remote_control_intents_summary_on">Сторонние приложения могут переключать туннели (дополнительно)</string>
|
||||
<string name="allow_remote_control_intents_title">Доступ к другим приложениям</string>
|
||||
@@ -72,7 +119,7 @@
|
||||
<string name="create_downloads_file_error">Не удалось создать файл в каталоге загрузок</string>
|
||||
<string name="create_empty">Создать вручную</string>
|
||||
<string name="create_from_file">Импорт из файла или архива</string>
|
||||
<string name="create_from_qr_code">Сканировать из QR-кода</string>
|
||||
<string name="create_from_qr_code">Сканировать QR-код</string>
|
||||
<string name="create_output_dir_error">Не удалось создать выходной каталог</string>
|
||||
<string name="create_temp_dir_error">Не удалось создать временный локальный каталог</string>
|
||||
<string name="create_tunnel">Создать туннель</string>
|
||||
@@ -88,12 +135,11 @@
|
||||
<string name="error_root">Пожалуйста, получите root-доступ и попробуйте снова</string>
|
||||
<string name="error_up">Ошибка при запуске туннеля: %s</string>
|
||||
<string name="exclude_private_ips">Исключить частные IP-адреса</string>
|
||||
<string name="excluded_applications">Исключенные приложения</string>
|
||||
<string name="generate_new_private_key">Сгенерировать новый приватный ключ</string>
|
||||
<string name="generic_error">Неизвестная “%s” ошибка</string>
|
||||
<string name="hint_automatic">(авто)</string>
|
||||
<string name="hint_generated">(авто)</string>
|
||||
<string name="hint_optional">(авто)</string>
|
||||
<string name="hint_generated">(сгенерирован)</string>
|
||||
<string name="hint_optional">(опционально)</string>
|
||||
<string name="hint_optional_discouraged">(опционально, не рекомендуется)</string>
|
||||
<string name="hint_random">(авто)</string>
|
||||
<string name="illegal_filename_error">Неверное имя файла “%s”</string>
|
||||
@@ -129,7 +175,7 @@
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="multiple_tunnels_summary_off">Только один туннель, выключит другие</string>
|
||||
<string name="multiple_tunnels_summary_on">Могут быть включены несколько туннелей одновременно</string>
|
||||
<string name="multiple_tunnels_title">Несколько одновременных туннелей</string>
|
||||
<string name="multiple_tunnels_title">Несколько туннелей</string>
|
||||
<string name="name">Имя</string>
|
||||
<string name="no_config_error">Попытка поднять туннель без конфигурации</string>
|
||||
<string name="no_configs_error">Конфигурации не найдены</string>
|
||||
@@ -145,16 +191,15 @@
|
||||
<string name="permission_label">управлять туннелями WireGuard</string>
|
||||
<string name="persistent_keepalive">Постоянное соединение</string>
|
||||
<string name="pre_shared_key">Общий ключ</string>
|
||||
<string name="pre_shared_key_enabled">включено</string>
|
||||
<string name="private_key">Приватный ключ</string>
|
||||
<string name="public_key">Публичный ключ</string>
|
||||
<string name="public_key_description">Публичный ключ</string>
|
||||
<string name="qr_code_hint">Совет: генерировать с `qrencode -t ansiutf8 < tunnel.conf`.</string>
|
||||
<string name="restore_on_boot_summary_off">Не поднимать ранее выбранные туннели при загрузке</string>
|
||||
<string name="restore_on_boot_summary_on">Поднимать ранее выбранные туннели при загрузке</string>
|
||||
<string name="restore_on_boot_title">Восстанавливать при загрузке</string>
|
||||
<string name="save">Сохранить</string>
|
||||
<string name="select_all">Выбрать все</string>
|
||||
<string name="set_exclusions">ОК</string>
|
||||
<string name="settings">Настройки</string>
|
||||
<string name="shell_exit_status_read_error">Shell не может прочитать статус выхода</string>
|
||||
<string name="shell_marker_count_error">Shell ожидает 4 маркера, получено %d</string>
|
||||
@@ -203,4 +248,8 @@
|
||||
<string name="zip_export_success">Сохранено в “%s”</string>
|
||||
<string name="zip_export_summary">Zip-архив будет сохранен в папке загрузок</string>
|
||||
<string name="zip_export_title">Экспорт туннелей в zip-архив</string>
|
||||
<string name="biometric_prompt_zip_exporter_title">Аутентификация для экспорта туннелей</string>
|
||||
<string name="biometric_prompt_private_key_title">Аутентификация для просмотра приватного ключа</string>
|
||||
<string name="biometric_auth_error">Ошибка аутентификации</string>
|
||||
<string name="biometric_auth_error_reason">Ошибка аутентификации: %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="delete_error">
|
||||
<item quantity="one">%d tunela ni bilo mogoče izbrisati: %s</item>
|
||||
<item quantity="two">%d tunelov ni bilo mogoče izbrisati: %s</item>
|
||||
<item quantity="few">%d tunelov ni bilo mogoče izbrisati: %s</item>
|
||||
<item quantity="other">%d tunelov ni bilo mogoče izbrisati: %s</item>
|
||||
</plurals>
|
||||
<plurals name="delete_success">
|
||||
<item quantity="one">%d tunel uspešno izbrisan</item>
|
||||
<item quantity="two">%d tunela uspešno izbrisana</item>
|
||||
<item quantity="few">%d tuneli uspešno izbrisani</item>
|
||||
<item quantity="other">%d tunelov uspešno izbrisanih</item>
|
||||
</plurals>
|
||||
<plurals name="delete_title">
|
||||
<item quantity="one">%d tunel izbran</item>
|
||||
<item quantity="two">%d tunela izbrana</item>
|
||||
<item quantity="few">%d tuneli izbrani</item>
|
||||
<item quantity="other">%d tunelov izbranih</item>
|
||||
</plurals>
|
||||
<plurals name="import_partial_success">
|
||||
<item quantity="one">%1$d tunel od %2$d uvožen</item>
|
||||
<item quantity="two">%1$d tunela od %2$d uvožena</item>
|
||||
<item quantity="few">%1$d tuneli od %2$d uvoženi</item>
|
||||
<item quantity="other">%1$d tunelov od %2$d uvoženih</item>
|
||||
</plurals>
|
||||
<plurals name="import_total_success">
|
||||
<item quantity="one">%d tunel uvožen</item>
|
||||
<item quantity="two">%d tunela uvožena</item>
|
||||
<item quantity="few">%d tuneli uvoženi</item>
|
||||
<item quantity="other">%d tunelov uvoženih</item>
|
||||
</plurals>
|
||||
<plurals name="set_excluded_applications">
|
||||
<item quantity="one">%d izvzeta aplikacija</item>
|
||||
<item quantity="two">%d izvzeti aplikaciji</item>
|
||||
<item quantity="few">%d izvzete aplikacije</item>
|
||||
<item quantity="other">%d izvzetih aplikacij</item>
|
||||
</plurals>
|
||||
<plurals name="set_included_applications">
|
||||
<item quantity="one">%d vključena aplikacija</item>
|
||||
<item quantity="two">%d vključeni aplikaciji</item>
|
||||
<item quantity="few">%d vključene aplikacije</item>
|
||||
<item quantity="other">%d vključenih aplikacij</item>
|
||||
</plurals>
|
||||
<plurals name="n_excluded_applications">
|
||||
<item quantity="one">%d izvzeta</item>
|
||||
<item quantity="two">%d izvzeti</item>
|
||||
<item quantity="few">%d izvzete</item>
|
||||
<item quantity="other">%d izvzetih</item>
|
||||
</plurals>
|
||||
<plurals name="n_included_applications">
|
||||
<item quantity="one">%d vključena</item>
|
||||
<item quantity="two">%d vključeni</item>
|
||||
<item quantity="few">%d vključene</item>
|
||||
<item quantity="other">%d vključenih</item>
|
||||
</plurals>
|
||||
<string name="all_applications">Vse aplikacije</string>
|
||||
<string name="exclude_from_tunnel">Izvzemi</string>
|
||||
<string name="include_in_tunnel">Vključi samo za</string>
|
||||
<plurals name="include_n_applications">
|
||||
<item quantity="one">Vključi za %d aplikacijo</item>
|
||||
<item quantity="two">Vključi za %d aplikaciji</item>
|
||||
<item quantity="few">Vključi za %d aplikacije</item>
|
||||
<item quantity="other">Vključi za %d aplikacij</item>
|
||||
</plurals>
|
||||
<plurals name="exclude_n_applications">
|
||||
<item quantity="one">Izvzemi %d aplikacijo</item>
|
||||
<item quantity="two">Izvzemi %d aplikaciji</item>
|
||||
<item quantity="few">Izvzemi %d aplikacije</item>
|
||||
<item quantity="other">Izvzemi %d aplikacij</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_unit">
|
||||
<item quantity="one">vsako %d sekundo</item>
|
||||
<item quantity="two">vsaki %d sekundi</item>
|
||||
<item quantity="few">vsake %d sekunde</item>
|
||||
<item quantity="other">vsakih %d sekund</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_suffix">
|
||||
<item quantity="one">sekunda</item>
|
||||
<item quantity="two">sekundi</item>
|
||||
<item quantity="few">sekunde</item>
|
||||
<item quantity="other">sekund</item>
|
||||
</plurals>
|
||||
<string name="use_all_applications">Vse aplikacije</string>
|
||||
<string name="add_peer">Dodaj vrstnike</string>
|
||||
<string name="addresses">Naslovi</string>
|
||||
<string name="applications">Aplikacije</string>
|
||||
<string name="allow_remote_control_intents_summary_off">Zunanjim aplikacijam ni dovoljeno preklapljati tunelov (priporočeno)</string>
|
||||
<string name="allow_remote_control_intents_summary_on">Zunanje aplikacije lahko preklapljajo tunele (napredno)</string>
|
||||
<string name="allow_remote_control_intents_title">Dovoljeno upravljanje preko zunanjih aplikacij</string>
|
||||
<string name="allowed_ips">Dovoljeni naslovi IP</string>
|
||||
<string name="app_name">WireGuard</string>
|
||||
<string name="bad_config_context">%1$s-ov %2$s</string>
|
||||
<string name="bad_config_context_top_level">%s</string>
|
||||
<string name="bad_config_error">%1$s v %2$s</string>
|
||||
<string name="bad_config_explanation_pka">: Mora biti pozitivno in ne večje od 65535</string>
|
||||
<string name="bad_config_explanation_positive_number">: Mora biti pozitivno</string>
|
||||
<string name="bad_config_explanation_udp_port">: Mora biti veljavna številka vrat UDP</string>
|
||||
<string name="bad_config_reason_invalid_key">Neveljaven ključ</string>
|
||||
<string name="bad_config_reason_invalid_number">Neveljavno število</string>
|
||||
<string name="bad_config_reason_invalid_value">Neveljavna vrednost</string>
|
||||
<string name="bad_config_reason_missing_attribute">Manjka atribut</string>
|
||||
<string name="bad_config_reason_missing_section">Manjka odsek</string>
|
||||
<string name="bad_config_reason_syntax_error">Sintaktična napaka</string>
|
||||
<string name="bad_config_reason_unknown_attribute">Neznan atribut</string>
|
||||
<string name="bad_config_reason_unknown_section">Neznani odsek</string>
|
||||
<string name="bad_config_reason_value_out_of_range">Vrednost je zunaj veljavnega območja</string>
|
||||
<string name="bad_extension_error">Datoteka mora biti .conf ali .zip</string>
|
||||
<string name="cancel">Prekliči</string>
|
||||
<string name="config_delete_error">Konfiguracijske datoteke %s ni bilo mogoče izbrisati</string>
|
||||
<string name="config_exists_error">Konfiguracija za „%s“ že obstaja</string>
|
||||
<string name="config_file_exists_error">Konfiguracijska datoteka za „%s“ že obstaja</string>
|
||||
<string name="config_not_found_error">Konfiguracijske datoteke za „%s“ ni bilo mogoče najti</string>
|
||||
<string name="config_rename_error">Konfiguracijske datoteke „%s“ ni bilo mogoče preimenovati</string>
|
||||
<string name="config_save_error">Konfiguracijske datoteke za „%1$s“ ni bilo mogoče shraniti: %2$s</string>
|
||||
<string name="config_save_success">Konfiguracijska datoteka za „%s“ uspešno shranjena</string>
|
||||
<string name="create_activity_title">Ustvarite tunel WireGuard</string>
|
||||
<string name="create_bin_dir_error">Lokalnega imenika za aplikacijo ni bilo mogoče ustvariti</string>
|
||||
<string name="create_downloads_file_error">Datoteke v mapi prenosov ni bilo mogoče ustvariti</string>
|
||||
<string name="create_empty">Ustvari na novo</string>
|
||||
<string name="create_from_file">Uvozi iz datoteke ali arhiva</string>
|
||||
<string name="create_from_qr_code">Skreniraj kodo QR</string>
|
||||
<string name="create_output_dir_error">Izhodnega imenika ni bilo mogoče ustvariti</string>
|
||||
<string name="create_temp_dir_error">Lokalnega začasnega imenika ni bilo mogoče ustvariti</string>
|
||||
<string name="create_tunnel">Ustvari tunel</string>
|
||||
<string name="dark_theme_summary_off">V uporabi svetla (dnevna) tema</string>
|
||||
<string name="dark_theme_summary_on">V uporabi temna (nočna) tema</string>
|
||||
<string name="dark_theme_title">Uporabi temno temo</string>
|
||||
<string name="delete">Izbriši</string>
|
||||
<string name="dns_servers">Strežniki DNS</string>
|
||||
<string name="edit">Uredi</string>
|
||||
<string name="endpoint">Končna točka</string>
|
||||
<string name="error_down">Napaka pri zaključevanju tunela: %s</string>
|
||||
<string name="error_fetching_apps">Napaka pri poizvedovanju seznama aplikacij: %s</string>
|
||||
<string name="error_root">Prosim omogočite dostop root in poskusite ponovno</string>
|
||||
<string name="error_up">Napaka pri vzpostavitvi tunela: %s</string>
|
||||
<string name="exclude_private_ips">Izvzemi zasebne naslove IP</string>
|
||||
<string name="generate_new_private_key">Izdelaj nov zasebni ključ</string>
|
||||
<string name="generic_error">Neznana napaka „%s“</string>
|
||||
<string name="hint_automatic">(samodejno)</string>
|
||||
<string name="hint_generated">(izdelano)</string>
|
||||
<string name="hint_optional">(izbirno)</string>
|
||||
<string name="hint_optional_discouraged">(izbirno, ni priporočljivo)</string>
|
||||
<string name="hint_random">(naključno)</string>
|
||||
<string name="illegal_filename_error">Neveljavno ime datoteke „%s“</string>
|
||||
<string name="import_error">Tunela ni bilo mogoče uvoziti: %s</string>
|
||||
<string name="import_from_qr_code">Uvozi tunel iz kode QR</string>
|
||||
<string name="import_success">„%s“ uvožen</string>
|
||||
<string name="interface_title">Vmesnik</string>
|
||||
<string name="key_contents_error">Napačni znaki v ključu</string>
|
||||
<string name="key_length_error">Napačna dolžina ključa</string>
|
||||
<string name="key_length_explanation_base64">: WireGuardovi ključi base64 morajo vsebovati 44 znakov (32 bajtov)</string>
|
||||
<string name="key_length_explanation_binary">: WireGuardovi ključi morajo biti veliki 32 bajtov</string>
|
||||
<string name="key_length_explanation_hex">: WireGuardovi ključi hex morajo biti veliki 64 znakov (32 bajtov)</string>
|
||||
<string name="listen_port">Vrata poslušanja</string>
|
||||
<string name="log_export_error">Dnevnika ni bilo mogoče izvoziti: %s</string>
|
||||
<string name="log_export_subject">Dnevniška datoteka WireGuard za Android</string>
|
||||
<string name="log_export_success">Shranjeno v „%s“</string>
|
||||
<string name="log_export_title">Izvozi dnevniško datoteko</string>
|
||||
<string name="log_saver_activity_label">Shrani dnevnik</string>
|
||||
<string name="log_viewer_pref_summary">Dnevniki lahko pomagajo pri razhroščevanju</string>
|
||||
<string name="log_viewer_pref_title">Prikaži dnevnik aplikacije</string>
|
||||
<string name="log_viewer_title">Dnevnik</string>
|
||||
<string name="logcat_error">Ukaza logcat ni bilo mogoče izvesti: </string>
|
||||
<string name="module_disabler_disabled_summary">Eksperimentalni modul jedra lahko izboljša zmogljivost</string>
|
||||
<string name="module_disabler_disabled_title">Omogoči zaledje za modul jedra</string>
|
||||
<string name="module_disabler_enabled_summary">Počasnejše uporabniško zaledje lahko izboljša stabilnost</string>
|
||||
<string name="module_disabler_enabled_title">Onemogoči zaledje za modul jedra</string>
|
||||
<string name="module_installer_error">Nekaj je šlo narobe, prosimo poskusite znova</string>
|
||||
<string name="module_installer_initial">Eksperimentalni modul jedra lahko izboljša zmogljivost</string>
|
||||
<string name="module_installer_not_found">Za vašo napravo ni razpoložljivih modulov</string>
|
||||
<string name="module_installer_title">Prenesi in namesti modul jedra</string>
|
||||
<string name="module_installer_working">Prenašanje in nameščanje …</string>
|
||||
<string name="module_version_error">Verzije modula jedra ni bilo mogoče določiti</string>
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="multiple_tunnels_summary_off">Vklop enega tunela bo izklopil druge</string>
|
||||
<string name="multiple_tunnels_summary_on">Več tunelov je lahko aktiviranih sočasno</string>
|
||||
<string name="multiple_tunnels_title">Dovoli več sočasnih tunelov</string>
|
||||
<string name="name">Ime</string>
|
||||
<string name="no_config_error">Poskus vzpostavitve tunela brez konfiguracije</string>
|
||||
<string name="no_configs_error">Ni najdenih konfiguracij</string>
|
||||
<string name="no_tunnels_error">Ni tunelov</string>
|
||||
<string name="parse_error_generic">niz</string>
|
||||
<string name="parse_error_inet_address">Naslov IP</string>
|
||||
<string name="parse_error_inet_endpoint">končna točka</string>
|
||||
<string name="parse_error_inet_network">Omrežje IP</string>
|
||||
<string name="parse_error_integer">število</string>
|
||||
<string name="parse_error_reason">Ni mogoče razčleniti %1$s „%2$s“</string>
|
||||
<string name="peer">Vrstnik</string>
|
||||
<string name="permission_description">upravljanje tunelov WireGuard, aktivacija in deaktivacija tunela, zaradi česar bo internetni promet mogoče napačno usmerjen</string>
|
||||
<string name="permission_label">upravljanje tunelov WireGuard</string>
|
||||
<string name="persistent_keepalive">Trajno ohranjanje povezave</string>
|
||||
<string name="pre_shared_key">Ključ v skupni rabi</string>
|
||||
<string name="pre_shared_key_enabled">omogočeno</string>
|
||||
<string name="private_key">Zasebni ključ</string>
|
||||
<string name="public_key">Javni ključ</string>
|
||||
<string name="qr_code_hint">Namig: izdelajte z `qrencode -t ansiutf8 < tunnel.conf`.</string>
|
||||
<string name="restore_on_boot_summary_off">Pri zagonu telefona omogočeni tuneli ne bodo aktivirani</string>
|
||||
<string name="restore_on_boot_summary_on">Pri zagonu telefona bodo omogočeni tuneli aktivirani</string>
|
||||
<string name="restore_on_boot_title">Ponovna vzpostavitev ob zagonu</string>
|
||||
<string name="save">Shrani</string>
|
||||
<string name="select_all">Izberi vse</string>
|
||||
<string name="settings">Nastavitve</string>
|
||||
<string name="shell_exit_status_read_error">Lupina ne more prebrati izhodnega statusa</string>
|
||||
<string name="shell_marker_count_error">Lupina je pričakovala 4 oznake, dobila pa je %d</string>
|
||||
<string name="shell_start_error">Lupina se ni mogla zagnati: %d</string>
|
||||
<string name="success_application_will_restart">Uspešno. Aplikacija se bo znova zagnala …</string>
|
||||
<string name="toggle_all">Preklopi vse</string>
|
||||
<string name="toggle_error">Napaka pri preklopu tunela WireGuard: %s</string>
|
||||
<string name="tools_installer_already">wg in wg-quick sta že nameščena</string>
|
||||
<string name="tools_installer_failure">Orodij ukazne vrstice ni bilo mogoče namestiti (ni dostopa root?)</string>
|
||||
<string name="tools_installer_initial">Namestitev izbirnih orodij za skripte</string>
|
||||
<string name="tools_installer_initial_magisk">Namestitev izbirnih orodij za skripte kot modula Magisk</string>
|
||||
<string name="tools_installer_initial_system">Namestitev izbirnih orodij za skripte na sistemsko particijo</string>
|
||||
<string name="tools_installer_success_magisk">wg in wg-quick kot modul Magisk nameščen (zahtevan ponovni zagon)</string>
|
||||
<string name="tools_installer_success_system">wg und wg-quick nameščena na sistemsko particijo</string>
|
||||
<string name="tools_installer_title">Namesti orodja za ukazno vrstico</string>
|
||||
<string name="tools_installer_working">Nameščam wg in wg-quick</string>
|
||||
<string name="tools_unavailable_error">Zahtevana orodja niso na voljo</string>
|
||||
<string name="transfer">Prenos</string>
|
||||
<string name="transfer_bytes">%d B</string>
|
||||
<string name="transfer_gibibytes">%.2f GiB</string>
|
||||
<string name="transfer_kibibytes">%.2f KiB</string>
|
||||
<string name="transfer_mibibytes">%.2f MiB</string>
|
||||
<string name="transfer_rx_tx">prejeto: %1$s, poslano: %2$s</string>
|
||||
<string name="transfer_tibibytes">%.2f TiB</string>
|
||||
<string name="tun_create_error">Naprave tun ni bilo mogoče ustvariti</string>
|
||||
<string name="tunnel_config_error">Tunela ni bilo mogoče nastaviti (wg-quick je vrnil %d)</string>
|
||||
<string name="tunnel_create_error">Tunela ni bilo mogoče ustvariti: %s</string>
|
||||
<string name="tunnel_create_success">Tunel „%s“ uspešno ustvarjen</string>
|
||||
<string name="tunnel_error_already_exists">Tunel „%s“ že obstaja</string>
|
||||
<string name="tunnel_error_invalid_name">Neveljavno ime</string>
|
||||
<string name="tunnel_list_placeholder">Dodaj tunel s klikom na modri gumb</string>
|
||||
<string name="tunnel_name">Ime tunela</string>
|
||||
<string name="tunnel_on_error">Tunela ni bilo mogoče vključiti (wgTurnOn je vrnil %d)</string>
|
||||
<string name="tunnel_rename_error">Tunela ni bilo mogoče preimenovati: %s</string>
|
||||
<string name="tunnel_rename_success">Tunel uspešno preimenovan v „%s“</string>
|
||||
<string name="type_name_go_userspace">Uporabniški prostor Go</string>
|
||||
<string name="type_name_kernel_module">Modul jedra</string>
|
||||
<string name="unknown_error">Neznana napaka</string>
|
||||
<string name="version_summary">Zaledje %1$s v%2$s</string>
|
||||
<string name="version_summary_checking">Preverjam verzijo zaledja %s</string>
|
||||
<string name="version_summary_unknown">Neznana verzija %s</string>
|
||||
<string name="version_title">WireGuard za Android v%s</string>
|
||||
<string name="vpn_not_authorized_error">Uporabnik ni odobril storitve VPN</string>
|
||||
<string name="vpn_start_error">Storitve Android VPN ni bilo mogoče zagnati</string>
|
||||
<string name="zip_export_error">Tunelov ni bilo mogoče izvoziti: %s</string>
|
||||
<string name="zip_export_success">Shranjeno v „%s“</string>
|
||||
<string name="zip_export_summary">Datoteka zip je bila shranjena v mapo Prenosi</string>
|
||||
<string name="zip_export_title">Izvozi tunele v datoteko zip</string>
|
||||
<string name="biometric_prompt_zip_exporter_title">Za izvoz tunelov se prijavite</string>
|
||||
<string name="biometric_prompt_private_key_title">Za ogled zasebnega ključa se prijavite</string>
|
||||
<string name="biometric_auth_error">Napaka pri overovljanju</string>
|
||||
<string name="biometric_auth_error_reason">Napaka pri overovljanju: %s</string>
|
||||
</resources>
|
||||
@@ -20,7 +20,7 @@
|
||||
<item name="android:windowBackground">@color/primary_color</item>
|
||||
<item name="alertDialogTheme">@style/AppTheme.Dialog</item>
|
||||
<item name="materialAlertDialogTheme">@style/AppTheme.Dialog</item>
|
||||
<item name="actionBarPopupTheme">@style/ThemeOverlay.MaterialComponents.ActionBar</item>
|
||||
<item name="textInputStyle">@style/TextInputLayoutBase</item>
|
||||
<item name="materialCardViewStyle">@style/AppTheme.MaterialCardView</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<item name="android:windowBackground">@color/primary_color</item>
|
||||
<item name="alertDialogTheme">@style/AppTheme.Dialog</item>
|
||||
<item name="materialAlertDialogTheme">@style/AppTheme.Dialog</item>
|
||||
<item name="actionBarPopupTheme">@style/ThemeOverlay.MaterialComponents.ActionBar</item>
|
||||
<item name="textInputStyle">@style/TextInputLayoutBase</item>
|
||||
<item name="materialCardViewStyle">@style/AppTheme.MaterialCardView</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -16,10 +16,36 @@
|
||||
<item quantity="other">导入了 %d 项</item>
|
||||
</plurals>
|
||||
<plurals name="set_excluded_applications">
|
||||
<item quantity="other">%d 个排除应用</item>
|
||||
<item quantity="other">对 %d 个应用不生效</item>
|
||||
</plurals>
|
||||
<plurals name="set_included_applications">
|
||||
<item quantity="other">对 %d 个应用生效</item>
|
||||
</plurals>
|
||||
<plurals name="n_excluded_applications">
|
||||
<item quantity="other">对 %d 个应用不生效</item>
|
||||
</plurals>
|
||||
<plurals name="n_included_applications">
|
||||
<item quantity="other">对 %d 个应用生效</item>
|
||||
</plurals>
|
||||
<string name="all_applications">对全部应用生效</string>
|
||||
<string name="exclude_from_tunnel">不生效</string>
|
||||
<string name="include_in_tunnel">生效</string>
|
||||
<plurals name="include_n_applications">
|
||||
<item quantity="other">设定 %d 个应用</item>
|
||||
</plurals>
|
||||
<plurals name="exclude_n_applications">
|
||||
<item quantity="other">设定 %d 个应用</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_unit">
|
||||
<item quantity="other">每隔 %d 秒</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_suffix">
|
||||
<item quantity="other">秒</item>
|
||||
</plurals>
|
||||
<string name="use_all_applications">不设定过滤</string>
|
||||
<string name="add_peer">添加节点</string>
|
||||
<string name="addresses">局域网 IP 地址</string>
|
||||
<string name="applications">应用过滤</string>
|
||||
<string name="allow_remote_control_intents_summary_off">外部应用当前不能控制隧道(不建议勾选)</string>
|
||||
<string name="allow_remote_control_intents_summary_on">外部应用当前能够控制隧道(面向高级用户)</string>
|
||||
<string name="allow_remote_control_intents_title">授权外部控制</string>
|
||||
@@ -70,7 +96,6 @@
|
||||
<string name="error_root">请获取 root 权限并重试</string>
|
||||
<string name="error_up">建立连接时出错:%s</string>
|
||||
<string name="exclude_private_ips">排除局域网</string>
|
||||
<string name="excluded_applications">排除的应用</string>
|
||||
<string name="generate_new_private_key">生成新的私钥</string>
|
||||
<string name="generic_error">未知的 “%s” 错误</string>
|
||||
<string name="hint_automatic">(自动)</string>
|
||||
@@ -127,16 +152,15 @@
|
||||
<string name="permission_label">控制 WireGuard 隧道</string>
|
||||
<string name="persistent_keepalive">连接保活间隔</string>
|
||||
<string name="pre_shared_key">预共享密钥</string>
|
||||
<string name="pre_shared_key_enabled">已启用</string>
|
||||
<string name="private_key">私钥</string>
|
||||
<string name="public_key">公钥</string>
|
||||
<string name="public_key_description">公钥</string>
|
||||
<string name="qr_code_hint">提示:使用命令 `qrencode -t ansiutf8 < tunnel.conf` 生成二维码</string>
|
||||
<string name="restore_on_boot_summary_off">未启用</string>
|
||||
<string name="restore_on_boot_summary_on">设备启动时自动开启上次使用的隧道</string>
|
||||
<string name="restore_on_boot_title">启动时恢复</string>
|
||||
<string name="save">保存</string>
|
||||
<string name="select_all">全选</string>
|
||||
<string name="set_exclusions">确定</string>
|
||||
<string name="settings">设置</string>
|
||||
<string name="shell_exit_status_read_error">Shell 无法读取退出状态</string>
|
||||
<string name="shell_marker_count_error">Shell 应获取 4 个标记,获取到 %d 个</string>
|
||||
@@ -184,5 +208,9 @@
|
||||
<string name="zip_export_error">无法导出隧道配置:%s</string>
|
||||
<string name="zip_export_success">已保存至 “%s”</string>
|
||||
<string name="zip_export_summary">zip 压缩包将保存至下载文件夹</string>
|
||||
<string name="zip_export_title">导出隧道配置为 zip 压缩包</string>
|
||||
<string name="zip_export_title">导出隧道配置</string>
|
||||
<string name="biometric_prompt_zip_exporter_title">导出配置需要认证指纹</string>
|
||||
<string name="biometric_prompt_private_key_title">查看私钥需要认证指纹</string>
|
||||
<string name="biometric_auth_error">认证失败</string>
|
||||
<string name="biometric_auth_error_reason">认证失败:%s</string>
|
||||
</resources>
|
||||
|
||||
@@ -24,8 +24,41 @@
|
||||
<item quantity="one">%d Excluded Application</item>
|
||||
<item quantity="other">%d Excluded Applications</item>
|
||||
</plurals>
|
||||
<plurals name="set_included_applications">
|
||||
<item quantity="one">%d Included Application</item>
|
||||
<item quantity="other">%d Included Applications</item>
|
||||
</plurals>
|
||||
<plurals name="n_excluded_applications">
|
||||
<item quantity="one">%d excluded</item>
|
||||
<item quantity="other">%d excluded</item>
|
||||
</plurals>
|
||||
<plurals name="n_included_applications">
|
||||
<item quantity="one">%d included</item>
|
||||
<item quantity="other">%d included</item>
|
||||
</plurals>
|
||||
<string name="all_applications">All Applications</string>
|
||||
<string name="exclude_from_tunnel">Exclude</string>
|
||||
<string name="include_in_tunnel">Include only</string>
|
||||
<plurals name="include_n_applications">
|
||||
<item quantity="one">Include %d app</item>
|
||||
<item quantity="other">Include %d apps</item>
|
||||
</plurals>
|
||||
<plurals name="exclude_n_applications">
|
||||
<item quantity="one">Exclude %d app</item>
|
||||
<item quantity="other">Exclude %d apps</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_unit">
|
||||
<item quantity="one">every second</item>
|
||||
<item quantity="other">every %d seconds</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_suffix">
|
||||
<item quantity="one">second</item>
|
||||
<item quantity="other">seconds</item>
|
||||
</plurals>
|
||||
<string name="use_all_applications">Use all apps</string>
|
||||
<string name="add_peer">Add peer</string>
|
||||
<string name="addresses">Addresses</string>
|
||||
<string name="applications">Applications</string>
|
||||
<string name="allow_remote_control_intents_summary_off">External apps may not toggle tunnels (recommended)</string>
|
||||
<string name="allow_remote_control_intents_summary_on">External apps may toggle tunnels (advanced)</string>
|
||||
<string name="allow_remote_control_intents_title">Allow remote control apps</string>
|
||||
@@ -68,6 +101,8 @@
|
||||
<string name="dark_theme_summary_on">Currently using dark (night) theme</string>
|
||||
<string name="dark_theme_title">Use dark theme</string>
|
||||
<string name="delete">Delete</string>
|
||||
<string name="disable_config_export_title">Disable config exporting</string>
|
||||
<string name="disable_config_export_description">Disabling config exporting makes private keys less accessible</string>
|
||||
<string name="dns_servers">DNS servers</string>
|
||||
<string name="edit">Edit</string>
|
||||
<string name="endpoint">Endpoint</string>
|
||||
@@ -76,7 +111,6 @@
|
||||
<string name="error_root">Please obtain root access and try again</string>
|
||||
<string name="error_up">Error bringing up tunnel: %s</string>
|
||||
<string name="exclude_private_ips">Exclude private IPs</string>
|
||||
<string name="excluded_applications">Excluded Applications</string>
|
||||
<string name="generate_new_private_key">Generate new private key</string>
|
||||
<string name="generic_error">Unknown “%s” error</string>
|
||||
<string name="hint_automatic">(auto)</string>
|
||||
@@ -133,16 +167,15 @@
|
||||
<string name="permission_label">control WireGuard tunnels</string>
|
||||
<string name="persistent_keepalive">Persistent keepalive</string>
|
||||
<string name="pre_shared_key">Pre-shared key</string>
|
||||
<string name="pre_shared_key_enabled">enabled</string>
|
||||
<string name="private_key">Private key</string>
|
||||
<string name="public_key">Public key</string>
|
||||
<string name="public_key_description">Public key</string>
|
||||
<string name="qr_code_hint">Tip: generate with `qrencode -t ansiutf8 < tunnel.conf`.</string>
|
||||
<string name="restore_on_boot_summary_off">Will not bring up enabled tunnels at boot</string>
|
||||
<string name="restore_on_boot_summary_on">Will bring up enabled tunnels at boot</string>
|
||||
<string name="restore_on_boot_title">Restore on boot</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="select_all">Select all</string>
|
||||
<string name="set_exclusions">Set Exclusions</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="shell_exit_status_read_error">Shell cannot read exit status</string>
|
||||
<string name="shell_marker_count_error">Shell expected 4 markers, received %d</string>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user