mirror of
https://github.com/amnezia-vpn/amneziawg-android.git
synced 2026-06-02 06:23:39 +02:00
Compare commits
135 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f670ff22c6 | |||
| cb3194f10a | |||
| f6b2bbf433 | |||
| 56a4862442 | |||
| 20390d65c8 | |||
| 177457e67b | |||
| 8caec4d739 | |||
| fb819b99a4 | |||
| fe82037f06 | |||
| c2aa1b21f8 | |||
| d69415b55a | |||
| 4fae2d1255 | |||
| a300f269f1 | |||
| 961cba3f7c | |||
| 6bc7386bff | |||
| 5fa08f286e | |||
| 35f868733c | |||
| e71b3d2583 | |||
| 755148242c | |||
| 15fea6f02f | |||
| cb2842e8ef | |||
| 106b67d892 | |||
| 996587f792 | |||
| 3a4bf35c77 | |||
| 46b37c0c26 | |||
| 5b5ba88a97 | |||
| ceb3095a0a | |||
| a31f0cf788 | |||
| 1dc74b171c | |||
| 9266487fe5 | |||
| 5d7ce139bc | |||
| ddb6c87ebf | |||
| 8a6f8f73cd | |||
| f4fc15538d | |||
| 938399d881 | |||
| 53ca421a85 | |||
| 32778d1c03 | |||
| a870bf6e04 | |||
| 7a8f708157 | |||
| e729c5dc51 | |||
| 4bf34c49b7 | |||
| 05511d4900 | |||
| 15da17b595 | |||
| b3c43e428f | |||
| 7bec539722 | |||
| a8dfebb086 | |||
| e72b4fc144 | |||
| 03189e7b20 | |||
| 10bb413187 | |||
| 1c814310b9 | |||
| 3fe9e3162f | |||
| 6da6f7886a | |||
| 8c2029870f | |||
| a5031a44a0 | |||
| 44b27fe472 | |||
| 93fb3b345b | |||
| 8b596697b7 | |||
| c536bbb7e9 | |||
| a978aac129 | |||
| eb8cab4110 | |||
| 0a36d9a5e9 | |||
| 309571039d | |||
| d56f2fb1bb | |||
| 9df8e5e239 | |||
| 444a86cc9f | |||
| 382e10e103 | |||
| dc002d77fa | |||
| aaa55c0dcc | |||
| 0ad3781ae5 | |||
| d738161a2e | |||
| 52c2e9cd24 | |||
| 5fd1a32ae4 | |||
| 655a853857 | |||
| 847da23300 | |||
| d5c07374ff | |||
| 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 |
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" />
|
||||
|
||||
@@ -22,12 +22,16 @@ The tunnel library is [on JCenter](https://bintray.com/wireguard/wireguard-andro
|
||||
implementation 'com.wireguard.android:tunnel:$wireguardTunnelVersion'
|
||||
```
|
||||
|
||||
The library makes use of Java 8 features, so be sure to support those in your gradle configuration:
|
||||
The library makes use of Java 8 features, so be sure to support those in your gradle configuration with desugaring:
|
||||
|
||||
```
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
coreLibraryDesugaringEnabled = true
|
||||
}
|
||||
dependencies {
|
||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.0.10"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+56
-27
@@ -1,33 +1,24 @@
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
}
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
agpVersion = '4.0.1'
|
||||
activityVersion = '1.2.0-beta02'
|
||||
agpVersion = '4.1.1'
|
||||
annotationsVersion = '1.1.0'
|
||||
appcompatVersion = '1.1.0'
|
||||
bintrayPluginVersion = '1.8.4'
|
||||
biometricVersion = '1.0.1'
|
||||
appcompatVersion = '1.2.0'
|
||||
biometricVersion = '1.1.0-rc01'
|
||||
collectionVersion = '1.1.0'
|
||||
constraintLayoutVersion = '1.1.3'
|
||||
constraintLayoutVersion = '2.0.4'
|
||||
coordinatorLayoutVersion = '1.1.0'
|
||||
coreKtxVersion = '1.3.0'
|
||||
coroutinesVersion = '1.3.7'
|
||||
eddsaVersion = '0.3.0'
|
||||
fragmentVersion = '1.2.5'
|
||||
coreKtxVersion = '1.3.2'
|
||||
coroutinesVersion = '1.4.2'
|
||||
datastoreVersion = '1.0.0-alpha02'
|
||||
desugarVersion = '1.0.10'
|
||||
fragmentVersion = '1.3.0-beta02'
|
||||
jsr305Version = '3.0.2'
|
||||
junitVersion = '4.13'
|
||||
kotlinVersion = '1.3.72'
|
||||
materialComponentsVersion = '1.2.0-alpha06'
|
||||
mavenPluginVersion = '2.1'
|
||||
junitVersion = '4.13.1'
|
||||
kotlinVersion = '1.4.21'
|
||||
lifecycleRuntimeKtxVersion = '2.3.0-beta01'
|
||||
materialComponentsVersion = '1.3.0-beta01'
|
||||
preferenceVersion = '1.1.1'
|
||||
streamsupportVersion = '1.7.2'
|
||||
threetenabpVersion = '1.2.4'
|
||||
zxingEmbeddedVersion = '3.6.0'
|
||||
|
||||
groupName = 'com.wireguard.android'
|
||||
@@ -35,8 +26,6 @@ buildscript {
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:$agpVersion"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
classpath "com.github.dcendents:android-maven-gradle-plugin:$mavenPluginVersion"
|
||||
classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:$bintrayPluginVersion"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
@@ -44,15 +33,55 @@ buildscript {
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "de.undercouch.download" version "4.1.1"
|
||||
}
|
||||
|
||||
task downloadCrowdin(type: Download) {
|
||||
src 'https://crowdin.com/backend/download/project/wireguard.zip'
|
||||
dest file('build/translations.zip')
|
||||
overwrite true
|
||||
}
|
||||
|
||||
task cleanCrowdin(type: Delete) {
|
||||
delete 'ui/src/main/res/values-*/strings.xml'
|
||||
}
|
||||
|
||||
task extractCrowdin(type: Copy, dependsOn: ['downloadCrowdin', 'cleanCrowdin']) {
|
||||
mustRunAfter 'downloadCrowdin'
|
||||
from zipTree(file('build/translations.zip'))
|
||||
into file('build/translations')
|
||||
doFirst {
|
||||
delete 'build/translations'
|
||||
}
|
||||
}
|
||||
|
||||
task crowdin(type: Copy, dependsOn: ['extractCrowdin']) {
|
||||
mustRunAfter 'extractCrowdin'
|
||||
from 'build/translations/wireguard-android/ui/src/main/res'
|
||||
into 'ui/src/main/res/'
|
||||
doLast {
|
||||
delete 'build/translations'
|
||||
delete 'build/translations.zip'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
tasks {
|
||||
wrapper {
|
||||
gradleVersion = "6.5.1"
|
||||
gradleVersion = "6.7.1"
|
||||
distributionType = Wrapper.DistributionType.ALL
|
||||
distributionSha256Sum = "143a28f54f1ae93ef4f72d862dbc3c438050d81bb45b4601eb7076e998362920"
|
||||
distributionSha256Sum = "22449f5231796abd892c98b2a07c9ceebe4688d192cd2d6763f8e3bf8acbedeb"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,6 @@ org.gradle.jvmargs=-Xmx1536m
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
# R8 Full mode
|
||||
android.enableR8.fullMode=true
|
||||
|
||||
# https://jakewharton.com/increased-accuracy-of-aapt2-keep-rules/
|
||||
android.useMinimalKeepRules=true
|
||||
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+2
-2
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=143a28f54f1ae93ef4f72d862dbc3c438050d81bb45b4601eb7076e998362920
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-all.zip
|
||||
distributionSha256Sum=22449f5231796abd892c98b2a07c9ceebe4688d192cd2d6763f8e3bf8acbedeb
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -130,7 +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
|
||||
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
set -ex
|
||||
curl -Lo - https://crowdin.com/backend/download/project/wireguard.zip | bsdtar -C ui/src/main/res -x -f - --strip-components 5 wireguard-android
|
||||
find ui/src/main/res -name strings.xml -exec bash -c '[[ $(xmllint --xpath "count(//resources/*)" {}) -ne 0 ]] || rm -rf "$(dirname {})"' \;
|
||||
+3
-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
|
||||
}
|
||||
@@ -47,16 +46,14 @@ android {
|
||||
}
|
||||
lintOptions {
|
||||
disable('LongLogTag')
|
||||
disable('NewApi') // Desugaring!
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
|
||||
+44
-48
@@ -1,53 +1,53 @@
|
||||
apply plugin: 'com.github.dcendents.android-maven'
|
||||
apply plugin: 'com.jfrog.bintray'
|
||||
apply plugin: 'maven-publish'
|
||||
|
||||
install {
|
||||
repositories.mavenInstaller {
|
||||
pom.project {
|
||||
name 'WireGuard Tunnel Library'
|
||||
description 'Embeddable tunnel library for WireGuard for Android'
|
||||
url 'https://www.wireguard.com/'
|
||||
afterEvaluate {
|
||||
publishing {
|
||||
publications {
|
||||
release(MavenPublication) {
|
||||
groupId = groupName
|
||||
artifactId = 'tunnel'
|
||||
version wireguardVersionName
|
||||
|
||||
packaging 'aar'
|
||||
groupId groupName
|
||||
artifactId 'tunnel'
|
||||
version wireguardVersionName
|
||||
artifact sourcesJar
|
||||
artifact javadocJar
|
||||
|
||||
licenses {
|
||||
license {
|
||||
name 'The Apache Software License, Version 2.0'
|
||||
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
|
||||
distribution 'repo'
|
||||
from components.getByName("release")
|
||||
|
||||
pom {
|
||||
name = 'WireGuard Tunnel Library'
|
||||
description = 'Embeddable tunnel library for WireGuard for Android'
|
||||
url = 'https://www.wireguard.com/'
|
||||
|
||||
licenses {
|
||||
license {
|
||||
name = 'The Apache Software License, Version 2.0'
|
||||
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
|
||||
distribution = 'repo'
|
||||
}
|
||||
}
|
||||
scm {
|
||||
connection = 'scm:git:https://git.zx2c4.com/wireguard-android'
|
||||
developerConnection = 'scm:git:https://git.zx2c4.com/wireguard-android'
|
||||
url = 'https://git.zx2c4.com/wireguard-android'
|
||||
}
|
||||
developers {
|
||||
organization {
|
||||
name = 'WireGuard'
|
||||
url = 'https://www.wireguard.com/'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
scm {
|
||||
connection 'scm:git:https://git.zx2c4.com/wireguard-android'
|
||||
url 'https://git.zx2c4.com/wireguard-android'
|
||||
}
|
||||
organization {
|
||||
name 'WireGuard'
|
||||
url 'https://www.wireguard.com/'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bintray {
|
||||
user = hasProperty('BINTRAY_USER') ? getProperty('BINTRAY_USER') : System.getenv('BINTRAY_USER')
|
||||
key = hasProperty('BINTRAY_KEY') ? getProperty('BINTRAY_KEY') : System.getenv('BINTRAY_KEY')
|
||||
|
||||
configurations = [ 'archives' ]
|
||||
|
||||
pkg {
|
||||
repo = 'wireguard-android'
|
||||
name = 'wireguard-android'
|
||||
userOrg = 'wireguard'
|
||||
licenses = [ 'Apache-2.0' ]
|
||||
vcsUrl = 'https://git.zx2c4.com/wireguard-android'
|
||||
publish = true
|
||||
|
||||
version {
|
||||
name = wireguardVersionName
|
||||
repositories {
|
||||
maven {
|
||||
name = "bintray"
|
||||
url = uri("https://api.bintray.com/maven/wireguard/wireguard-android/wireguard-android/;publish=1;override=0")
|
||||
credentials {
|
||||
username = hasProperty('BINTRAY_USER') ? getProperty('BINTRAY_USER') : System.getenv('BINTRAY_USER')
|
||||
password = hasProperty('BINTRAY_KEY') ? getProperty('BINTRAY_KEY') : System.getenv('BINTRAY_KEY')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,9 +68,5 @@ android.libraryVariants.all { variant ->
|
||||
archiveClassifier = 'sources'
|
||||
from android.sourceSets.main.java.srcDirs
|
||||
}
|
||||
artifacts {
|
||||
archives sourcesJar
|
||||
archives javadocJar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -34,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";
|
||||
@@ -44,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;
|
||||
}
|
||||
@@ -65,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) {
|
||||
@@ -75,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();
|
||||
@@ -124,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);
|
||||
@@ -167,8 +214,10 @@ public final class GoBackend implements Backend {
|
||||
throw new BackendException(Reason.VPN_NOT_AUTHORIZED);
|
||||
|
||||
final VpnService service;
|
||||
if (!vpnService.isDone())
|
||||
startVpnService();
|
||||
if (!vpnService.isDone()) {
|
||||
Log.d(TAG, "Requesting to start VpnService");
|
||||
context.startService(new Intent(context, VpnService.class));
|
||||
}
|
||||
|
||||
try {
|
||||
service = vpnService.get(2, TimeUnit.SECONDS);
|
||||
@@ -255,11 +304,10 @@ public final class GoBackend implements Backend {
|
||||
tunnel.onStateChange(state);
|
||||
}
|
||||
|
||||
private void startVpnService() {
|
||||
Log.d(TAG, "Requesting to start VpnService");
|
||||
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();
|
||||
}
|
||||
@@ -293,6 +341,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,72 @@ 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))
|
||||
final Pair<Long, Long> rxTx = peerBytes.get(peer);
|
||||
if (rxTx == null)
|
||||
return 0;
|
||||
return peerBytes.get(peer).first;
|
||||
return rxTx.first;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the transmitted traffic (in bytes) for the {@link com.wireguard.config.Peer} referenced by
|
||||
* the provided {@link Key}
|
||||
*
|
||||
* @param peer A {@link Key} representing a {@link com.wireguard.config.Peer}.
|
||||
* @return a long representing the number of bytes transmitted by this peer.
|
||||
*/
|
||||
public long peerTx(final Key peer) {
|
||||
if (!peerBytes.containsKey(peer))
|
||||
final Pair<Long, Long> rxTx = peerBytes.get(peer);
|
||||
if (rxTx == null)
|
||||
return 0;
|
||||
return peerBytes.get(peer).second;
|
||||
return rxTx.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 +99,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,11 +32,10 @@ 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.
|
||||
* Implementation of {@link Backend} that uses the kernel module and {@code wg-quick} to provide
|
||||
* WireGuard tunnels.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
@@ -67,7 +66,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")) {
|
||||
|
||||
@@ -43,8 +43,11 @@ public class RootShell {
|
||||
public RootShell(final Context context) {
|
||||
localBinaryDir = new File(context.getCodeCacheDir(), "bin");
|
||||
localTemporaryDir = new File(context.getCacheDir(), "tmp");
|
||||
preamble = String.format("export CALLING_PACKAGE=%s PATH=\"%s:$PATH\" TMPDIR='%s'; id -u\n",
|
||||
context.getPackageName(), localBinaryDir, localTemporaryDir);
|
||||
final String packageName = context.getPackageName();
|
||||
if (packageName.contains("'"))
|
||||
throw new RuntimeException("Impossibly invalid package name contains a single quote");
|
||||
preamble = String.format("export CALLING_PACKAGE=%s PATH=\"%s:$PATH\" TMPDIR='%s'; magisk --sqlite \"UPDATE policies SET notification=0, logging=0 WHERE package_name='%s'\" >/dev/null 2>&1; id -u\n",
|
||||
packageName, localBinaryDir, localTemporaryDir, packageName);
|
||||
}
|
||||
|
||||
private static boolean isExecutableInPath(final String name) {
|
||||
|
||||
@@ -143,7 +143,7 @@ public final class ToolsInstaller {
|
||||
extract();
|
||||
final StringBuilder script = new StringBuilder("set -ex; ");
|
||||
|
||||
script.append("trap 'rm -rf /data/adb/moduleswireguard' INT TERM EXIT; ");
|
||||
script.append("trap 'rm -rf /data/adb/modules/wireguard' 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; ");
|
||||
|
||||
@@ -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#][^#]*)");
|
||||
|
||||
@@ -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
|
||||
@@ -223,9 +221,7 @@ 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())
|
||||
@@ -339,11 +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(Lists.of(Attribute.split(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,
|
||||
|
||||
@@ -13,7 +13,7 @@ import java.util.Arrays;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Implementation of the Curve25519 elliptic curve algorithm.
|
||||
* Implementation of Curve25519 ECDH.
|
||||
* <p>
|
||||
* This implementation was imported to WireGuard from noise-java:
|
||||
* https://github.com/rweather/noise-java
|
||||
@@ -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
@@ -20,11 +20,11 @@ export GOARCH := $(NDK_GO_ARCH_MAP_$(ANDROID_ARCH_NAME))
|
||||
export GOOS := android
|
||||
export CGO_ENABLED := 1
|
||||
|
||||
GO_VERSION := 1.14.4
|
||||
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 := 3fa7ed8dc44fdd50c0bfe72676250cceca527d59950aef20af906a670cf88de2
|
||||
GO_HASH_linux-amd64 := aed845e4185a0b2a3c3d5e1d0a35491702c55889192bb9c30e67a3de6849c067
|
||||
GO_HASH_darwin-amd64 := 9bd39600d9fa1fa4a5ccce8761d249f7421cffe671376f791293c4138f3d7c62
|
||||
GO_HASH_linux-amd64 := b49fda1ca29a1946d6bb2a5a6982cf07ccd2aba849289508ee0f9918f6bb4552
|
||||
|
||||
default: $(DESTDIR)/libwg-go.so
|
||||
|
||||
|
||||
@@ -46,7 +46,6 @@ type TunnelHandle struct {
|
||||
var tunnelHandles map[int32]TunnelHandle
|
||||
|
||||
func init() {
|
||||
device.RoamingDisabled = true
|
||||
tunnelHandles = make(map[int32]TunnelHandle)
|
||||
signals := make(chan os.Signal)
|
||||
signal.Notify(signals, unix.SIGUSR2)
|
||||
@@ -91,6 +90,7 @@ func wgTurnOn(ifnameRef string, tunFd int32, settings string) int32 {
|
||||
logger.Error.Println(setError)
|
||||
return -1
|
||||
}
|
||||
device.DisableSomeRoamingForBrokenMobileSemantics()
|
||||
|
||||
var uapi net.Listener
|
||||
|
||||
@@ -172,7 +172,7 @@ func wgGetSocketV6(tunnelHandle int32) int32 {
|
||||
if bind == nil {
|
||||
return -1
|
||||
}
|
||||
fd, err := bind.PeekLookAtSocketFd4()
|
||||
fd, err := bind.PeekLookAtSocketFd6()
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
module golang.zx2c4.com/wireguard/android
|
||||
|
||||
go 1.14
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
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
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924 // indirect
|
||||
golang.org/x/sys v0.0.0-20201223074533-0d417f636930
|
||||
golang.zx2c4.com/wireguard v0.0.20201119-0.20201223215156-09728dc6b340
|
||||
)
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
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/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
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/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY=
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
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-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/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201223074533-0d417f636930 h1:vRgIt+nup/B/BwIS0g2oC0haq0iqbV3ZA+u6+0TlNCo=
|
||||
golang.org/x/sys v0.0.0-20201223074533-0d417f636930/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
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/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
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=
|
||||
golang.zx2c4.com/wireguard v0.0.20201119-0.20201223215156-09728dc6b340 h1:X6jrf2sUEj3n+q2oB/I3C088vQFKREz2UzgVJ8wENtI=
|
||||
golang.zx2c4.com/wireguard v0.0.20201119-0.20201223215156-09728dc6b340/go.mod h1:ITsWNpkFv78VPB7f8MiyuxeEMcHR4jfxHGCJLPP3GHs=
|
||||
|
||||
@@ -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: eb4665ecf0...265e81a344
+19
-20
@@ -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
|
||||
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,6 +57,7 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation project(":tunnel")
|
||||
implementation "androidx.activity:activity-ktx:$activityVersion"
|
||||
implementation "androidx.annotation:annotation:$annotationsVersion"
|
||||
implementation "androidx.appcompat:appcompat:$appcompatVersion"
|
||||
implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion"
|
||||
@@ -73,17 +65,24 @@ dependencies {
|
||||
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 "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.google.android.material:material:$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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -45,7 +48,6 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
@@ -53,6 +55,15 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activity.TvMainActivity"
|
||||
android:theme="@style/TvTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activity.SettingsActivity"
|
||||
android:label="@string/settings"
|
||||
@@ -113,7 +124,15 @@
|
||||
android:value="false" />
|
||||
</service>
|
||||
|
||||
<meta-data android:name="android.content.APP_RESTRICTIONS"
|
||||
<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)
|
||||
|
||||
@@ -19,13 +19,17 @@ import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
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 +40,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 +59,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 +95,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 +103,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 +122,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 +139,90 @@ 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 val downloadsFileSaver = DownloadsFileSaver(this)
|
||||
|
||||
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("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) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
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 +254,7 @@ 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 const val TAG = "WireGuard/LogViewerActivity"
|
||||
}
|
||||
|
||||
private inner class LogEntryAdapter : RecyclerView.Adapter<LogEntryAdapter.ViewHolder>() {
|
||||
@@ -348,7 +331,7 @@ class LogViewerActivity : AppCompatActivity() {
|
||||
return openPipeHelper(uri, "text/plain", null, log) { output, _, _, _, l ->
|
||||
try {
|
||||
FileOutputStream(output.fileDescriptor).write(l!!)
|
||||
} catch (_: Exception) {
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,9 @@ import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.ActionBar
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import androidx.fragment.app.commit
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.fragment.TunnelDetailFragment
|
||||
import com.wireguard.android.fragment.TunnelEditorFragment
|
||||
@@ -59,16 +58,6 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener
|
||||
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.main_activity_container).setOnApplyWindowInsetsListener { _, insets ->
|
||||
supportFragmentManager.addOnBackStackChangedListener {
|
||||
supportFragmentManager.fragments.forEach {
|
||||
ViewCompat.dispatchApplyWindowInsets(it.requireView(), WindowInsetsCompat.toWindowInsetsCompat(insets))
|
||||
}
|
||||
}
|
||||
insets
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
@@ -84,11 +73,11 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener
|
||||
true
|
||||
}
|
||||
R.id.menu_action_edit -> {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.detail_container, TunnelEditorFragment())
|
||||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.detail_container, TunnelEditorFragment())
|
||||
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||
addToBackStack(null)
|
||||
}
|
||||
true
|
||||
}
|
||||
// This menu item is handled by the editor fragment.
|
||||
@@ -116,11 +105,11 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener
|
||||
fragmentManager.popBackStackImmediate()
|
||||
} else if (backStackEntries == 0) {
|
||||
// Create and show a new detail fragment.
|
||||
fragmentManager.beginTransaction()
|
||||
.add(R.id.detail_container, TunnelDetailFragment())
|
||||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
fragmentManager.commit {
|
||||
add(R.id.detail_container, TunnelDetailFragment())
|
||||
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||
addToBackStack(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,55 +5,33 @@
|
||||
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.fragment.app.commit
|
||||
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) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(android.R.id.content, SettingsFragment())
|
||||
.commit()
|
||||
supportFragmentManager.commit {
|
||||
add(android.R.id.content, SettingsFragment())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,18 +43,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) {
|
||||
@@ -98,8 +67,8 @@ class SettingsActivity : ThemeChangeAwareActivity() {
|
||||
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 {
|
||||
@@ -115,13 +84,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package com.wireguard.android.activity
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.commit
|
||||
import com.wireguard.android.fragment.TunnelEditorFragment
|
||||
import com.wireguard.android.model.ObservableTunnel
|
||||
|
||||
@@ -15,9 +16,9 @@ class TunnelCreatorActivity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (supportFragmentManager.findFragmentById(android.R.id.content) == null) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(android.R.id.content, TunnelEditorFragment())
|
||||
.commit()
|
||||
supportFragmentManager.commit {
|
||||
add(android.R.id.content, TunnelEditorFragment())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,32 +10,53 @@ import android.os.Bundle
|
||||
import android.service.quicksettings.TileService
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
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.GoBackend
|
||||
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)
|
||||
private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { toggleTunnelWithPermissionsResult() }
|
||||
|
||||
private fun toggleTunnelWithPermissionsResult() {
|
||||
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()
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleScope.launch {
|
||||
if (Application.getBackend() is GoBackend) {
|
||||
val intent = GoBackend.VpnService.prepare(this@TunnelToggleActivity)
|
||||
if (intent != null) {
|
||||
permissionActivityResultLauncher.launch(intent)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
toggleTunnelWithPermissionsResult()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -0,0 +1,391 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.activity
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.os.storage.StorageManager
|
||||
import android.os.storage.StorageVolume
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.view.forEach
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ObservableBoolean
|
||||
import androidx.databinding.ObservableField
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.databinding.Keyed
|
||||
import com.wireguard.android.databinding.ObservableKeyedArrayList
|
||||
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter
|
||||
import com.wireguard.android.databinding.TvActivityBinding
|
||||
import com.wireguard.android.databinding.TvFileListItemBinding
|
||||
import com.wireguard.android.databinding.TvTunnelListItemBinding
|
||||
import com.wireguard.android.model.ObservableTunnel
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import com.wireguard.android.util.QuantityFormatter
|
||||
import com.wireguard.android.util.TunnelImporter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
class TvMainActivity : AppCompatActivity() {
|
||||
private val tunnelFileImportResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { data ->
|
||||
if (data == null) return@registerForActivityResult
|
||||
lifecycleScope.launch {
|
||||
TunnelImporter.importTunnel(contentResolver, data) {
|
||||
Toast.makeText(this@TvMainActivity, it, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
private var pendingTunnel: ObservableTunnel? = null
|
||||
private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
val tunnel = pendingTunnel
|
||||
if (tunnel != null)
|
||||
setTunnelStateWithPermissionsResult(tunnel)
|
||||
pendingTunnel = null
|
||||
}
|
||||
|
||||
private fun setTunnelStateWithPermissionsResult(tunnel: ObservableTunnel) {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
tunnel.setStateAsync(Tunnel.State.TOGGLE)
|
||||
} catch (e: Throwable) {
|
||||
val error = ErrorMessages[e]
|
||||
val message = getString(R.string.error_up, error)
|
||||
Toast.makeText(this@TvMainActivity, message, Toast.LENGTH_LONG).show()
|
||||
Log.e(TAG, message, e)
|
||||
}
|
||||
updateStats()
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var binding: TvActivityBinding
|
||||
private val isDeleting = ObservableBoolean()
|
||||
private val files = ObservableKeyedArrayList<String, KeyedFile>()
|
||||
private val filesRoot = ObservableField("")
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = TvActivityBinding.inflate(layoutInflater)
|
||||
lifecycleScope.launch {
|
||||
binding.tunnels = Application.getTunnelManager().getTunnels()
|
||||
if (binding.tunnels?.isEmpty() == true)
|
||||
binding.importButton.requestFocus()
|
||||
else
|
||||
binding.tunnelList.requestFocus()
|
||||
}
|
||||
binding.isDeleting = isDeleting
|
||||
binding.files = files
|
||||
binding.filesRoot = filesRoot
|
||||
val gridManager = binding.tunnelList.layoutManager as GridLayoutManager
|
||||
gridManager.spanSizeLookup = SlatedSpanSizeLookup(gridManager)
|
||||
binding.tunnelRowConfigurationHandler = object : ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TvTunnelListItemBinding, ObservableTunnel> {
|
||||
override fun onConfigureRow(binding: TvTunnelListItemBinding, item: ObservableTunnel, position: Int) {
|
||||
binding.isDeleting = isDeleting
|
||||
binding.isFocused = ObservableBoolean()
|
||||
binding.root.setOnFocusChangeListener { _, focused ->
|
||||
binding.isFocused?.set(focused)
|
||||
}
|
||||
binding.root.setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
if (isDeleting.get()) {
|
||||
try {
|
||||
item.deleteAsync()
|
||||
if (this@TvMainActivity.binding.tunnels?.isEmpty() != false)
|
||||
isDeleting.set(false)
|
||||
} catch (e: Throwable) {
|
||||
val error = ErrorMessages[e]
|
||||
val message = getString(R.string.config_delete_error, error)
|
||||
Toast.makeText(this@TvMainActivity, message, Toast.LENGTH_LONG).show()
|
||||
Log.e(TAG, message, e)
|
||||
}
|
||||
} else {
|
||||
if (Application.getBackend() is GoBackend) {
|
||||
val intent = GoBackend.VpnService.prepare(binding.root.context)
|
||||
if (intent != null) {
|
||||
pendingTunnel = item
|
||||
permissionActivityResultLauncher.launch(intent)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
setTunnelStateWithPermissionsResult(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.filesRowConfigurationHandler = object : ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TvFileListItemBinding, KeyedFile> {
|
||||
override fun onConfigureRow(binding: TvFileListItemBinding, item: KeyedFile, position: Int) {
|
||||
binding.root.setOnClickListener {
|
||||
if (item.file.isDirectory)
|
||||
navigateTo(item.file)
|
||||
else {
|
||||
val uri = Uri.fromFile(item.file)
|
||||
files.clear()
|
||||
filesRoot.set("")
|
||||
lifecycleScope.launch {
|
||||
TunnelImporter.importTunnel(contentResolver, uri) {
|
||||
Toast.makeText(this@TvMainActivity, it, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
runOnUiThread {
|
||||
this@TvMainActivity.binding.tunnelList.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.importButton.setOnClickListener {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
if (filesRoot.get()?.isEmpty() != false) {
|
||||
navigateTo(File("/"))
|
||||
runOnUiThread {
|
||||
binding.filesList.requestFocus()
|
||||
}
|
||||
} else {
|
||||
files.clear()
|
||||
filesRoot.set("")
|
||||
runOnUiThread {
|
||||
binding.tunnelList.requestFocus()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
tunnelFileImportResultLauncher.launch("*/*")
|
||||
} catch (_: Throwable) {
|
||||
Toast.makeText(this@TvMainActivity, getString(R.string.tv_no_file_picker), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.deleteButton.setOnClickListener {
|
||||
isDeleting.set(!isDeleting.get())
|
||||
runOnUiThread {
|
||||
binding.tunnelList.requestFocus()
|
||||
}
|
||||
}
|
||||
binding.executePendingBindings()
|
||||
setContentView(binding.root)
|
||||
|
||||
lifecycleScope.launch {
|
||||
while (true) {
|
||||
updateStats()
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var pendingNavigation: File? = null
|
||||
private val permissionRequestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||
val to = pendingNavigation
|
||||
if (it && to != null)
|
||||
navigateTo(to)
|
||||
pendingNavigation = null
|
||||
}
|
||||
|
||||
private var cachedRoots: Collection<KeyedFile>? = null
|
||||
|
||||
private suspend fun makeStorageRoots(): Collection<KeyedFile> = withContext(Dispatchers.IO) {
|
||||
cachedRoots?.let { return@withContext it }
|
||||
val list = HashSet<KeyedFile>()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
val storageManager: StorageManager = getSystemService() ?: return@withContext list
|
||||
list.addAll(storageManager.storageVolumes.mapNotNull { volume ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
volume.directory?.let { KeyedFile(it, volume.getDescription(this@TvMainActivity)) }
|
||||
} else {
|
||||
KeyedFile((StorageVolume::class.java.getMethod("getPathFile").invoke(volume) as File), volume.getDescription(this@TvMainActivity))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
list.add(KeyedFile(Environment.getExternalStorageDirectory()))
|
||||
try {
|
||||
File("/storage").listFiles()?.forEach {
|
||||
if (!it.isDirectory) return@forEach
|
||||
try {
|
||||
if (Environment.isExternalStorageRemovable(it)) {
|
||||
list.add(KeyedFile(it))
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
}
|
||||
cachedRoots = list
|
||||
list
|
||||
}
|
||||
|
||||
private fun isBelowCachedRoots(maybeChild: File): Boolean {
|
||||
val cachedRoots = cachedRoots ?: return true
|
||||
for (root in cachedRoots) {
|
||||
if (maybeChild.canonicalPath.startsWith(root.file.canonicalPath))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun navigateTo(directory: File) {
|
||||
require(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
|
||||
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
pendingNavigation = directory
|
||||
permissionRequestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
return
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
if (isBelowCachedRoots(directory)) {
|
||||
val roots = makeStorageRoots()
|
||||
if (roots.count() == 1) {
|
||||
navigateTo(roots.first().file)
|
||||
return@launch
|
||||
}
|
||||
files.clear()
|
||||
files.addAll(roots)
|
||||
filesRoot.set(getString(R.string.tv_select_a_storage_drive))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val newFiles = withContext(Dispatchers.IO) {
|
||||
val newFiles = ArrayList<KeyedFile>()
|
||||
try {
|
||||
directory.parentFile?.let {
|
||||
newFiles.add(KeyedFile(it, "../"))
|
||||
}
|
||||
val listing = directory.listFiles() ?: return@withContext null
|
||||
listing.forEach {
|
||||
if (it.extension == "conf" || it.extension == "zip" || it.isDirectory)
|
||||
newFiles.add(KeyedFile(it))
|
||||
}
|
||||
newFiles.sortWith { a, b ->
|
||||
if (a.file.isDirectory && !b.file.isDirectory) -1
|
||||
else if (!a.file.isDirectory && b.file.isDirectory) 1
|
||||
else a.file.compareTo(b.file)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
newFiles
|
||||
}
|
||||
if (newFiles?.isEmpty() != false)
|
||||
return@launch
|
||||
files.clear()
|
||||
files.addAll(newFiles)
|
||||
filesRoot.set(directory.canonicalPath)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
when {
|
||||
isDeleting.get() -> {
|
||||
isDeleting.set(false)
|
||||
runOnUiThread {
|
||||
binding.tunnelList.requestFocus()
|
||||
}
|
||||
}
|
||||
filesRoot.get()?.isNotEmpty() == true -> {
|
||||
files.clear()
|
||||
filesRoot.set("")
|
||||
runOnUiThread {
|
||||
binding.tunnelList.requestFocus()
|
||||
}
|
||||
}
|
||||
else -> super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateStats() {
|
||||
binding.tunnelList.forEach { viewItem ->
|
||||
val listItem = DataBindingUtil.findBinding<TvTunnelListItemBinding>(viewItem)
|
||||
?: return@forEach
|
||||
try {
|
||||
val tunnel = listItem.item!!
|
||||
if (tunnel.state != Tunnel.State.UP || isDeleting.get()) {
|
||||
throw Exception()
|
||||
}
|
||||
val statistics = tunnel.getStatisticsAsync()
|
||||
val rx = statistics.totalRx()
|
||||
val tx = statistics.totalTx()
|
||||
listItem.tunnelTransfer.text = getString(R.string.transfer_rx_tx, QuantityFormatter.formatBytes(rx), QuantityFormatter.formatBytes(tx))
|
||||
listItem.tunnelTransfer.visibility = View.VISIBLE
|
||||
} catch (_: Throwable) {
|
||||
listItem.tunnelTransfer.visibility = View.GONE
|
||||
listItem.tunnelTransfer.text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class KeyedFile(val file: File, private val forcedKey: String? = null) : Keyed<String> {
|
||||
override val key: String
|
||||
get() = forcedKey ?: if (file.isDirectory) "${file.name}/" else file.name
|
||||
}
|
||||
|
||||
private class SlatedSpanSizeLookup(private val gridManager: GridLayoutManager) : SpanSizeLookup() {
|
||||
private val originalHeight = gridManager.spanCount
|
||||
private var newWidth = 0
|
||||
private lateinit var sizeMap: Array<IntArray?>
|
||||
|
||||
private fun emptyUnderIndex(index: Int, size: Int): Int {
|
||||
sizeMap[size - 1]?.let { return it[index] }
|
||||
val sizes = IntArray(size)
|
||||
val oh = originalHeight
|
||||
val nw = newWidth
|
||||
var empties = 0
|
||||
for (i in 0 until size) {
|
||||
val ox = (i + empties) / oh
|
||||
val oy = (i + empties) % oh
|
||||
var empty = 0
|
||||
for (j in oy + 1 until oh) {
|
||||
val ni = nw * j + ox
|
||||
if (ni < size)
|
||||
break
|
||||
empty++
|
||||
}
|
||||
empties += empty
|
||||
sizes[i] = empty
|
||||
}
|
||||
sizeMap[size - 1] = sizes
|
||||
return sizes[index]
|
||||
}
|
||||
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
if (newWidth == 0) {
|
||||
val child = gridManager.getChildAt(0) ?: return 1
|
||||
if (child.width == 0) return 1
|
||||
newWidth = gridManager.width / child.width
|
||||
sizeMap = Array(originalHeight * newWidth - 1) { null }
|
||||
}
|
||||
val total = gridManager.itemCount
|
||||
if (total >= originalHeight * newWidth || total == 0)
|
||||
return 1
|
||||
return emptyUnderIndex(position, total) + 1
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "WireGuard/TvMainActivity"
|
||||
}
|
||||
}
|
||||
@@ -25,8 +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.
|
||||
@@ -158,7 +158,7 @@ object BindingAdapters {
|
||||
return 0
|
||||
return try {
|
||||
Integer.parseInt(s)
|
||||
} catch (_: Exception) {
|
||||
} catch (_: Throwable) {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.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.requireTargetFragment
|
||||
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_ANY) != 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?) {
|
||||
@@ -82,23 +87,22 @@ class AddTunnelsSheet : BottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,24 +4,27 @@
|
||||
*/
|
||||
package com.wireguard.android.fragment
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Dialog
|
||||
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 androidx.fragment.app.setFragmentResult
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.wireguard.android.Application
|
||||
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 com.wireguard.android.util.requireTargetFragment
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class AppListDialogFragment : DialogFragment() {
|
||||
private val appData = ObservableKeyedArrayList<String, ApplicationData>()
|
||||
@@ -33,40 +36,42 @@ class AppListDialogFragment : DialogFragment() {
|
||||
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
|
||||
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()
|
||||
lifecycleScope.launch(Dispatchers.Default) {
|
||||
try {
|
||||
val applicationData: MutableList<ApplicationData> = ArrayList()
|
||||
withContext(Dispatchers.IO) {
|
||||
val packageInfos = pm.getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET), 0)
|
||||
packageInfos.forEach {
|
||||
val packageName = it.packageName
|
||||
val appInfo = it.applicationInfo
|
||||
val appData = ApplicationData(appInfo.loadIcon(pm), appInfo.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 })
|
||||
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()
|
||||
}
|
||||
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)
|
||||
require(requireTargetFragment() is AppSelectionListener) { "${requireTargetFragment()} must implement AppSelectionListener" }
|
||||
currentlySelectedApps = (arguments?.getStringArrayList(KEY_SELECTED_APPS) ?: emptyList())
|
||||
initiallyExcluded = arguments?.getBoolean(KEY_IS_EXCLUDED) ?: true
|
||||
}
|
||||
@@ -123,23 +128,22 @@ class AppListDialogFragment : DialogFragment() {
|
||||
selectedApps.add(data.packageName)
|
||||
}
|
||||
}
|
||||
(requireTargetFragment() as AppSelectionListener).onSelectedAppsSelected(selectedApps, tabs?.selectedTabPosition == 0)
|
||||
setFragmentResult(REQUEST_SELECTION, bundleOf(
|
||||
KEY_SELECTED_APPS to selectedApps.toTypedArray(),
|
||||
KEY_IS_EXCLUDED to (tabs?.selectedTabPosition == 0)
|
||||
))
|
||||
dismiss()
|
||||
}
|
||||
|
||||
interface AppSelectionListener {
|
||||
fun onSelectedAppsSelected(selectedApps: List<String>, isExcluded: Boolean)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_SELECTED_APPS = "selected_apps"
|
||||
private const val KEY_IS_EXCLUDED = "is_excluded"
|
||||
fun <T> newInstance(selectedApps: ArrayList<String?>?, isExcluded: Boolean, target: T): AppListDialogFragment where T : Fragment?, T : AppSelectionListener? {
|
||||
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_SELECTED_APPS, selectedApps)
|
||||
extras.putBoolean(KEY_IS_EXCLUDED, isExcluded)
|
||||
val fragment = AppListDialogFragment()
|
||||
fragment.setTargetFragment(target, 0)
|
||||
fragment.arguments = extras
|
||||
return fragment
|
||||
}
|
||||
|
||||
@@ -5,62 +5,56 @@
|
||||
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
|
||||
* attached to a `BaseActivity`.
|
||||
*/
|
||||
abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
|
||||
private var baseActivity: BaseActivity? = null
|
||||
private var pendingTunnel: ObservableTunnel? = null
|
||||
private var pendingTunnelUp: Boolean? = 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
|
||||
}
|
||||
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() = (activity as? BaseActivity)?.selectedTunnel
|
||||
protected set(tunnel) {
|
||||
(activity as? BaseActivity)?.selectedTunnel = tunnel
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
if (context is BaseActivity) {
|
||||
baseActivity = context
|
||||
baseActivity?.addOnSelectedTunnelChangedListener(this)
|
||||
} else {
|
||||
baseActivity = null
|
||||
}
|
||||
(activity as? BaseActivity)?.addOnSelectedTunnelChangedListener(this)
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
baseActivity?.removeOnSelectedTunnelChangedListener(this)
|
||||
baseActivity = null
|
||||
(activity as? BaseActivity)?.removeOnSelectedTunnelChangedListener(this)
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
@@ -70,14 +64,15 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
|
||||
is TunnelListItemBinding -> binding.item
|
||||
else -> return
|
||||
} ?: return
|
||||
Application.getBackendAsync().thenAccept { backend: Backend? ->
|
||||
if (backend is GoBackend) {
|
||||
val intent = GoBackend.VpnService.prepare(view.context)
|
||||
val activity = activity ?: return
|
||||
activity.lifecycleScope.launch {
|
||||
if (Application.getBackend() is GoBackend) {
|
||||
val intent = GoBackend.VpnService.prepare(activity)
|
||||
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 +80,27 @@ 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)
|
||||
val activity = activity ?: return
|
||||
activity.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 = activity.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(activity, 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
|
||||
@@ -26,14 +29,15 @@ class ConfigNamingDialogFragment : DialogFragment() {
|
||||
private var imm: InputMethodManager? = null
|
||||
|
||||
private fun createTunnelAndDismiss() {
|
||||
binding?.let {
|
||||
val name = it.tunnelNameText.text.toString()
|
||||
Application.getTunnelManager().create(name, config).whenComplete { tunnel, throwable ->
|
||||
if (tunnel != null) {
|
||||
dismiss()
|
||||
} else {
|
||||
it.tunnelNameTextLayout.error = throwable.message
|
||||
}
|
||||
val binding = binding ?: return
|
||||
val activity = activity ?: return
|
||||
val name = binding.tunnelNameText.text.toString()
|
||||
activity.lifecycleScope.launch {
|
||||
try {
|
||||
Application.getTunnelManager().create(name, config)
|
||||
dismiss()
|
||||
} catch (e: Throwable) {
|
||||
binding.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,15 +11,15 @@ 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 java.util.Timer
|
||||
import java.util.TimerTask
|
||||
import com.wireguard.android.util.QuantityFormatter
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Fragment that shows details about a specific tunnel.
|
||||
@@ -27,18 +27,7 @@ import java.util.TimerTask
|
||||
class TunnelDetailFragment : BaseFragment() {
|
||||
private var binding: TunnelDetailFragmentBinding? = null
|
||||
private var lastState = Tunnel.State.TOGGLE
|
||||
private var timer: Timer? = null
|
||||
|
||||
private fun formatBytes(bytes: Long): String {
|
||||
val context = requireContext()
|
||||
return when {
|
||||
bytes < 1024 -> context.getString(R.string.transfer_bytes, bytes)
|
||||
bytes < 1024 * 1024 -> context.getString(R.string.transfer_kibibytes, bytes / 1024.0)
|
||||
bytes < 1024 * 1024 * 1024 -> context.getString(R.string.transfer_mibibytes, bytes / (1024.0 * 1024.0))
|
||||
bytes < 1024 * 1024 * 1024 * 1024L -> context.getString(R.string.transfer_gibibytes, bytes / (1024.0 * 1024.0 * 1024.0))
|
||||
else -> context.getString(R.string.transfer_tibibytes, bytes / (1024.0 * 1024.0 * 1024.0) / 1024.0)
|
||||
}
|
||||
}
|
||||
private var timerActive = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -53,11 +42,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
|
||||
}
|
||||
|
||||
@@ -68,28 +53,36 @@ class TunnelDetailFragment : BaseFragment() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
timer = Timer()
|
||||
timer!!.scheduleAtFixedRate(object : TimerTask() {
|
||||
override fun run() {
|
||||
timerActive = true
|
||||
lifecycleScope.launch {
|
||||
while (timerActive) {
|
||||
updateStats()
|
||||
delay(1000)
|
||||
}
|
||||
}, 0, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
val binding = binding ?: return
|
||||
binding.tunnel = newTunnel
|
||||
if (newTunnel == null) {
|
||||
binding.config = null
|
||||
} else {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
binding.config = newTunnel.getConfigAsync()
|
||||
} catch (_: Throwable) {
|
||||
binding.config = null
|
||||
}
|
||||
}
|
||||
}
|
||||
lastState = Tunnel.State.TOGGLE
|
||||
updateStats()
|
||||
lifecycleScope.launch { updateStats() }
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
timerActive = false
|
||||
super.onStop()
|
||||
if (timer != null) {
|
||||
timer!!.cancel()
|
||||
timer = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewStateRestored(savedInstanceState: Bundle?) {
|
||||
@@ -99,24 +92,17 @@ class TunnelDetailFragment : BaseFragment() {
|
||||
super.onViewStateRestored(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun updateStats() {
|
||||
if (binding == null || !isResumed) return
|
||||
val tunnel = binding!!.tunnel ?: return
|
||||
private suspend fun updateStats() {
|
||||
val binding = binding ?: return
|
||||
val tunnel = binding.tunnel ?: return
|
||||
if (!isResumed) return
|
||||
val state = tunnel.state
|
||||
if (state != Tunnel.State.UP && lastState == state) return
|
||||
lastState = state
|
||||
tunnel.statisticsAsync.whenComplete { statistics, throwable ->
|
||||
if (throwable != null) {
|
||||
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))
|
||||
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)
|
||||
@@ -126,10 +112,17 @@ class TunnelDetailFragment : BaseFragment() {
|
||||
peer.transferText.visibility = View.GONE
|
||||
continue
|
||||
}
|
||||
peer.transferText.text = requireContext().getString(R.string.transfer_rx_tx, formatBytes(rx), formatBytes(tx))
|
||||
peer.transferText.text = getString(R.string.transfer_rx_tx, QuantityFormatter.formatBytes(rx), QuantityFormatter.formatBytes(tx))
|
||||
peer.transferLabel.visibility = View.VISIBLE
|
||||
peer.transferText.visibility = View.VISIBLE
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
for (i in 0 until binding.peersLayout.childCount) {
|
||||
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i))
|
||||
?: continue
|
||||
peer.transferLabel.visibility = View.GONE
|
||||
peer.transferText.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,46 +18,48 @@ 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.AppSelectionListener
|
||||
import com.wireguard.android.model.ObservableTunnel
|
||||
import com.wireguard.android.util.BiometricAuthenticator
|
||||
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(), AppSelectionListener {
|
||||
class TunnelEditorFragment : BaseFragment() {
|
||||
private var haveShownKeys = false
|
||||
private var binding: TunnelEditorFragmentBinding? = null
|
||||
private var tunnel: ObservableTunnel? = null
|
||||
|
||||
private fun onConfigLoaded(config: Config) {
|
||||
binding?.config = ConfigProxy(config)
|
||||
}
|
||||
|
||||
private fun onConfigSaved(savedTunnel: Tunnel, throwable: Throwable?) {
|
||||
val message: String
|
||||
val ctx = activity ?: Application.get()
|
||||
if (throwable == null) {
|
||||
message = getString(R.string.config_save_success, savedTunnel.name)
|
||||
val message = ctx.getString(R.string.config_save_success, savedTunnel.name)
|
||||
Log.d(TAG, message)
|
||||
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
|
||||
onFinished()
|
||||
} else {
|
||||
val error = ErrorMessages[throwable]
|
||||
message = getString(R.string.config_save_error, savedTunnel.name, error)
|
||||
val message = ctx.getString(R.string.config_save_error, savedTunnel.name, error)
|
||||
Log.e(TAG, message, throwable)
|
||||
binding?.let {
|
||||
Snackbar.make(it.mainContainer, message, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
val binding = binding
|
||||
if (binding != null)
|
||||
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show()
|
||||
else
|
||||
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,8 +78,6 @@ class TunnelEditorFragment : BaseFragment(), AppSelectionListener {
|
||||
binding = TunnelEditorFragmentBinding.inflate(inflater, container, false)
|
||||
binding?.apply {
|
||||
executePendingBindings()
|
||||
setUpRoot(root as ViewGroup)
|
||||
setUpScrollingContent(mainContainer, null)
|
||||
privateKeyTextLayout.setEndIconOnClickListener { config?.`interface`?.generateKeyPair() }
|
||||
}
|
||||
return binding?.root
|
||||
@@ -89,23 +89,6 @@ class TunnelEditorFragment : BaseFragment(), AppSelectionListener {
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onSelectedAppsSelected(selectedApps: List<String>, isExcluded: Boolean) {
|
||||
requireNotNull(binding) { "Tried to set excluded/included apps while no view was loaded" }
|
||||
if (isExcluded) {
|
||||
binding!!.config!!.`interface`.includedApplications.clear()
|
||||
binding!!.config!!.`interface`.excludedApplications.apply {
|
||||
clear()
|
||||
addAll(selectedApps)
|
||||
}
|
||||
} else {
|
||||
binding!!.config!!.`interface`.excludedApplications.clear()
|
||||
binding!!.config!!.`interface`.includedApplications.apply {
|
||||
clear()
|
||||
addAll(selectedApps)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFinished() {
|
||||
// Hide the keyboard; it rarely goes away on its own.
|
||||
val activity = activity ?: return
|
||||
@@ -115,14 +98,11 @@ class TunnelEditorFragment : BaseFragment(), AppSelectionListener {
|
||||
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 {
|
||||
@@ -130,7 +110,7 @@ class TunnelEditorFragment : BaseFragment(), AppSelectionListener {
|
||||
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)
|
||||
@@ -138,20 +118,36 @@ class TunnelEditorFragment : BaseFragment(), AppSelectionListener {
|
||||
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) }
|
||||
val activity = requireActivity()
|
||||
activity.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
|
||||
@@ -169,8 +165,26 @@ class TunnelEditorFragment : BaseFragment(), AppSelectionListener {
|
||||
if (selectedApps.isNotEmpty())
|
||||
isExcluded = false
|
||||
}
|
||||
val fragment = AppListDialogFragment.newInstance(selectedApps, isExcluded, this)
|
||||
fragment.show(parentFragmentManager, null)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,46 +201,60 @@ class TunnelEditorFragment : BaseFragment(), AppSelectionListener {
|
||||
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?) {
|
||||
val message: String
|
||||
private fun onTunnelCreated(newTunnel: ObservableTunnel?, throwable: Throwable?) {
|
||||
val ctx = activity ?: Application.get()
|
||||
if (throwable == null) {
|
||||
tunnel = newTunnel
|
||||
message = getString(R.string.tunnel_create_success, tunnel!!.name)
|
||||
val message = ctx.getString(R.string.tunnel_create_success, tunnel!!.name)
|
||||
Log.d(TAG, message)
|
||||
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
|
||||
onFinished()
|
||||
} else {
|
||||
val error = ErrorMessages[throwable]
|
||||
message = getString(R.string.tunnel_create_error, error)
|
||||
val message = ctx.getString(R.string.tunnel_create_error, error)
|
||||
Log.e(TAG, message, throwable)
|
||||
binding?.let {
|
||||
Snackbar.make(it.mainContainer, message, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
val binding = binding
|
||||
if (binding != null)
|
||||
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show()
|
||||
else
|
||||
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTunnelRenamed(renamedTunnel: ObservableTunnel, newConfig: Config,
|
||||
throwable: Throwable?) {
|
||||
val message: String
|
||||
private suspend fun onTunnelRenamed(renamedTunnel: ObservableTunnel, newConfig: Config,
|
||||
throwable: Throwable?) {
|
||||
val ctx = activity ?: Application.get()
|
||||
if (throwable == null) {
|
||||
message = getString(R.string.tunnel_rename_success, renamedTunnel.name)
|
||||
val message = ctx.getString(R.string.tunnel_rename_success, renamedTunnel.name)
|
||||
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) }
|
||||
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)
|
||||
val message = ctx.getString(R.string.tunnel_rename_error, error)
|
||||
Log.e(TAG, message, throwable)
|
||||
binding?.let {
|
||||
Snackbar.make(it.mainContainer, message, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
val binding = binding
|
||||
if (binding != null)
|
||||
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show()
|
||||
else
|
||||
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,9 @@
|
||||
*/
|
||||
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
|
||||
import android.os.Bundle
|
||||
import android.provider.OpenableColumns
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
@@ -19,33 +15,29 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.AnimationUtils
|
||||
import android.widget.Toast
|
||||
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.util.TunnelImporter
|
||||
import com.wireguard.android.widget.MultiselectableRelativeLayout
|
||||
import com.wireguard.config.Config
|
||||
import java9.util.concurrent.CompletableFuture
|
||||
import java.io.BufferedReader
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.ArrayList
|
||||
import java.util.HashSet
|
||||
import java.util.Locale
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
/**
|
||||
* Fragment containing a list of known WireGuard tunnels. It allows creating and deleting tunnels.
|
||||
@@ -54,122 +46,25 @@ class TunnelListFragment : BaseFragment() {
|
||||
private val actionModeListener = ActionModeListener()
|
||||
private var actionMode: ActionMode? = null
|
||||
private var binding: TunnelListFragmentBinding? = null
|
||||
private fun importTunnel(configText: String) {
|
||||
try {
|
||||
// Ensure the config text is parseable before proceeding…
|
||||
Config.parse(ByteArrayInputStream(configText.toByteArray(StandardCharsets.UTF_8)))
|
||||
|
||||
// Config text is valid, now create the tunnel…
|
||||
newInstance(configText).show(parentFragmentManager, null)
|
||||
} catch (e: Exception) {
|
||||
onTunnelImportFinished(emptyList(), listOf<Throwable>(e))
|
||||
private val tunnelFileImportResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { data ->
|
||||
if (data == null) return@registerForActivityResult
|
||||
val activity = activity ?: return@registerForActivityResult
|
||||
val contentResolver = activity.contentResolver ?: return@registerForActivityResult
|
||||
activity.lifecycleScope.launch {
|
||||
TunnelImporter.importTunnel(contentResolver, data) { showSnackbar(it) }
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
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: 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) {
|
||||
throwables.add(e)
|
||||
null
|
||||
}
|
||||
|
||||
if (tunnel != null) {
|
||||
tunnels.add(tunnel)
|
||||
}
|
||||
}
|
||||
onTunnelImportFinished(tunnels, throwables)
|
||||
}
|
||||
}
|
||||
}
|
||||
private val qrImportResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
val qrCode = IntentIntegrator.parseActivityResult(result.resultCode, result.data)?.contents
|
||||
?: return@registerForActivityResult
|
||||
val activity = activity ?: return@registerForActivityResult
|
||||
val fragManager = parentFragmentManager
|
||||
activity.lifecycleScope.launch { TunnelImporter.importTunnel(fragManager, qrCode) { showSnackbar(it) } }
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -178,38 +73,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
|
||||
}
|
||||
@@ -226,53 +116,34 @@ class TunnelListFragment : BaseFragment() {
|
||||
|
||||
override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?) {
|
||||
binding ?: return
|
||||
Application.getTunnelManager().tunnels.thenAccept { tunnels ->
|
||||
if (newTunnel != null) viewForTunnel(newTunnel, tunnels).setSingleSelected(true)
|
||||
if (oldTunnel != null) viewForTunnel(oldTunnel, tunnels).setSingleSelected(false)
|
||||
lifecycleScope.launch {
|
||||
val tunnels = Application.getTunnelManager().getTunnels()
|
||||
if (newTunnel != null) viewForTunnel(newTunnel, tunnels)?.setSingleSelected(true)
|
||||
if (oldTunnel != null) viewForTunnel(oldTunnel, tunnels)?.setSingleSelected(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTunnelDeletionFinished(count: Int, throwable: Throwable?) {
|
||||
val message: String
|
||||
val ctx = activity ?: Application.get()
|
||||
if (throwable == null) {
|
||||
message = resources.getQuantityString(R.plurals.delete_success, count, count)
|
||||
message = ctx.resources.getQuantityString(R.plurals.delete_success, count, count)
|
||||
} else {
|
||||
val error = ErrorMessages[throwable]
|
||||
message = resources.getQuantityString(R.plurals.delete_error, count, count, error)
|
||||
message = ctx.resources.getQuantityString(R.plurals.delete_error, count, count, error)
|
||||
Log.e(TAG, message, throwable)
|
||||
}
|
||||
showSnackbar(message)
|
||||
}
|
||||
|
||||
private fun onTunnelImportFinished(tunnels: List<ObservableTunnel>, throwables: Collection<Throwable>) {
|
||||
var message = ""
|
||||
for (throwable in throwables) {
|
||||
val error = ErrorMessages[throwable]
|
||||
message = getString(R.string.import_error, error)
|
||||
Log.e(TAG, message, throwable)
|
||||
}
|
||||
if (tunnels.size == 1 && throwables.isEmpty())
|
||||
message = getString(R.string.import_success, tunnels[0].name)
|
||||
else if (tunnels.isEmpty() && throwables.size == 1)
|
||||
else if (throwables.isEmpty())
|
||||
message = resources.getQuantityString(R.plurals.import_total_success,
|
||||
tunnels.size, tunnels.size)
|
||||
else if (!throwables.isEmpty())
|
||||
message = resources.getQuantityString(R.plurals.import_partial_success,
|
||||
tunnels.size + throwables.size,
|
||||
tunnels.size, tunnels.size + throwables.size)
|
||||
showSnackbar(message)
|
||||
}
|
||||
|
||||
override fun onViewStateRestored(savedInstanceState: Bundle?) {
|
||||
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
|
||||
@@ -293,15 +164,17 @@ class TunnelListFragment : BaseFragment() {
|
||||
}
|
||||
|
||||
private fun showSnackbar(message: CharSequence) {
|
||||
binding?.let {
|
||||
Snackbar.make(it.mainContainer, message, Snackbar.LENGTH_LONG)
|
||||
.setAnchorView(it.createFab)
|
||||
val binding = binding
|
||||
if (binding != null)
|
||||
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG)
|
||||
.setAnchorView(binding.createFab)
|
||||
.show()
|
||||
}
|
||||
else
|
||||
Toast.makeText(activity ?: Application.get(), message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun viewForTunnel(tunnel: ObservableTunnel, tunnels: List<*>): MultiselectableRelativeLayout {
|
||||
return binding!!.tunnelList.findViewHolderForAdapterPosition(tunnels.indexOf(tunnel))!!.itemView as MultiselectableRelativeLayout
|
||||
private fun viewForTunnel(tunnel: ObservableTunnel, tunnels: List<*>): MultiselectableRelativeLayout? {
|
||||
return binding?.tunnelList?.findViewHolderForAdapterPosition(tunnels.indexOf(tunnel))?.itemView as? MultiselectableRelativeLayout
|
||||
}
|
||||
|
||||
private inner class ActionModeListener : ActionMode.Callback {
|
||||
@@ -315,26 +188,31 @@ class TunnelListFragment : BaseFragment() {
|
||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.menu_action_delete -> {
|
||||
val activity = activity ?: return true
|
||||
val copyCheckedItems = HashSet(checkedItems)
|
||||
binding?.createFab?.apply {
|
||||
visibility = View.VISIBLE
|
||||
scaleX = 1f
|
||||
scaleY = 1f
|
||||
}
|
||||
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)
|
||||
activity.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)
|
||||
}
|
||||
@@ -423,8 +301,6 @@ class TunnelListFragment : BaseFragment() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val REQUEST_IMPORT = 1
|
||||
private const val REQUEST_TARGET_FRAGMENT = 2
|
||||
private const val CHECKED_ITEMS = "CHECKED_ITEMS"
|
||||
private const val TAG = "WireGuard/TunnelListFragment"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -94,16 +109,26 @@ class ObservableTunnel internal constructor(
|
||||
var statistics: Statistics? = null
|
||||
get() {
|
||||
if (field == null || field?.isStale != false)
|
||||
manager.getTunnelStatistics(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.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 != false)
|
||||
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,16 @@
|
||||
*/
|
||||
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 android.widget.Toast
|
||||
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 +21,148 @@ 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.ErrorMessages
|
||||
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 +170,85 @@ 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
|
||||
manager.setTunnelState(tunnel, state)
|
||||
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
|
||||
try {
|
||||
manager.setTunnelState(tunnel, state)
|
||||
} catch (e: Throwable) {
|
||||
Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
+33
-17
@@ -4,17 +4,26 @@
|
||||
*/
|
||||
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.lifecycle.lifecycleScope
|
||||
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.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 kotlin.system.exitProcess
|
||||
|
||||
class KernelModuleDisablerPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
||||
@@ -22,8 +31,8 @@ class KernelModuleDisablerPreference(context: Context, attrs: AttributeSet?) : P
|
||||
|
||||
init {
|
||||
isVisible = false
|
||||
Application.getBackendAsync().thenAccept { backend ->
|
||||
setState(if (backend is WgQuickBackend) State.ENABLED else State.DISABLED)
|
||||
lifecycleScope.launch {
|
||||
setState(if (Application.getBackend() is WgQuickBackend) State.ENABLED else State.DISABLED)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,26 +40,29 @@ class KernelModuleDisablerPreference(context: Context, attrs: AttributeSet?) : P
|
||||
|
||||
override fun getTitle() = if (state == State.UNKNOWN) "" else 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()
|
||||
}
|
||||
Application.getAsyncWorker().runAsync {
|
||||
Application.getTunnelManager().tunnels.thenApply { observableTunnels ->
|
||||
val downings = observableTunnels.map { it.setStateAsync(Tunnel.State.DOWN).toCompletableFuture() }.toTypedArray()
|
||||
CompletableFuture.allOf(*downings).thenRun {
|
||||
activity.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,4 +81,8 @@ class KernelModuleDisablerPreference(context: Context, attrs: AttributeSet?) : P
|
||||
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,22 +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.AdminKnobs
|
||||
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
|
||||
@@ -29,52 +32,48 @@ import java.util.zip.ZipOutputStream
|
||||
*/
|
||||
class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
||||
private var exportedFilePath: String? = null
|
||||
private val downloadsFileSaver = DownloadsFileSaver(activity)
|
||||
|
||||
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("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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,22 +83,17 @@ class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference
|
||||
|
||||
override fun onClick() {
|
||||
if (AdminKnobs.disableConfigExport) return
|
||||
val prefActivity = FragmentUtils.getPrefActivity(this)
|
||||
val fragment = prefActivity.supportFragmentManager.fragments.first()
|
||||
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()
|
||||
|
||||
@@ -12,5 +12,6 @@ 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
|
||||
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,23 @@
|
||||
|
||||
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 +30,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,
|
||||
@@ -55,12 +40,12 @@ object BiometricAuthenticator {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
Log.d(TAG, "BiometricAuthentication error: errorCode=$errorCode, msg=$errString")
|
||||
callback(when (errorCode) {
|
||||
BiometricConstants.ERROR_CANCELED, BiometricConstants.ERROR_USER_CANCELED,
|
||||
BiometricConstants.ERROR_NEGATIVE_BUTTON -> {
|
||||
BiometricPrompt.ERROR_CANCELED, BiometricPrompt.ERROR_USER_CANCELED,
|
||||
BiometricPrompt.ERROR_NEGATIVE_BUTTON -> {
|
||||
Result.Cancelled
|
||||
}
|
||||
BiometricConstants.ERROR_HW_NOT_PRESENT, BiometricConstants.ERROR_HW_UNAVAILABLE,
|
||||
BiometricConstants.ERROR_NO_BIOMETRICS, BiometricConstants.ERROR_NO_DEVICE_CREDENTIAL -> {
|
||||
BiometricPrompt.ERROR_HW_NOT_PRESENT, BiometricPrompt.ERROR_HW_UNAVAILABLE,
|
||||
BiometricPrompt.ERROR_NO_BIOMETRICS, BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> {
|
||||
Result.HardwareUnavailableOrDisabled
|
||||
}
|
||||
else -> Result.Failure(errorCode, fragment.getString(R.string.biometric_auth_error_reason, errString))
|
||||
@@ -77,12 +62,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,65 +4,97 @@
|
||||
*/
|
||||
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.ActivityResultLauncher
|
||||
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
|
||||
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()
|
||||
}
|
||||
class DownloadsFileSaver(private val context: ComponentActivity) {
|
||||
private lateinit var activityResult: ActivityResultLauncher<String>
|
||||
private lateinit var futureGrant: CompletableDeferred<Boolean>
|
||||
|
||||
init {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
futureGrant = CompletableDeferred()
|
||||
activityResult = context.registerForActivityResult(ActivityResultContracts.RequestPermission()) { ret -> futureGrant.complete(ret) }
|
||||
}
|
||||
if (path == null) {
|
||||
path = "Download/"
|
||||
cursor = contentResolver.query(contentUri, arrayOf(MediaColumns.DISPLAY_NAME), null, null, null)
|
||||
}
|
||||
|
||||
suspend fun save(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) {
|
||||
activityResult.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
val granted = futureGrant.await()
|
||||
if (!granted) {
|
||||
futureGrant = CompletableDeferred()
|
||||
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
|
||||
|
||||
@@ -6,7 +6,7 @@ package com.wireguard.android.util
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.os.RemoteException
|
||||
import com.wireguard.android.Application.Companion.get
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.backend.BackendException
|
||||
import com.wireguard.android.util.RootShell.RootShellException
|
||||
@@ -63,7 +63,7 @@ object ErrorMessages {
|
||||
)
|
||||
|
||||
operator fun get(throwable: Throwable?): String {
|
||||
val resources = get().resources
|
||||
val resources = Application.get().resources
|
||||
if (throwable == null) return resources.getString(R.string.unknown_error)
|
||||
val rootCause = rootCause(throwable)
|
||||
return when {
|
||||
|
||||
@@ -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,7 +8,11 @@ package com.wireguard.android.util
|
||||
import android.content.Context
|
||||
import android.util.TypedValue
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.fragment.app.Fragment
|
||||
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()
|
||||
@@ -16,6 +20,12 @@ fun Context.resolveAttribute(@AttrRes attrRes: Int): Int {
|
||||
return typedValue.data
|
||||
}
|
||||
|
||||
fun Fragment.requireTargetFragment(): Fragment {
|
||||
return requireNotNull(targetFragment) { "A target fragment should always be set for $this" }
|
||||
}
|
||||
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,22 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util
|
||||
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
|
||||
object QuantityFormatter {
|
||||
fun formatBytes(bytes: Long): String {
|
||||
val context = Application.get().applicationContext
|
||||
return when {
|
||||
bytes < 1024 -> context.getString(R.string.transfer_bytes, bytes)
|
||||
bytes < 1024 * 1024 -> context.getString(R.string.transfer_kibibytes, bytes / 1024.0)
|
||||
bytes < 1024 * 1024 * 1024 -> context.getString(R.string.transfer_mibibytes, bytes / (1024.0 * 1024.0))
|
||||
bytes < 1024 * 1024 * 1024 * 1024L -> context.getString(R.string.transfer_gibibytes, bytes / (1024.0 * 1024.0 * 1024.0))
|
||||
else -> context.getString(R.string.transfer_tibibytes, bytes / (1024.0 * 1024.0 * 1024.0) / 1024.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import android.util.Log
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.fragment.ConfigNamingDialogFragment
|
||||
import com.wireguard.android.model.ObservableTunnel
|
||||
import com.wireguard.config.Config
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.BufferedReader
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.ArrayList
|
||||
import java.util.Locale
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
object TunnelImporter {
|
||||
suspend fun importTunnel(contentResolver: ContentResolver, uri: Uri, messageCallback: (CharSequence) -> Unit) = withContext(Dispatchers.IO) {
|
||||
val context = Application.get().applicationContext
|
||||
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) { context.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) { context.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
|
||||
}
|
||||
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) })
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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 {
|
||||
require(throwables.isNotEmpty()) { context.getString(R.string.no_configs_error) }
|
||||
}
|
||||
}
|
||||
val tunnels = futureTunnels.mapNotNull {
|
||||
try {
|
||||
it.await()
|
||||
} catch (e: Throwable) {
|
||||
throwables.add(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main.immediate) { onTunnelImportFinished(tunnels, throwables, messageCallback) }
|
||||
} catch (e: Throwable) {
|
||||
withContext(Dispatchers.Main.immediate) { onTunnelImportFinished(emptyList(), listOf(e), messageCallback) }
|
||||
}
|
||||
}
|
||||
|
||||
fun importTunnel(parentFragmentManager: FragmentManager, configText: String, messageCallback: (CharSequence) -> Unit) {
|
||||
try {
|
||||
// Ensure the config text is parseable before proceeding…
|
||||
Config.parse(ByteArrayInputStream(configText.toByteArray(StandardCharsets.UTF_8)))
|
||||
|
||||
// Config text is valid, now create the tunnel…
|
||||
ConfigNamingDialogFragment.newInstance(configText).show(parentFragmentManager, null)
|
||||
} catch (e: Throwable) {
|
||||
onTunnelImportFinished(emptyList(), listOf<Throwable>(e), messageCallback)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTunnelImportFinished(tunnels: List<ObservableTunnel>, throwables: Collection<Throwable>, messageCallback: (CharSequence) -> Unit) {
|
||||
val context = Application.get().applicationContext
|
||||
var message = ""
|
||||
for (throwable in throwables) {
|
||||
val error = ErrorMessages[throwable]
|
||||
message = context.getString(R.string.import_error, error)
|
||||
Log.e(TAG, message, throwable)
|
||||
}
|
||||
if (tunnels.size == 1 && throwables.isEmpty())
|
||||
message = context.getString(R.string.import_success, tunnels[0].name)
|
||||
else if (tunnels.isEmpty() && throwables.size == 1)
|
||||
else if (throwables.isEmpty())
|
||||
message = context.resources.getQuantityString(R.plurals.import_total_success,
|
||||
tunnels.size, tunnels.size)
|
||||
else if (!throwables.isEmpty())
|
||||
message = context.resources.getQuantityString(R.plurals.import_partial_success,
|
||||
tunnels.size + throwables.size,
|
||||
tunnels.size, tunnels.size + throwables.size)
|
||||
|
||||
messageCallback(message)
|
||||
}
|
||||
|
||||
private const val TAG = "WireGuard/TunnelImporter"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.FloatingActionButton
|
||||
|
||||
/**
|
||||
* 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: FloatingActionButton?) {
|
||||
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: FloatingActionButton) {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<scale
|
||||
android:duration="300"
|
||||
android:fromXScale="1.0"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<scale
|
||||
android:duration="300"
|
||||
android:fromXScale="0"
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<!--
|
||||
~ Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
~ SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M19,11H7.83l4.88,-4.88c0.39,-0.39 0.39,-1.03 0,-1.42 -0.39,-0.39 -1.02,-0.39 -1.41,0l-6.59,6.59c-0.39,0.39 -0.39,1.02 0,1.41l6.59,6.59c0.39,0.39 1.02,0.39 1.41,0 0.39,-0.39 0.39,-1.02 0,-1.41L7.83,13H19c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,211 @@
|
||||
<!--
|
||||
~ Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
~ SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="1934.5dp"
|
||||
android:height="393.14dp"
|
||||
android:viewportWidth="1934.5"
|
||||
android:viewportHeight="393.14">
|
||||
<path
|
||||
android:fillColor="#88171a"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M0,0L1934.5,0L1934.5,393.14L0,393.14Z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M432.69,266.47 L367.47,66.48l24.46,0l49.64,154.34 50.91,-154.34l22.28,0l50.72,153.98 49.81,-153.98L639.03,66.48L573.99,266.47L555.87,266.47L503.16,105.43 450.99,266.47Z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m634.56,111.4l24.64,0l0,155.07l-24.64,0z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m716.31,198.9l0,67.57L692.04,266.47L692.04,112.13l106.52,0c15.82,0 28.2,3.8 37.14,11.41 8.94,7.61 13.41,18.18 13.41,31.7 0.18,10.5 -3.6,20.69 -10.6,28.53 -7.06,8.03 -16.27,12.95 -27.63,14.77l42.93,67.93l-26.27,0l-43.84,-67.57zM716.31,176.25l82.24,0c8.33,0 14.7,-1.81 19.11,-5.43 4.41,-3.63 6.61,-8.82 6.61,-15.58 0,-6.77 -2.2,-11.93 -6.61,-15.49 -4.41,-3.56 -10.78,-5.34 -19.11,-5.34l-82.24,0z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M871.98,266.47L871.98,111.77l145.1,0l0,22.64l-120.83,0l0,38.59l79.34,0l0,22.28l-79.34,0l0,48.91l127.53,0l0,22.28L871.97,266.47Z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m1194.46,212.31l0,-29.35l-54.53,0l0,-23.01l79.34,0l0,60.14c-9.66,16.67 -22.25,29.44 -37.77,38.31 -15.52,8.88 -33.13,13.32 -52.81,13.32 -29.95,0 -54.68,-9.87 -74.18,-29.62 -19.51,-19.75 -29.26,-44.78 -29.26,-75.09 0,-30.43 9.78,-55.52 29.34,-75.27 19.57,-19.75 44.27,-29.62 74.09,-29.62 18.48,0 35.32,4.05 50.54,12.13 15.2,8.08 28.01,20.01 37.13,34.6L1195.55,123.72c-6.13,-11.8 -15.58,-21.56 -27.17,-28.08 -12.09,-6.83 -25.79,-10.33 -39.67,-10.15 -22.46,0 -41.06,7.7 -55.8,23.1 -14.73,15.4 -22.1,34.87 -22.1,58.42 0,23.55 7.37,42.99 22.1,58.33 14.73,15.34 33.33,23.01 55.8,23.01 14.01,0 26.48,-3.02 37.41,-9.06 10.93,-6.04 20.38,-15.03 28.35,-26.99z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m1246.36,112.14l24.28,0l0,99.09c0,14.85 3.5,24.87 10.5,30.07 7.01,5.2 20.89,7.79 41.67,7.79 20.89,0 34.84,-2.59 41.84,-7.79 7.01,-5.19 10.51,-15.21 10.51,-30.07L1375.16,112.14l24.09,0l0,105.43c0,18.96 -6.01,32.67 -18.02,41.13 -12.02,8.45 -31.61,12.68 -58.79,12.68 -27.05,0 -46.49,-4.17 -58.33,-12.5 -11.84,-8.33 -17.75,-22.1 -17.75,-41.3z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m1395.46,266.47 l78.62,-154.7l15.22,0l79.34,154.7l-25.9,0l-20.11,-39.31l-81.7,0l-19.93,39.31zM1451.44,206.69l60.51,0l-30.07,-59.23z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m1595.56,198.9l0,67.57l-24.28,0L1571.29,112.13l106.52,0c15.82,0 28.2,3.8 37.14,11.41 8.94,7.61 13.41,18.18 13.41,31.7 0.18,10.5 -3.6,20.69 -10.6,28.53 -7.06,8.03 -16.27,12.95 -27.62,14.77l42.93,67.93l-26.27,0l-43.84,-67.57zM1595.56,176.25l82.24,0c8.33,0 14.7,-1.81 19.11,-5.43 4.41,-3.63 6.61,-8.82 6.61,-15.58 0,-6.77 -2.2,-11.93 -6.61,-15.49 -4.41,-3.56 -10.78,-5.34 -19.11,-5.34l-82.24,0z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m1820.26,111.77c25.24,0 45.59,7.21 61.05,21.65 15.46,14.43 23.19,33.18 23.19,56.25 0,23.31 -7.58,41.94 -22.73,55.89 -15.16,13.95 -35.66,20.92 -61.5,20.92l-68.66,0L1751.6,111.78l68.66,0zM1820.63,134.05l-44.75,0l0,110.14l44.75,0c18.35,0 32.79,-4.92 43.29,-14.77 10.51,-9.84 15.76,-23.21 15.76,-40.12 0,-16.3 -5.43,-29.59 -16.3,-39.85 -10.87,-10.26 -25.12,-15.4 -42.75,-15.4l0,0z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m429.67,318.63l0,18.61l-6.35,0l0,-40.45l37.37,0l0,5.92l-31.02,0l0,10.08l20.04,0l0,5.83z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m455.24,337.24 l20.56,-40.45l3.98,0l20.75,40.45l-6.78,0l-5.26,-10.28l-21.36,0l-5.21,10.28zM469.87,321.61l15.82,0l-7.86,-15.49z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m505.02,332.08 l3.08,-5.3c2.81,1.87 5.87,3.34 9.09,4.35 3.04,1.01 6.22,1.53 9.43,1.57 3.06,0.14 6.09,-0.62 8.71,-2.2 2.03,-1.16 3.3,-3.32 3.32,-5.66 0.08,-2.16 -1.1,-4.17 -3.03,-5.14 -2.03,-1.12 -5.21,-1.75 -9.57,-1.87 -7.36,-0.09 -12.41,-0.95 -15.16,-2.56 -2.75,-1.61 -4.13,-4.28 -4.13,-8.01 -0.04,-3.39 1.67,-6.56 4.53,-8.38 3.01,-2.12 7.06,-3.18 12.15,-3.18 3.43,-0.01 6.83,0.49 10.11,1.47 3.33,1.01 6.52,2.48 9.45,4.36l-3.17,5.26c-2.52,-1.7 -5.25,-3.03 -8.15,-3.95 -2.69,-0.9 -5.5,-1.37 -8.34,-1.4 -2.56,-0.11 -5.1,0.44 -7.39,1.61 -1.66,0.76 -2.75,2.39 -2.8,4.21 -0.03,1.77 1.13,3.33 2.82,3.84 1.88,0.76 5.14,1.14 9.78,1.14 6.44,0 11.31,1.09 14.59,3.27 3.29,2.18 4.93,5.42 4.93,9.71 0.01,3.8 -1.86,7.37 -5,9.52 -3.33,2.53 -7.65,3.79 -12.95,3.79 -3.95,-0.01 -7.88,-0.57 -11.67,-1.68 -3.75,-1.07 -7.34,-2.67 -10.63,-4.76z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m568.06,337.24l0,-34.81l-17.05,0l0,-5.63l40.45,0l0,5.63L574.36,302.43l0,34.81z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m604.52,345.48c0.91,-1.32 1.73,-2.7 2.46,-4.12 0.68,-1.32 1.25,-2.7 1.7,-4.12l-0.38,0c-0.96,0.01 -1.86,-0.44 -2.44,-1.21 -0.67,-0.87 -1.01,-1.95 -0.97,-3.06 -0.05,-1.22 0.36,-2.41 1.16,-3.34 0.73,-0.84 1.8,-1.32 2.92,-1.3 1.3,-0.02 2.52,0.64 3.22,1.73 0.89,1.39 1.32,3.02 1.23,4.67 -0.09,2.03 -0.71,3.99 -1.8,5.71 -1.5,2.45 -3.3,4.7 -5.35,6.7z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m668.13,337.24l0,-40.45l5.25,0l16.58,23.68 16.58,-23.68l5.26,0l0,40.45l-6.3,0l0,-29.08l-15.54,22.49 -15.49,-22.49l0,29.08z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m748.83,338.52c-7.2,0 -13.12,-2.04 -17.76,-6.11 -4.42,-3.87 -6.96,-9.46 -6.96,-15.33 -0.01,-5.87 2.52,-11.47 6.94,-15.34 4.63,-4.02 10.55,-6.04 17.78,-6.04 7.3,0 13.25,2.01 17.88,6.02 4.42,3.89 6.95,9.48 6.94,15.36 -0.01,5.88 -2.54,11.47 -6.96,15.35 -4.64,4.06 -10.59,6.09 -17.85,6.09zM748.83,332.6c5.4,0 9.81,-1.46 13.22,-4.38 3.25,-2.79 5.12,-6.86 5.11,-11.15 -0.01,-4.29 -1.89,-8.35 -5.14,-11.14 -3.43,-2.94 -7.82,-4.4 -13.19,-4.4 -5.36,0 -9.75,1.46 -13.14,4.38 -3.23,2.8 -5.09,6.87 -5.09,11.15 0,4.28 1.86,8.35 5.09,11.16 3.39,2.92 7.78,4.38 13.14,4.38z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m803.92,296.79c6.6,0 11.92,1.89 15.96,5.66 4.04,3.78 6.06,8.68 6.06,14.71 0,6.09 -1.98,10.96 -5.94,14.61 -3.96,3.65 -9.32,5.47 -16.08,5.47l-17.95,0l0,-40.45zM804.02,302.61l-11.7,0l0,28.8l11.7,0c4.8,0 8.57,-1.29 11.32,-3.86 2.75,-2.57 4.13,-6.07 4.13,-10.49 0.13,-3.93 -1.42,-7.72 -4.27,-10.42 -2.84,-2.68 -6.57,-4.03 -11.18,-4.03z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m837.87,337.24l0,-40.45l37.94,0l0,5.92l-31.59,0l0,10.09l20.75,0l0,5.82l-20.75,0l0,12.79l33.34,0l0,5.82z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m894.94,319.57l0,17.67l-6.35,0L888.59,296.88l27.85,0c4.14,0 7.37,0.99 9.71,2.98 2.38,2.08 3.67,5.14 3.51,8.29 0.05,2.75 -0.94,5.41 -2.77,7.46 -1.86,2.11 -4.43,3.48 -7.22,3.86l11.22,17.76l-6.87,0l-11.46,-17.67zM894.94,313.65l21.5,0c1.78,0.12 3.55,-0.39 5,-1.42 1.18,-1.01 1.82,-2.52 1.72,-4.07 0.1,-1.55 -0.54,-3.05 -1.72,-4.05 -1.46,-1.02 -3.22,-1.52 -5,-1.4l-21.5,0z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m943.11,337.24l0,-40.45l3.32,0l25.72,28.42l0,-28.42l6.2,0l0,40.45l-3.32,0l-25.67,-28.32l0,28.32z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m999,345.48c0.91,-1.32 1.73,-2.7 2.46,-4.12 0.68,-1.32 1.25,-2.7 1.7,-4.12l-0.37,0c-0.96,0.01 -1.87,-0.44 -2.44,-1.21 -0.67,-0.87 -1.01,-1.95 -0.97,-3.06 -0.05,-1.22 0.37,-2.41 1.16,-3.34 0.72,-0.84 1.8,-1.32 2.91,-1.3 1.3,-0.02 2.52,0.64 3.22,1.73 0.89,1.39 1.32,3.02 1.23,4.67 -0.09,2.03 -0.71,3.99 -1.8,5.71 -1.5,2.45 -3.3,4.7 -5.35,6.7z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m1059.16,332.08 l3.08,-5.3c2.81,1.87 5.87,3.34 9.09,4.35 3.04,1.01 6.22,1.53 9.43,1.57 3.06,0.14 6.09,-0.62 8.71,-2.2 2.03,-1.16 3.3,-3.32 3.31,-5.66 0.09,-2.16 -1.1,-4.17 -3.03,-5.14 -2.02,-1.12 -5.21,-1.75 -9.57,-1.87 -7.35,-0.09 -12.41,-0.95 -15.15,-2.56 -2.75,-1.61 -4.12,-4.28 -4.12,-8.01 -0.04,-3.39 1.67,-6.56 4.53,-8.38 3.02,-2.12 7.06,-3.18 12.15,-3.18 3.43,-0.01 6.83,0.49 10.11,1.47 3.33,1.01 6.52,2.48 9.45,4.36l-3.17,5.26c-2.51,-1.7 -5.25,-3.03 -8.15,-3.95 -2.69,-0.9 -5.51,-1.37 -8.34,-1.4 -2.56,-0.11 -5.1,0.44 -7.39,1.61 -1.66,0.76 -2.74,2.39 -2.8,4.21 -0.03,1.77 1.13,3.33 2.82,3.84 1.88,0.76 5.14,1.14 9.78,1.14 6.44,0 11.3,1.09 14.59,3.27 3.28,2.18 4.93,5.41 4.93,9.71 0.01,3.8 -1.86,7.37 -5,9.52 -3.33,2.53 -7.65,3.79 -12.95,3.79 -3.95,-0.01 -7.88,-0.57 -11.68,-1.68 -3.75,-1.07 -7.33,-2.67 -10.63,-4.76z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m1112.56,337.24l0,-40.45l37.94,0l0,5.92L1118.91,302.71l0,10.09l20.74,0l0,5.82L1118.91,318.63l0,12.79l33.34,0l0,5.82z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m1206.06,327.34c-2.21,3.55 -5.35,6.42 -9.07,8.31 -3.96,1.96 -8.34,2.94 -12.77,2.87 -7.1,0 -12.95,-2.01 -17.55,-6.04 -4.38,-3.89 -6.89,-9.48 -6.89,-15.34 0,-5.87 2.51,-11.45 6.89,-15.35 4.59,-4.06 10.44,-6.08 17.55,-6.08 4.15,-0.06 8.25,0.79 12.03,2.49 3.47,1.55 6.47,3.97 8.71,7.04l-5.02,3.51c-1.7,-2.33 -3.97,-4.16 -6.6,-5.33 -2.87,-1.28 -5.98,-1.92 -9.12,-1.87 -4.69,-0.17 -9.27,1.41 -12.86,4.43 -3.23,2.78 -5.09,6.84 -5.09,11.11 0.01,4.27 1.87,8.32 5.11,11.1 3.58,3.02 8.16,4.59 12.83,4.43 3.3,0.07 6.57,-0.66 9.52,-2.13 2.93,-1.58 5.41,-3.86 7.24,-6.64z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m1218.66,296.89l6.34,0l0,25.91c0,3.89 0.92,6.51 2.75,7.86 1.83,1.36 5.46,2.04 10.89,2.04 5.46,0 9.11,-0.68 10.94,-2.04 1.83,-1.36 2.75,-3.98 2.75,-7.86l0,-25.91l6.3,0l0,27.56c0,4.96 -1.57,8.54 -4.71,10.75 -3.14,2.21 -8.27,3.31 -15.37,3.31 -7.07,0 -12.16,-1.09 -15.25,-3.27 -3.09,-2.18 -4.64,-5.78 -4.64,-10.8z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m1280.86,319.57l0,17.67l-6.34,0L1274.52,296.88l27.85,0c4.14,0 7.37,0.99 9.71,2.98 2.38,2.08 3.67,5.14 3.51,8.29 0.05,2.75 -0.94,5.41 -2.77,7.46 -1.86,2.11 -4.43,3.48 -7.22,3.86l11.22,17.76l-6.86,0l-11.46,-17.67zM1280.86,313.65l21.5,0c1.78,0.12 3.55,-0.39 5,-1.42 1.18,-1.01 1.82,-2.52 1.73,-4.07 0.1,-1.55 -0.55,-3.05 -1.73,-4.05 -1.46,-1.02 -3.22,-1.52 -5,-1.4l-21.5,0z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m1328.96,337.24l0,-40.45l37.94,0l0,5.92l-31.59,0l0,10.09l20.75,0l0,5.82l-20.75,0l0,12.79l33.34,0l0,5.82z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m1431.46,337.24l-3.27,0l-20.32,-40.45l6.72,0l15.2,30.27 14.97,-30.27l6.96,0z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m1468.06,319.57l0,17.67l-6.35,0L1461.71,296.79l29.41,0c4.17,0 7.42,0.99 9.73,2.96 2.37,2.09 3.66,5.15 3.48,8.31 0.1,3.13 -1.16,6.16 -3.46,8.29 -2.49,2.22 -5.76,3.37 -9.09,3.22zM1468.06,313.65l23.06,0c1.77,0.11 3.51,-0.39 4.95,-1.42 2.28,-2.21 2.31,-5.85 0.07,-8.1 -1.35,-1.03 -3.04,-1.53 -4.73,-1.42l-23.35,0z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m1515.26,337.24l0,-40.45l3.32,0l25.71,28.42l0,-28.42l6.21,0l0,40.45l-3.32,0l-25.67,-28.32l0,28.32z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m1610.06,337.24l0,-34.81l-17.05,0l0,-5.63l40.45,0l0,5.63l-17.1,0l0,34.81z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m1641.76,296.89l6.35,0l0,25.91c0,3.89 0.91,6.51 2.75,7.86 1.83,1.36 5.46,2.04 10.89,2.04 5.46,0 9.11,-0.68 10.94,-2.04 1.83,-1.36 2.75,-3.98 2.75,-7.86l0,-25.91l6.3,0l0,27.56c0,4.96 -1.57,8.54 -4.71,10.75 -3.14,2.21 -8.27,3.31 -15.37,3.31 -7.07,0 -12.16,-1.09 -15.25,-3.27 -3.09,-2.18 -4.64,-5.78 -4.64,-10.8z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m1697.66,337.24l0,-40.45l3.31,0l25.72,28.42l0,-28.42l6.21,0l0,40.45l-3.32,0l-25.67,-28.32l0,28.32z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m1748.76,337.24l0,-40.45l3.32,0l25.71,28.42L1777.79,296.79L1784,296.79l0,40.45l-3.32,0l-25.67,-28.32l0,28.32z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m1799.76,337.24l0,-40.45l37.94,0l0,5.92l-31.59,0l0,10.09l20.75,0l0,5.82l-20.75,0l0,12.79l33.34,0l0,5.82z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m1850.46,337.24l0,-40.45l6.35,0l0,34.62l30.03,0l0,5.82z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M329.74,185.56C329.74,185.56 336.68,40 176.7,40 35.22,40 30.8,179.63 30.8,179.63 30.8,179.63 9.99,340 179.96,340 342.98,340 329.74,185.56 329.74,185.56ZM131.94,134.7c30.02,-18.36 68.36,-7.14 82.73,20.47 2.72,5.23 3.07,13.29 1.34,18.78 -5.95,18.96 -20.02,29.59 -39.31,34.1 5.69,-4.87 10.22,-10.4 11.66,-18.03 1.52,-7.31 -0.13,-14.93 -4.55,-20.95 -7.03,-9.65 -19.59,-13.49 -30.81,-9.39 -11.89,4.51 -18.39,15.35 -17.22,28.68 1.09,12.38 10.48,20.41 28.06,23.45 -2.63,1.39 -4.65,2.42 -6.63,3.52 -8.05,4.41 -15.06,10.51 -20.54,17.87 -1.79,2.41 -3.01,2.6 -5.73,0.94 -35.34,-21.61 -37.61,-75.84 0.98,-99.45zM105.49,268.23c-5.68,1.44 -11.18,3.57 -16.98,5.47 2.84,-19.15 25.27,-36.79 44.23,-34.78 -5.49,7.57 -8.7,16.56 -9.24,25.9 -6.3,1.16 -12.25,1.94 -18.01,3.4zM226.28,81.25c5.61,0.21 11.23,0.12 16.84,0.25 1.4,0.09 2.8,0.29 4.17,0.58 -1.26,1.93 -2.67,3.75 -4.23,5.43 -2.01,1.87 -4.28,3.7 -7.17,0.85 -0.69,-0.68 -2.34,-0.53 -3.55,-0.54 -5.58,-0.07 -11.17,-0.25 -16.75,-0.04 -4.84,0.16 -9.66,0.65 -14.43,1.47 -0.9,0.16 -2.23,3.13 -1.82,4.22 0.97,2.59 2.38,5.44 4.47,7.09 7.75,6.11 15.97,11.59 23.75,17.66 7.56,5.9 14.59,12.36 18.87,21.25 5.58,11.59 5.74,23.74 3.34,35.95 -4.02,20.38 -14.33,37.26 -31.03,49.53 -6.73,4.94 -15.06,7.74 -22.77,11.29 -6.78,3.13 -13.75,5.81 -20.55,8.9 -12.25,5.57 -19.13,18.87 -17.1,32.69 1.85,12.69 12.98,23.27 25.73,25.46 15.29,2.62 31.07,-7.32 34.81,-22.86 4.2,-17.48 -5.29,-33.08 -23.07,-37.81 -0.78,-0.21 -1.57,-0.41 -3.2,-0.83 4.75,-2.12 8.86,-3.64 12.66,-5.72 6.61,-3.64 13.11,-7.49 19.48,-11.56 1.88,-1.2 2.89,-1.2 4.49,0.18 12.23,10.57 19.52,23.72 21.56,39.84 3.39,26.68 -9.25,51.2 -33.07,63.76 -36.87,19.44 -81.97,-2.68 -90.11,-43.55 -6.97,-35 17.73,-66.75 47.46,-72.88 12.79,-2.63 24.48,-7.96 33.57,-17.81 5.87,-6.35 8.71,-11.81 9.68,-14.27 1.81,-4.61 2.73,-9.52 2.72,-14.47 -0.2,-4.29 -1.2,-8.49 -2.96,-12.4 -3.1,-7.07 -15,-18.33 -17.94,-20.7l-28,-21.92c-0.98,-0.81 -2.1,-0.75 -4.51,-0.59 -2.86,0.19 -10.18,0.6 -13.33,-0.23 2.55,-1.93 9.52,-4.74 12.51,-7.01 -9.08,-6.13 -19.43,-3.92 -28.94,-5.75 2.2,-4.09 13.08,-10.39 19.27,-11.09 -0.37,-3.46 -0.93,-6.89 -1.69,-10.28 -0.38,-1.39 -1.93,-2.74 -3.29,-3.54 -3.29,-1.93 -6.77,-3.52 -10.55,-5.43 3.39,-2.19 7.31,-3.4 11.33,-3.51 3.82,-0.15 7.63,0.23 11.35,1.1 6.74,1.54 12.12,0.54 17.49,-4.05 -4.22,-1.7 -8.45,-3.25 -12.54,-5.09 -4.03,-1.84 -7.96,-3.9 -11.78,-6.16 10.62,1.48 20.89,5.46 31.75,4 0.09,-0.49 0.19,-0.99 0.28,-1.48 -8.12,-1.89 -16.24,-3.78 -25.23,-5.87 15.04,-1.37 29.04,-1.6 42.3,4.85 3.73,1.82 7.63,3.32 11.21,5.4 1.74,1.02 2.92,3.01 4.35,4.56 1.13,1.23 2.05,2.88 3.44,3.63 5.3,2.82 11.13,2.93 17.08,2.79 0.05,-0.68 0.09,-1.31 0.13,-1.99 5.98,1.87 12.72,8.77 12.71,13.8 -9.69,0 -19.37,-0.04 -29.06,0.06 -1.04,0.01 -2.06,0.77 -3.09,1.17 0.98,0.57 1.94,1.6 2.94,1.64z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m213.78,66.91c-0.84,0.53 -0.94,1.71 -0.19,2.37 0.61,1.08 1.99,1.45 3.07,0.82 0.94,-0.47 1.85,-0.97 2.98,-1.57 -0.91,-0.78 -1.64,-1.42 -2.39,-2.03 -1.32,-1.09 -2.41,-0.41 -3.47,0.41z"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillColor="#d1d1d1"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="m1890,97.71l-1.24,0c-7.56,0.08 -11.84,6.64 -11.84,13 0,6.16 3.92,12.32 12.2,12.32l0.56,0c8.6,-0.24 12.36,-6.16 12.36,-12.24l0,-0.72c-0.28,-6.36 -4.84,-12.36 -12.04,-12.36zM1884.48,102.87l0,14.44l1.8,0l0,-12.96l3.4,0c1.88,0 2.8,1.16 2.8,2.32 0,1.4 -1.36,3 -3.8,3 -0.37,0 -0.84,-0.08 -1.28,-0.16l5.44,7.8l2.36,0l-4.92,-6.64c2.4,-0.44 4,-2.36 4,-4.24 0,-1.76 -1.52,-3.56 -4.88,-3.56zM1881.04,103.16c2.52,-2.72 5.16,-3.72 8.24,-3.72l1.28,0c5.48,0.24 9.44,5.8 9.44,11.16 0,0.76 -0.08,1.64 -0.24,2.36 -0.84,4.92 -5.12,8.36 -10.08,8.36 -0.12,0 -0.37,0.04 -0.49,0.04 -6.28,0 -10.28,-5.68 -10.28,-11.36 0,-0.48 0.08,-1.12 0.12,-1.64 0,0 0.28,-3.12 2,-5.2z"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -62,6 +62,8 @@
|
||||
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"
|
||||
@@ -83,6 +85,9 @@
|
||||
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"
|
||||
@@ -107,6 +112,9 @@
|
||||
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}"
|
||||
@@ -131,6 +139,9 @@
|
||||
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}"
|
||||
@@ -155,6 +166,9 @@
|
||||
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}"
|
||||
@@ -169,7 +183,7 @@
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/listen_port_text"
|
||||
android:text="@string/listen_port"
|
||||
android:visibility="@{config.interface.listenPort.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
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"
|
||||
@@ -181,9 +195,13 @@
|
||||
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.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
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"
|
||||
@@ -197,7 +215,7 @@
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/mtu_text"
|
||||
android:text="@string/mtu"
|
||||
android:visibility="@{config.interface.mtu.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
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"
|
||||
@@ -210,9 +228,12 @@
|
||||
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.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
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"
|
||||
@@ -237,6 +258,9 @@
|
||||
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}"
|
||||
|
||||
@@ -48,6 +48,8 @@
|
||||
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}"
|
||||
@@ -62,7 +64,7 @@
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/pre_shared_key_text"
|
||||
android:text="@string/pre_shared_key"
|
||||
android:visibility="@{item.preSharedKey.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
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" />
|
||||
|
||||
@@ -74,9 +76,12 @@
|
||||
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.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
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=" />
|
||||
@@ -98,6 +103,9 @@
|
||||
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}"
|
||||
@@ -112,7 +120,7 @@
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/endpoint_text"
|
||||
android:text="@string/endpoint"
|
||||
android:visibility="@{item.endpoint.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
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" />
|
||||
|
||||
@@ -122,9 +130,12 @@
|
||||
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.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
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" />
|
||||
@@ -136,7 +147,7 @@
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/persistent_keepalive_text"
|
||||
android:text="@string/persistent_keepalive"
|
||||
android:visibility="@{item.persistentKeepalive.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
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" />
|
||||
|
||||
@@ -146,9 +157,12 @@
|
||||
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.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
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" />
|
||||
@@ -173,6 +187,7 @@
|
||||
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"
|
||||
|
||||
@@ -76,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>
|
||||
@@ -98,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()}"
|
||||
@@ -111,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"
|
||||
@@ -123,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}" />
|
||||
@@ -144,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>
|
||||
|
||||
@@ -154,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>
|
||||
@@ -185,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>
|
||||
|
||||
@@ -195,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>
|
||||
@@ -216,6 +247,9 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="4dp"
|
||||
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"
|
||||
@@ -223,11 +257,13 @@
|
||||
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"
|
||||
@@ -238,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"
|
||||
|
||||
@@ -51,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"
|
||||
@@ -73,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>
|
||||
@@ -84,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()}"
|
||||
@@ -106,18 +117,23 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="4dp"
|
||||
android:hint="@string/persistent_keepalive"
|
||||
app:suffixText="@{@plurals/persistent_keepalive_seconds_suffix(BindingAdapters.tryParseInt(item.persistentKeepalive))}"
|
||||
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:text="@={item.persistentKeepalive}"/>
|
||||
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>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
@@ -134,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>
|
||||
|
||||
@@ -153,7 +173,11 @@
|
||||
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>
|
||||
|
||||
@@ -164,6 +188,8 @@
|
||||
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"
|
||||
|
||||
@@ -31,8 +31,9 @@
|
||||
android:id="@+id/tunnel_list"
|
||||
android:layout_width="match_parent"
|
||||
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}"
|
||||
@@ -72,6 +73,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="@dimen/fab_margin"
|
||||
android:nextFocusUp="@id/tunnel_list"
|
||||
app:srcCompat="@drawable/ic_action_add_white" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="android.view.View" />
|
||||
|
||||
<import type="com.wireguard.android.model.ObservableTunnel" />
|
||||
|
||||
<import type="com.wireguard.android.activity.TvMainActivity.KeyedFile" />
|
||||
|
||||
<variable
|
||||
name="isDeleting"
|
||||
type="androidx.databinding.ObservableBoolean" />
|
||||
|
||||
<variable
|
||||
name="files"
|
||||
type="com.wireguard.android.databinding.ObservableKeyedArrayList<String, KeyedFile>" />
|
||||
|
||||
<variable
|
||||
name="filesRoot"
|
||||
type="androidx.databinding.ObservableField<String>" />
|
||||
|
||||
<variable
|
||||
name="tunnels"
|
||||
type="com.wireguard.android.databinding.ObservableKeyedArrayList<String, ObservableTunnel>" />
|
||||
|
||||
<variable
|
||||
name="tunnelRowConfigurationHandler"
|
||||
type="com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler" />
|
||||
|
||||
<variable
|
||||
name="filesRowConfigurationHandler"
|
||||
type="com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/banner_logo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginTop="5dp"
|
||||
app:cardElevation="2dp"
|
||||
app:contentPadding="0dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="320dp"
|
||||
android:layout_height="67dp"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:scaleType="fitXY"
|
||||
app:srcCompat="@drawable/tv_logo_banner" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/tunnel_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="@{(tunnels.isEmpty || !filesRoot.isEmpty) ? View.GONE : View.VISIBLE}"
|
||||
app:configurationHandler="@{tunnelRowConfigurationHandler}"
|
||||
app:items="@{tunnels}"
|
||||
app:layout="@{@layout/tv_tunnel_list_item}"
|
||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||
app:layout_constraintBottom_toTopOf="@id/delete_button"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/banner_logo"
|
||||
app:spanCount="3"
|
||||
tools:itemCount="10"
|
||||
tools:listitem="@layout/tv_tunnel_list_item" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/files_root_label"
|
||||
style="@style/TextAppearance.MaterialComponents.Headline5"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@{filesRoot}"
|
||||
android:visibility="@{filesRoot.isEmpty ? View.GONE : View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/banner_logo"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/files_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="@{filesRoot.isEmpty ? View.GONE : View.VISIBLE}"
|
||||
app:configurationHandler="@{filesRowConfigurationHandler}"
|
||||
app:items="@{files}"
|
||||
app:layout="@{@layout/tv_file_list_item}"
|
||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||
app:layout_constraintBottom_toTopOf="@id/import_button"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/files_root_label"
|
||||
app:spanCount="5"
|
||||
tools:itemCount="10"
|
||||
tools:listitem="@layout/tv_file_list_item"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.MaterialComponents.Headline4"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:text="@string/tv_add_tunnel_get_started"
|
||||
android:visibility="@{(filesRoot.isEmpty && tunnels.isEmpty) ? View.VISIBLE : View.GONE}"
|
||||
app:layout_constraintBottom_toTopOf="@id/delete_button"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/banner_logo"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/import_button"
|
||||
style="@style/Widget.MaterialComponents.Button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:minWidth="0dp"
|
||||
android:visibility="@{isDeleting ? View.GONE : View.VISIBLE}"
|
||||
app:icon="@{filesRoot.isEmpty ? @drawable/ic_action_add_white : @drawable/ic_arrow_back}"
|
||||
app:iconPadding="0dp"
|
||||
app:iconTint="?attr/colorOnPrimary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/delete_button"
|
||||
style="@style/Widget.MaterialComponents.Button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:minWidth="0dp"
|
||||
android:visibility="@{((tunnels.isEmpty && !isDeleting) || !filesRoot.isEmpty) ? View.GONE : View.VISIBLE}"
|
||||
app:icon="@{isDeleting ? @drawable/ic_arrow_back : @drawable/ic_action_delete}"
|
||||
app:iconPadding="0dp"
|
||||
app:iconTint="?attr/colorOnPrimary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="com.wireguard.android.activity.TvMainActivity.KeyedFile" />
|
||||
|
||||
<variable
|
||||
name="key"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="KeyedFile" />
|
||||
</data>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="320dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="0dp"
|
||||
android:backgroundTint="@color/tv_card_background"
|
||||
android:checkable="true"
|
||||
android:focusable="true"
|
||||
app:contentPadding="8dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
style="@style/TextAppearance.MaterialComponents.Headline5"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{key}"
|
||||
android:textColor="?attr/colorOnPrimary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</layout>
|
||||
@@ -0,0 +1,81 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="android.view.View" />
|
||||
|
||||
<import type="com.wireguard.android.model.ObservableTunnel" />
|
||||
|
||||
<import type="com.wireguard.android.backend.Tunnel.State" />
|
||||
|
||||
<variable
|
||||
name="isDeleting"
|
||||
type="androidx.databinding.ObservableBoolean" />
|
||||
|
||||
<variable
|
||||
name="isFocused"
|
||||
type="androidx.databinding.ObservableBoolean" />
|
||||
|
||||
<variable
|
||||
name="key"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="com.wireguard.android.model.ObservableTunnel" />
|
||||
</data>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="225dp"
|
||||
android:layout_height="110dp"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="0dp"
|
||||
android:backgroundTint="@{(item.state == State.UP && !isDeleting) ? @color/secondary_dark_color : (isDeleting && isFocused) ? @color/tv_card_delete_background : @color/tv_card_background}"
|
||||
android:checkable="true"
|
||||
android:focusable="true"
|
||||
app:contentPadding="8dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/tunnel_name"
|
||||
style="@style/TextAppearance.MaterialComponents.Headline5"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{item.name}"
|
||||
android:textColor="?attr/colorOnPrimary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="@sample/interface_names.json/names/names/name" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/tunnel_transfer"
|
||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="@{isDeleting ? View.GONE : View.VISIBLE}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:text="rx: 200 MB, tx: 100 MB" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/tunnel_delete"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/tv_delete"
|
||||
android:visibility="@{(isDeleting && isFocused) ? View.VISIBLE : View.GONE}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:visibility="gone" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</layout>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 53 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="delete_success">
|
||||
<item quantity="zero">تم حذف نفق %d بنجاح</item>
|
||||
<item quantity="one">تم حذف %d نفق بنجاح</item>
|
||||
<item quantity="two">تم حذف %d نفق بنجاح</item>
|
||||
<item quantity="few">تم حذف %d نفق بنجاح</item>
|
||||
<item quantity="many">تم حذف %d نفق بنجاح</item>
|
||||
<item quantity="other">تم حذف %d انفاق بنجاح</item>
|
||||
</plurals>
|
||||
<plurals name="delete_title">
|
||||
<item quantity="zero">%d نفق محدد</item>
|
||||
<item quantity="one">%d نفق محدد</item>
|
||||
<item quantity="two">%d نفق محدد</item>
|
||||
<item quantity="few">%d عنصر محدد</item>
|
||||
<item quantity="many">%d عنصر محدد</item>
|
||||
<item quantity="other">%d عنصر محدد</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
@@ -0,0 +1,98 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="delete_error">
|
||||
<item quantity="one">No s\'ha pogut esborrar %d túnel: %s</item>
|
||||
<item quantity="other">No s\'han pogut esborrar %d túnels: %s</item>
|
||||
</plurals>
|
||||
<plurals name="delete_title">
|
||||
<item quantity="one">%d túnel seleccionat</item>
|
||||
<item quantity="other">%d túnels seleccionats</item>
|
||||
</plurals>
|
||||
<plurals name="n_excluded_applications">
|
||||
<item quantity="one">%d exclòs</item>
|
||||
<item quantity="other">%d exclosos</item>
|
||||
</plurals>
|
||||
<plurals name="n_included_applications">
|
||||
<item quantity="one">%d inclòs</item>
|
||||
<item quantity="other">%d inclosos</item>
|
||||
</plurals>
|
||||
<string name="all_applications">Totes les aplicacions</string>
|
||||
<string name="exclude_from_tunnel">Exclou</string>
|
||||
<string name="include_in_tunnel">Inclou només</string>
|
||||
<plurals name="include_n_applications">
|
||||
<item quantity="one">Inclou %d aplicació</item>
|
||||
<item quantity="other">Inclou %d aplicacions</item>
|
||||
</plurals>
|
||||
<plurals name="exclude_n_applications">
|
||||
<item quantity="one">Exclou %d aplicació</item>
|
||||
<item quantity="other">Exclou %d aplicacions</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_unit">
|
||||
<item quantity="one">cada segon</item>
|
||||
<item quantity="other">cada %d segons</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_suffix">
|
||||
<item quantity="one">segon</item>
|
||||
<item quantity="other">segons</item>
|
||||
</plurals>
|
||||
<string name="add_peer">Afegir parell</string>
|
||||
<string name="addresses">Adreces</string>
|
||||
<string name="applications">Aplicacions</string>
|
||||
<string name="allowed_ips">IPs permeses</string>
|
||||
<string name="bad_config_explanation_positive_number">: Ha de ser positiu</string>
|
||||
<string name="bad_config_reason_invalid_key">Clau no vàlida</string>
|
||||
<string name="bad_config_reason_invalid_number">Número no vàlid</string>
|
||||
<string name="bad_config_reason_invalid_value">Valor no vàlid</string>
|
||||
<string name="bad_config_reason_syntax_error">Error de sintaxi</string>
|
||||
<string name="bad_config_reason_unknown_attribute">Atribut desconegut</string>
|
||||
<string name="bad_config_reason_unknown_section">Secció desconeguda</string>
|
||||
<string name="cancel">Cancel·la</string>
|
||||
<string name="create_activity_title">Crear túnel WireGuard</string>
|
||||
<string name="create_empty">Crea des de zero</string>
|
||||
<string name="create_from_file">Importa des de fitxer o arxiu</string>
|
||||
<string name="create_from_qr_code">Escaneja codi QR</string>
|
||||
<string name="create_tunnel">Crear túnel</string>
|
||||
<string name="dark_theme_title">Utilitza tema fosc</string>
|
||||
<string name="delete">Elimina</string>
|
||||
<string name="dns_servers">Servidors DNS</string>
|
||||
<string name="edit">Edita</string>
|
||||
<string name="exclude_private_ips">Exclou IPs privades</string>
|
||||
<string name="generate_new_private_key">Genera nova clau privada</string>
|
||||
<string name="generic_error">Error “%s” desconegut</string>
|
||||
<string name="hint_generated">(generat)</string>
|
||||
<string name="hint_optional">(opcional)</string>
|
||||
<string name="hint_optional_discouraged">(opcional, no recomanat)</string>
|
||||
<string name="hint_random">(aleatori)</string>
|
||||
<string name="import_from_qr_code">Importa túnel desde codi QR</string>
|
||||
<string name="import_success">Importat “%s”</string>
|
||||
<string name="interface_title">Interfície</string>
|
||||
<string name="key_length_error">Longitud de clau incorrecta</string>
|
||||
<string name="log_export_success">Guardat a \"%s\"</string>
|
||||
<string name="log_export_title">Exporta el registre</string>
|
||||
<string name="log_saver_activity_label">Guarda registre</string>
|
||||
<string name="log_viewer_pref_title">Mostra el registre d\'aplicació</string>
|
||||
<string name="log_viewer_title">Registre</string>
|
||||
<string name="multiple_tunnels_title">Permet múltiples túnels simultanis</string>
|
||||
<string name="name">Nom</string>
|
||||
<string name="no_tunnels_error">No existeixen túnels</string>
|
||||
<string name="parse_error_inet_address">Adreça IP</string>
|
||||
<string name="parse_error_inet_network">Xarxa IP</string>
|
||||
<string name="parse_error_integer">número</string>
|
||||
<string name="peer">Parell</string>
|
||||
<string name="pre_shared_key_enabled">activat</string>
|
||||
<string name="private_key">Clau privada</string>
|
||||
<string name="public_key">Clau pública</string>
|
||||
<string name="restore_on_boot_title">Restableix a l\'inici</string>
|
||||
<string name="save">Guarda</string>
|
||||
<string name="select_all">Selecciona-ho tot</string>
|
||||
<string name="settings">Configuració</string>
|
||||
<string name="tunnel_error_already_exists">El túnel \"%s\" ja existeix</string>
|
||||
<string name="tunnel_error_invalid_name">Nom no vàlid</string>
|
||||
<string name="tunnel_list_placeholder">Afegiu un túnel usant el botó blau</string>
|
||||
<string name="tunnel_name">Nom del túnel</string>
|
||||
<string name="unknown_error">Error desconegut</string>
|
||||
<string name="version_summary_unknown">Versió de %s desconeguda</string>
|
||||
<string name="zip_export_success">Guardat a \"%s\"</string>
|
||||
<string name="biometric_auth_error">Error d\'autenticació</string>
|
||||
<string name="biometric_auth_error_reason">Error d\'autenticació: %s</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="delete_error">
|
||||
<item quantity="one">Kan ikke slette %d tunnel: %s</item>
|
||||
<item quantity="other">Ikke i stand til at slette %d tunneler: %s</item>
|
||||
</plurals>
|
||||
<plurals name="delete_success">
|
||||
<item quantity="one">Slettede %d tunnel</item>
|
||||
<item quantity="other">%d tunneller blev slettet</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="delete_error">
|
||||
<item quantity="one">%d Tunnel konnte nicht gelöscht werden: %s</item>
|
||||
<item quantity="one">%d Tunnel konnten nicht gelöscht werden: %s</item>
|
||||
<item quantity="other">%d Tunnel konnten nicht gelöscht werden: %s</item>
|
||||
</plurals>
|
||||
<plurals name="delete_success">
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="delete_error">
|
||||
<item quantity="one">Δεν είναι δυνατή η διαγραφή του %d tunnel: %s</item>
|
||||
<item quantity="other">Δεν είναι δυνατή η διαγραφή %d tunnels: %s</item>
|
||||
</plurals>
|
||||
<plurals name="delete_success">
|
||||
<item quantity="one">Το %d tunnel διαγράφηκε με επιτυχία</item>
|
||||
<item quantity="other">Διαγράφηκαν επιτυχώς τα %d tunnels</item>
|
||||
</plurals>
|
||||
<plurals name="delete_title">
|
||||
<item quantity="one">%d επιλεγμένο tunnel</item>
|
||||
<item quantity="other">%d επιλεγμένα tunnels</item>
|
||||
</plurals>
|
||||
<plurals name="import_partial_success">
|
||||
<item quantity="one">Έγινε εισαγωγή %1$d από %2$d tunnels</item>
|
||||
<item quantity="other">Έγινε εισαγωγή %1$d από %2$d tunnels</item>
|
||||
</plurals>
|
||||
<plurals name="import_total_success">
|
||||
<item quantity="one">Εισαγωγή tunnel %d</item>
|
||||
<item quantity="other">Εισαγωγή tunnels %d</item>
|
||||
</plurals>
|
||||
<plurals name="set_excluded_applications">
|
||||
<item quantity="one">%d Εξαιρούμενη εφαρμογή</item>
|
||||
<item quantity="other">%d Εξαιρούμενες εφαρμογές</item>
|
||||
</plurals>
|
||||
<plurals name="set_included_applications">
|
||||
<item quantity="one">%d Συμπεριλαμβανόμενη εφαρμογή</item>
|
||||
<item quantity="other">%d Συμπεριλαμβανόμενες εφαρμογές</item>
|
||||
</plurals>
|
||||
<plurals name="n_excluded_applications">
|
||||
<item quantity="one">εξαιρέθηκε %d</item>
|
||||
<item quantity="other">εξαιρέθηκε %d</item>
|
||||
</plurals>
|
||||
<plurals name="n_included_applications">
|
||||
<item quantity="one">περιλαμβάνεται %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 app</item>
|
||||
<item quantity="other">Συμπερίληψη %d apps</item>
|
||||
</plurals>
|
||||
<plurals name="exclude_n_applications">
|
||||
<item quantity="one">Εξαίρεση %d app</item>
|
||||
<item quantity="other">Εξαίρεση %d apps</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_unit">
|
||||
<item quantity="one">κάθε δευτερόλεπτο</item>
|
||||
<item quantity="other">κάθε %d δευτερόλεπτα</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_suffix">
|
||||
<item quantity="one">δευτερόλεπτο</item>
|
||||
<item quantity="other">δευτερόλεπτα</item>
|
||||
</plurals>
|
||||
<string name="use_all_applications">Χρησιμοποίησε όλες τις εφαρμογές</string>
|
||||
<string name="add_peer">Προσθήκη peer</string>
|
||||
<string name="addresses">Διευθύνσεις</string>
|
||||
<string name="applications">Εφαρμογές</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,180 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="n_excluded_applications">
|
||||
<item quantity="one">%d excluido</item>
|
||||
<item quantity="other">%d excluidos</item>
|
||||
</plurals>
|
||||
<string name="all_applications">Todas las Aplicaciones</string>
|
||||
<string name="exclude_from_tunnel">Excluir</string>
|
||||
<string name="include_in_tunnel">Sólo inclusión</string>
|
||||
<string name="use_all_applications">Usar todas las aplicaciones</string>
|
||||
<string name="add_peer">Añadir par</string>
|
||||
<string name="addresses">Direcciones</string>
|
||||
<string name="applications">Aplicaciones</string>
|
||||
<string name="allow_remote_control_intents_summary_off">Las aplicaciones externas no pueden cambiar túneles (recomendado)</string>
|
||||
<string name="allow_remote_control_intents_summary_on">Las aplicaciones externas pueden cambiar túneles (avanzado)</string>
|
||||
<string name="allow_remote_control_intents_title">Permitir aplicaciones de mando remoto</string>
|
||||
<string name="allowed_ips">IPs permitidas</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 en %2$s</string>
|
||||
<string name="bad_config_explanation_pka">: Debe ser positivo y no más de 65535</string>
|
||||
<string name="bad_config_explanation_positive_number">: Debe ser positivo</string>
|
||||
<string name="bad_config_explanation_udp_port">: Debe ser un número válido de puerto UDP</string>
|
||||
<string name="bad_config_reason_invalid_key">Clave inválida</string>
|
||||
<string name="bad_config_reason_invalid_number">Número inválido</string>
|
||||
<string name="bad_config_reason_invalid_value">Valor inválido</string>
|
||||
<string name="bad_config_reason_missing_attribute">Falta atributo</string>
|
||||
<string name="bad_config_reason_missing_section">Sección faltante</string>
|
||||
<string name="bad_config_reason_syntax_error">Error de sintaxis</string>
|
||||
<string name="bad_config_reason_unknown_attribute">Atributo desconocido</string>
|
||||
<string name="bad_config_reason_unknown_section">Sección desconocida</string>
|
||||
<string name="bad_config_reason_value_out_of_range">Valor fuera de rango</string>
|
||||
<string name="bad_extension_error">El archivo debe ser .conf o .zip</string>
|
||||
<string name="cancel">Cancelar</string>
|
||||
<string name="config_delete_error">No se puede eliminar el archivo de configuración %s</string>
|
||||
<string name="config_exists_error">La configuración para “%s” ya existe</string>
|
||||
<string name="config_file_exists_error">El archivo de configuración “%s” ya existe</string>
|
||||
<string name="config_not_found_error">Archivo de configuración “%s” no encontrado</string>
|
||||
<string name="config_rename_error">No se puede renombrar el archivo de configuración “%s”</string>
|
||||
<string name="config_save_error">No se puede guardar la configuración para “%1$s”: %2$s</string>
|
||||
<string name="config_save_success">Configuración guardada correctamente para “%s”</string>
|
||||
<string name="create_activity_title">Crear túnel WireGuard</string>
|
||||
<string name="create_bin_dir_error">No se puede crear el directorio binario local</string>
|
||||
<string name="create_downloads_file_error">No se puede crear archivo en el directorio de descargas</string>
|
||||
<string name="create_empty">Crear de cero</string>
|
||||
<string name="create_from_file">Importar desde archivo</string>
|
||||
<string name="create_from_qr_code">Escanear desde código QR</string>
|
||||
<string name="create_output_dir_error">No se puede crear el directorio de salida</string>
|
||||
<string name="create_temp_dir_error">No se puede crear la carpeta temporal</string>
|
||||
<string name="create_tunnel">Crear túnel</string>
|
||||
<string name="dark_theme_summary_off">Actualmente usando tema claro (día)</string>
|
||||
<string name="dark_theme_summary_on">Actualmente usando tema oscuro (noche)</string>
|
||||
<string name="dark_theme_title">Usar tema oscuro</string>
|
||||
<string name="delete">Eliminar</string>
|
||||
<string name="dns_servers">Servidores DNS</string>
|
||||
<string name="edit">Editar</string>
|
||||
<string name="endpoint">Punto final</string>
|
||||
<string name="error_down">Error al bajar el túnel: %s</string>
|
||||
<string name="error_fetching_apps">Error al obtener la lista de aplicaciones: %s</string>
|
||||
<string name="error_root">Por favor, obtén acceso root y vuelve a intentarlo</string>
|
||||
<string name="error_up">Error al abrir el túnel: %s</string>
|
||||
<string name="exclude_private_ips">Excluir direcciones privadas</string>
|
||||
<string name="generate_new_private_key">Generar nueva clave privada</string>
|
||||
<string name="generic_error">Error desconocido “%s”</string>
|
||||
<string name="hint_automatic">(auto)</string>
|
||||
<string name="hint_generated">(generado)</string>
|
||||
<string name="hint_optional">(opcional)</string>
|
||||
<string name="hint_optional_discouraged">(opcional, no recomendado)</string>
|
||||
<string name="hint_random">(aleatorio)</string>
|
||||
<string name="illegal_filename_error">Nombre de archivo no válido “%s”</string>
|
||||
<string name="import_error">No se puede importar túnel: %s</string>
|
||||
<string name="import_from_qr_code">Importar túnel desde código QR</string>
|
||||
<string name="import_success">Importado “%s”</string>
|
||||
<string name="interface_title">Interfaz</string>
|
||||
<string name="key_contents_error">Caracteres incorrectos en la clave</string>
|
||||
<string name="key_length_error">Longitud de clave incorrecta</string>
|
||||
<string name="key_length_explanation_base64">Las claves base64 de WireGuard deben tener 44 caracteres (32 bytes)</string>
|
||||
<string name="key_length_explanation_binary">: Las claves WireGuard deben tener 32 bytes</string>
|
||||
<string name="key_length_explanation_hex">: Las claves hexadecimales de Wirex deben tener 64 caracteres (32 bytes)</string>
|
||||
<string name="listen_port">Puerto de escucha</string>
|
||||
<string name="log_export_error">No se pudo exportar el registro: %s</string>
|
||||
<string name="log_export_subject">Archivo de registro WireGuard Android</string>
|
||||
<string name="log_export_success">Guardado en “%s”</string>
|
||||
<string name="log_export_title">Exportar archivo de registro</string>
|
||||
<string name="log_saver_activity_label">Guardar registro</string>
|
||||
<string name="log_viewer_pref_summary">Los registros pueden ayudar con la depuración</string>
|
||||
<string name="log_viewer_pref_title">Ver registro de aplicación</string>
|
||||
<string name="log_viewer_title">Registro</string>
|
||||
<string name="logcat_error">No se puede ejecutar logcat: </string>
|
||||
<string name="module_disabler_disabled_summary">El módulo experimental del kernel puede mejorar el rendimiento</string>
|
||||
<string name="module_disabler_disabled_title">Habilitar backend del módulo del kernel</string>
|
||||
<string name="module_disabler_enabled_summary">El backend más lento del espacio de usuario puede mejorar la estabilidad</string>
|
||||
<string name="module_disabler_enabled_title">Desactivar backend del módulo del kernel</string>
|
||||
<string name="module_installer_error">Ocurrió un error. Intente de nuevo</string>
|
||||
<string name="module_installer_initial">El módulo experimental del kernel puede mejorar el rendimiento</string>
|
||||
<string name="module_installer_not_found">No hay módulos disponibles para tu dispositivo</string>
|
||||
<string name="module_installer_title">Descargar e instalar el módulo del kernel</string>
|
||||
<string name="module_installer_working">Descargando e instalando…</string>
|
||||
<string name="module_version_error">No se puede determinar la versión del módulo del kernel</string>
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="multiple_tunnels_summary_off">Activar un túnel apagará los demás</string>
|
||||
<string name="multiple_tunnels_summary_on">Múltiples túneles pueden ser activados simultáneamente</string>
|
||||
<string name="multiple_tunnels_title">Permitir múltiples túneles simultáneos</string>
|
||||
<string name="name">Nombre</string>
|
||||
<string name="no_config_error">Intentando abrir un túnel sin configuración</string>
|
||||
<string name="no_configs_error">No se encontraron configuraciones</string>
|
||||
<string name="no_tunnels_error">No existen túneles</string>
|
||||
<string name="parse_error_generic">cadena</string>
|
||||
<string name="parse_error_inet_address">Dirección IP</string>
|
||||
<string name="parse_error_inet_endpoint">punto final</string>
|
||||
<string name="parse_error_inet_network">Red IP</string>
|
||||
<string name="parse_error_integer">número</string>
|
||||
<string name="parse_error_reason">No se puede analizar %1$s “%2$s”</string>
|
||||
<string name="peer">Pares</string>
|
||||
<string name="permission_description">controlar túneles de WireGuard, habilitando y desactivando túneles a su antojo, lo que podría conducir mal al tráfico de Internet</string>
|
||||
<string name="permission_label">controlar túneles de WireGuard</string>
|
||||
<string name="persistent_keepalive">Mantenimiento persistente</string>
|
||||
<string name="pre_shared_key">Clave precompartida</string>
|
||||
<string name="pre_shared_key_enabled">activado</string>
|
||||
<string name="private_key">Clave privada</string>
|
||||
<string name="public_key">Clave pública</string>
|
||||
<string name="qr_code_hint">Consejo: generar con `qrencode -t ansiutf8 < tunnel.conf`.</string>
|
||||
<string name="restore_on_boot_summary_off">No mostrará túneles habilitados al arrancar</string>
|
||||
<string name="restore_on_boot_summary_on">Mostrará túneles habilitados al arrancar</string>
|
||||
<string name="restore_on_boot_title">Restaurar al arrancar</string>
|
||||
<string name="save">Guardar</string>
|
||||
<string name="select_all">Seleccionar todo</string>
|
||||
<string name="settings">Preferencias</string>
|
||||
<string name="shell_exit_status_read_error">Shell no puede leer estado de salida</string>
|
||||
<string name="shell_marker_count_error">Shell esperaba 4 marcadores, recibió %d</string>
|
||||
<string name="shell_start_error">No se pudo iniciar Shell: %d</string>
|
||||
<string name="success_application_will_restart">Éxito. La aplicación se reiniciará ahora…</string>
|
||||
<string name="toggle_all">Cambiar Todos</string>
|
||||
<string name="toggle_error">Error al cambiar el túnel WireGuard: %s</string>
|
||||
<string name="tools_installer_already">wg y wg-quick ya están instalados</string>
|
||||
<string name="tools_installer_failure">No se puede instalar herramientas de línea de comandos (sin root?)</string>
|
||||
<string name="tools_installer_initial">Instalar herramientas opcionales para el scripting</string>
|
||||
<string name="tools_installer_initial_magisk">Instalar herramientas opcionales para el scripting como módulo Magisk</string>
|
||||
<string name="tools_installer_initial_system">Instalar herramientas opcionales para el scripting en la partición del sistema</string>
|
||||
<string name="tools_installer_success_magisk">wg y wg-quick instalados como un módulo Magisk (requiere reinicio)</string>
|
||||
<string name="tools_installer_success_system">wg y wg-quick instalados en la partición del sistema</string>
|
||||
<string name="tools_installer_title">Instalar herramientas de línea de comandos</string>
|
||||
<string name="tools_installer_working">Instalando wg y wg-quick</string>
|
||||
<string name="tools_unavailable_error">Herramientas requeridas no disponibles</string>
|
||||
<string name="transfer">Transferir</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">rx: %1$s, tx: %2$s</string>
|
||||
<string name="transfer_tibibytes">%.2f TiB</string>
|
||||
<string name="tun_create_error">No se puede crear el dispositivo túnel</string>
|
||||
<string name="tunnel_config_error">No se puede configurar el túnel (wg-quick devuelto %d)</string>
|
||||
<string name="tunnel_create_error">No se puede crear el dispositivo túnel: %s</string>
|
||||
<string name="tunnel_create_success">Túnel creado con éxito “%s”</string>
|
||||
<string name="tunnel_error_already_exists">Túnel “%s” ya existe</string>
|
||||
<string name="tunnel_error_invalid_name">Nombre inválido</string>
|
||||
<string name="tunnel_list_placeholder">Añadir un túnel usando el botón azul</string>
|
||||
<string name="tunnel_name">Nombre del túnel</string>
|
||||
<string name="tunnel_on_error">No se puede activar el túnel (wgTurnOn devolvió %d)</string>
|
||||
<string name="tunnel_rename_error">No se puede renombrar túnel: %s</string>
|
||||
<string name="tunnel_rename_success">Túnel renombrado con éxito a “%s”</string>
|
||||
<string name="type_name_go_userspace">Ir al espacio de usuario</string>
|
||||
<string name="type_name_kernel_module">Módulo Kernel</string>
|
||||
<string name="unknown_error">Error desconocido</string>
|
||||
<string name="version_summary">%1$s backend v%2$s</string>
|
||||
<string name="version_summary_checking">Comprobando versión de backend %s</string>
|
||||
<string name="version_summary_unknown">Versión %s desconocida</string>
|
||||
<string name="version_title">WireGuard para Android -%s</string>
|
||||
<string name="vpn_not_authorized_error">Servicio VPN no autorizado por el usuario</string>
|
||||
<string name="vpn_start_error">No se puede iniciar el servicio VPN Android</string>
|
||||
<string name="zip_export_error">No se pueden exportar túneles: %s</string>
|
||||
<string name="zip_export_success">Guardado en “%s”</string>
|
||||
<string name="zip_export_summary">El archivo Zip se guardará en la carpeta de descargas</string>
|
||||
<string name="zip_export_title">Exportar túneles a archivo zip</string>
|
||||
<string name="biometric_prompt_zip_exporter_title">Autenticar para exportar túneles</string>
|
||||
<string name="biometric_prompt_private_key_title">Autenticar para ver la clave privada</string>
|
||||
<string name="biometric_auth_error">Error de autenticación</string>
|
||||
<string name="biometric_auth_error_reason">Error de autenticación: %s</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,212 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="delete_error">
|
||||
<item quantity="one">حذف %d تونل امکانپذیر نیست: %s</item>
|
||||
<item quantity="other">حذف %d تونلها امکانپذیر نیست: %s</item>
|
||||
</plurals>
|
||||
<plurals name="delete_success">
|
||||
<item quantity="one">%d تونل با موقیت حذف شد</item>
|
||||
<item quantity="other">%d تونلها با موقیت حذف شدند</item>
|
||||
</plurals>
|
||||
<plurals name="delete_title">
|
||||
<item quantity="one">%d تونل انتخاب شد</item>
|
||||
<item quantity="other">%d تونلها انتخاب شدند</item>
|
||||
</plurals>
|
||||
<plurals name="import_partial_success">
|
||||
<item quantity="one">%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="other">%d از تونل اضافه شد</item>
|
||||
</plurals>
|
||||
<plurals name="set_excluded_applications">
|
||||
<item quantity="one">%d برنامه استثنا</item>
|
||||
<item quantity="other">%d برنامههای استثنا</item>
|
||||
</plurals>
|
||||
<plurals name="set_included_applications">
|
||||
<item quantity="one">%d برنامه مشمول</item>
|
||||
<item quantity="other">%d برنامههای مشمول</item>
|
||||
</plurals>
|
||||
<plurals name="n_excluded_applications">
|
||||
<item quantity="one">%d استثنا</item>
|
||||
<item quantity="other">%d استثناها</item>
|
||||
</plurals>
|
||||
<plurals name="n_included_applications">
|
||||
<item quantity="one">شامل %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="other">شامل %d برنامه</item>
|
||||
</plurals>
|
||||
<plurals name="exclude_n_applications">
|
||||
<item quantity="one">جداکردن %d برنامه</item>
|
||||
<item quantity="other">جداکردن %d برنامه</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_unit">
|
||||
<item quantity="one">هر ثانیه</item>
|
||||
<item quantity="other">هر %d ثانیه</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_suffix">
|
||||
<item quantity="one">ثانیه</item>
|
||||
<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>
|
||||
<string name="allowed_ips">IPهای مجاز</string>
|
||||
<string name="app_name">WireGuard</string>
|
||||
<string name="bad_config_context">%1$s\'s %2$s</string>
|
||||
<string name="bad_config_context_top_level">%s</string>
|
||||
<string name="bad_config_error">%1$s در %2$s</string>
|
||||
<string name="bad_config_explanation_pka">: باید مثبت و بیشتر از ۶۵۵۳۵ نباشد</string>
|
||||
<string name="bad_config_explanation_positive_number">: باید مثبت باشد</string>
|
||||
<string name="bad_config_explanation_udp_port">: باید یک شماره پورت UDP معتبر باشد</string>
|
||||
<string name="bad_config_reason_invalid_key">کلید نامعتبر است</string>
|
||||
<string name="bad_config_reason_invalid_number">شماره نامعتبر است</string>
|
||||
<string name="bad_config_reason_invalid_value">مقدار نامعتبر است</string>
|
||||
<string name="bad_config_reason_missing_attribute">مشخصه موجود نیست</string>
|
||||
<string name="bad_config_reason_missing_section">بخش موجود نیست</string>
|
||||
<string name="bad_config_reason_syntax_error">خطای نحوی</string>
|
||||
<string name="bad_config_reason_unknown_attribute">مشخصهٔ نامعلوم</string>
|
||||
<string name="bad_config_reason_unknown_section">بخش نامعلوم</string>
|
||||
<string name="bad_config_reason_value_out_of_range">مقدار خارج از محدوده</string>
|
||||
<string name="bad_extension_error">پرونده باید .conf یا .zip باشد</string>
|
||||
<string name="cancel">لغو</string>
|
||||
<string name="config_delete_error">نمیتوان پرونده پیکربندی %s را حذف کرد</string>
|
||||
<string name="config_exists_error">پیکربندی برای ”%s” در حال حاضر وجود دارد</string>
|
||||
<string name="config_file_exists_error">فایل پیکربندی ”%s” در حال حاضر وجود دارد</string>
|
||||
<string name="config_not_found_error">پرونده پیکربندی “%s” یافت نشد</string>
|
||||
<string name="config_rename_error">نمیتوان نام پرونده پیکربندی “%s” را تغییر داد</string>
|
||||
<string name="config_save_error">نمیتوان پیکربندی برای “%1$s”: %2$s را ذخیره کرد</string>
|
||||
<string name="config_save_success">پیکربندی برای “%s” با موفقیت ذخیره شد</string>
|
||||
<string name="create_activity_title">ساخت تونل WireGuard</string>
|
||||
<string name="create_bin_dir_error">نمیتوان دایرکتوری باینری محلی را ایجاد کرد</string>
|
||||
<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_output_dir_error">نمیتوان دایرکتوری خروجی را ایجاد کرد</string>
|
||||
<string name="create_temp_dir_error">نمیتوان دایرکتوری موقت محلی را ساخت</string>
|
||||
<string name="create_tunnel">ساختن تونل</string>
|
||||
<string name="dark_theme_summary_off">اکنون از پوسته روشن(روز) استفاده میشود</string>
|
||||
<string name="dark_theme_summary_on">اکنون از پوسته تاریک(شب) استفاده میشود</string>
|
||||
<string name="dark_theme_title">استفاده از پوسته تاریک</string>
|
||||
<string name="delete">حذف</string>
|
||||
<string name="dns_servers">سرورهای DNS</string>
|
||||
<string name="edit">ویرایش</string>
|
||||
<string name="endpoint">نقطه پایان</string>
|
||||
<string name="error_down">خطا هنگام بستن تونل: %s</string>
|
||||
<string name="error_fetching_apps">خطا هنگام واکشی فهرست برنامهها: %s</string>
|
||||
<string name="error_root">لطفا دسترسی روت را فراهمکرده و دوباره تلاش کنید</string>
|
||||
<string name="error_up">خطا هنگام راهاندازی تونل: %s</string>
|
||||
<string name="exclude_private_ips">مستثنی کردن IPهای خصوصی</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_optional_discouraged">(اختیاری، پیشنهاد نمیشود)</string>
|
||||
<string name="hint_random">(تصادفی)</string>
|
||||
<string name="illegal_filename_error">نام پرونده “%s” غیرمجاز است</string>
|
||||
<string name="import_error">نمیتوان تونل را وارد کرد: %s</string>
|
||||
<string name="import_from_qr_code">وارد کردن تونل از کد QR</string>
|
||||
<string name="import_success">“%s” وارد شد</string>
|
||||
<string name="interface_title">رابط</string>
|
||||
<string name="key_contents_error">در کلید نویسههای بد وجود دارد</string>
|
||||
<string name="key_length_error">طول کلید نادرست است</string>
|
||||
<string name="key_length_explanation_base64">: کلیدهای WireGuard base64 باید دارای ۴۴ نویسه باشند ( ۳۲ بایت)</string>
|
||||
<string name="key_length_explanation_binary">: کلیدهای WireGuard باید ۳۲ بایت باشند</string>
|
||||
<string name="key_length_explanation_hex">: کلیدهای هگز WireGuard باید دارای ۶۴ نویسه باشند ( ۳۲ بایت)</string>
|
||||
<string name="listen_port">شنود پورت</string>
|
||||
<string name="log_export_error">نمیتوان گزارش رویداد را برونبرد: %s</string>
|
||||
<string name="log_export_subject">پرونده گزارش رویداد WireGuard اندروید</string>
|
||||
<string name="log_export_success">ذخیره شد در “%s”</string>
|
||||
<string name="log_export_title">برونبرد پرونده گزارش رویداد</string>
|
||||
<string name="log_saver_activity_label">ذخیره گزارش رویداد</string>
|
||||
<string name="log_viewer_pref_summary">گزارش رویداد شاید به اشکال زدایی کمک کند</string>
|
||||
<string name="log_viewer_pref_title">نمایش گزارش رویداد برنامه</string>
|
||||
<string name="log_viewer_title">گزارش رویداد</string>
|
||||
<string name="logcat_error">نمیتوان logcat را اجرا کرد: </string>
|
||||
<string name="module_disabler_disabled_summary">ماژول آزمایشیِ کرنل می تواند کارایی را افزایش دهد</string>
|
||||
<string name="module_disabler_disabled_title">فعالسازی ماژول کرنل ِبک اند</string>
|
||||
<string name="module_disabler_enabled_title">غیرفعالسازی پسزمینه واحد هسته</string>
|
||||
<string name="module_installer_error">مشکلی پیش آمد. لطفا دوباره تلاش کنید</string>
|
||||
<string name="module_installer_not_found">هیچ واحدی برای دستگاه شما در دسترس نیست</string>
|
||||
<string name="module_installer_title">واحد هسته را بارگیری و نصب کن</string>
|
||||
<string name="module_installer_working">در حال بارگیری و نصب…</string>
|
||||
<string name="module_version_error">نمیتوان نگارش واحد هسته را مشخص کرد</string>
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="multiple_tunnels_summary_off">روشن کردن یک تونل ، تونل های دیگر را خاموش خواهد کرد</string>
|
||||
<string name="name">نام</string>
|
||||
<string name="no_config_error">تلاش برای فعالسازی تونل بدون تنظیمات</string>
|
||||
<string name="no_configs_error">هیچ پیکربندی یافت نشد</string>
|
||||
<string name="no_tunnels_error">هیچ تونلی وجود ندارد</string>
|
||||
<string name="parse_error_generic">رشته</string>
|
||||
<string name="parse_error_inet_address">نشانی IP</string>
|
||||
<string name="parse_error_inet_endpoint">نقطه پایان</string>
|
||||
<string name="parse_error_inet_network">شبکه IP</string>
|
||||
<string name="parse_error_integer">شماره</string>
|
||||
<string name="parse_error_reason">نمیتوان %1$s “%2$s” تجزیه کرد</string>
|
||||
<string name="peer">همتا</string>
|
||||
<string name="permission_description">کنترل تونل های وایرگارد، فعال و غیرفعال کردن تونل ها، و حتی تغییر مسیر ترافیک اینترنت</string>
|
||||
<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="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="settings">تنظیمات</string>
|
||||
<string name="shell_start_error">آغاز پوسته شکست خورد: %d</string>
|
||||
<string name="success_application_will_restart">موفقیت. برنامه اکنون دوباره راهاندازی خواهد شد…</string>
|
||||
<string name="toggle_all">معکوس کردن همه</string>
|
||||
<string name="tools_installer_title">ابزارهای خط فرمان را نصب کنید</string>
|
||||
<string name="tools_installer_working">در حال نصب wg و wg-quick</string>
|
||||
<string name="tools_unavailable_error">ابزارهای لازم در دسترس نیست</string>
|
||||
<string name="transfer">انتقال</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">rx: %1$s, tx: %2$s</string>
|
||||
<string name="transfer_tibibytes">%.2f TiB</string>
|
||||
<string name="tunnel_create_error">نمیتوان تونل را ساخت: %s</string>
|
||||
<string name="tunnel_create_success">تونل “%s” با موفقیت ساخته شد</string>
|
||||
<string name="tunnel_error_already_exists">تونل “%s” از قبل وجود دارد</string>
|
||||
<string name="tunnel_error_invalid_name">نام نامعتبر</string>
|
||||
<string name="tunnel_list_placeholder">بهوسیله دکمه آبی یک تونل بیفزایید</string>
|
||||
<string name="tunnel_name">نام تونل</string>
|
||||
<string name="tunnel_rename_error">ناتوان در تغییر نام تونل: %s</string>
|
||||
<string name="tunnel_rename_success">نام تونل با موفقیت تغییر یافت به “%s”</string>
|
||||
<string name="type_name_go_userspace">رفتن به فضای کاربر</string>
|
||||
<string name="type_name_kernel_module">واحد هسته</string>
|
||||
<string name="unknown_error">خطای نامشخص</string>
|
||||
<string name="version_summary">%1$s پسزمینه نگارش%2$s</string>
|
||||
<string name="version_summary_checking">در حال بررسی نگارش پسزمینه %s</string>
|
||||
<string name="version_summary_unknown">نگارش %s ناشناخته</string>
|
||||
<string name="version_title">WireGuard برای اندروید نگارش %s</string>
|
||||
<string name="vpn_not_authorized_error">کاربر به سرویس VPN اجازه نداد</string>
|
||||
<string name="vpn_start_error">نمیتوان سرویس VPN اندروید را آغاز کرد</string>
|
||||
<string name="zip_export_error">نمیتوان تونلها را برونبرد: %s</string>
|
||||
<string name="zip_export_success">ذخیره شد در “%s”</string>
|
||||
<string name="zip_export_summary">پرونده زیپ در پوشه بارگیریها ذخیره خواهد شد</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>
|
||||
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="persistent_keepalive_seconds_unit">
|
||||
<item quantity="one">joka sekunti</item>
|
||||
<item quantity="other">%d sekunnin välein</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_suffix">
|
||||
<item quantity="one">sekunti</item>
|
||||
<item quantity="other">sekuntia</item>
|
||||
</plurals>
|
||||
<string name="use_all_applications">Käytä kaikkia sovelluksia</string>
|
||||
<string name="add_peer">Lisää toinen osapuoli</string>
|
||||
<string name="addresses">Osoitteet</string>
|
||||
<string name="applications">Sovellukset</string>
|
||||
<string name="allowed_ips">Sallitut IP-osoitteet</string>
|
||||
<string name="app_name">WireGuard</string>
|
||||
<string name="bad_config_context_top_level">%s</string>
|
||||
<string name="bad_config_reason_invalid_key">Virheellinen avain</string>
|
||||
<string name="bad_config_reason_invalid_number">Virheellinen luku</string>
|
||||
<string name="bad_config_reason_invalid_value">Virheellinen arvo</string>
|
||||
<string name="bad_config_reason_missing_attribute">Attribuutti puuttuu</string>
|
||||
<string name="bad_config_reason_missing_section">Osio puuttuu</string>
|
||||
<string name="bad_config_reason_syntax_error">Syntaksivirhe</string>
|
||||
<string name="bad_config_reason_unknown_attribute">Tuntematon määrite</string>
|
||||
<string name="bad_config_reason_unknown_section">Tuntematon osio</string>
|
||||
<string name="bad_config_reason_value_out_of_range">Arvo alueen ulkopuolella</string>
|
||||
<string name="bad_extension_error">Tiedoston on oltava .conf tai .zip</string>
|
||||
<string name="cancel">Peruuta</string>
|
||||
<string name="config_not_found_error">Asetustiedostoa “%s” ei löydy</string>
|
||||
<string name="config_rename_error">Asetustiedostoa \"%s\" ei voi nimetä uudelleen</string>
|
||||
<string name="config_save_success">Asetustiedosto \"%s\" tallennettu onnistuneesti</string>
|
||||
<string name="create_activity_title">Luo WireGuard Tunnel</string>
|
||||
<string name="exclude_private_ips">Jätä pois yksityiset IP-osoitteet</string>
|
||||
<string name="generate_new_private_key">Luo uusi yksityinen avain</string>
|
||||
<string name="generic_error">Tuntematon ”%s” virhe</string>
|
||||
<string name="hint_automatic">(oletus)</string>
|
||||
<string name="hint_generated">(generoitu)</string>
|
||||
<string name="hint_optional">(valinnainen)</string>
|
||||
<string name="hint_optional_discouraged">(valinnainen, ei suositeltava)</string>
|
||||
<string name="hint_random">(satunnainen)</string>
|
||||
<string name="illegal_filename_error">Virheellinen tiedostonimi “%s”</string>
|
||||
<string name="import_error">Tunnelia \"%s\" ei voitu tuoda</string>
|
||||
<string name="import_from_qr_code">Tuo tunneli QR-koodista</string>
|
||||
<string name="import_success">Tuotu ”%s”</string>
|
||||
<string name="key_contents_error">Virheellinen merkki avaimessa</string>
|
||||
<string name="key_length_error">Virheellinen avaimen pituus</string>
|
||||
<string name="key_length_explanation_base64">: WireGuardin base64-avainten pituus on oltava 44 merkkiä (32 tavua)</string>
|
||||
<string name="key_length_explanation_binary">: WireGuard-avainten on oltava 32 tavua</string>
|
||||
<string name="key_length_explanation_hex">: WireGuardin base64-avainten pituus on oltava 64 merkkiä (32 tavua)</string>
|
||||
<string name="listen_port">Kuuntele porttia</string>
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="no_configs_error">Asetuksia ei löydy</string>
|
||||
<string name="no_tunnels_error">Tunneleita ei ole</string>
|
||||
<string name="parse_error_generic">merkkijono</string>
|
||||
<string name="parse_error_inet_address">IP-osoite</string>
|
||||
<string name="parse_error_inet_network">IP-verkko</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">rx: %1$s, tx: %2$s</string>
|
||||
<string name="transfer_tibibytes">%.2f TiB</string>
|
||||
<string name="tunnel_name">Tunnelin nimi</string>
|
||||
</resources>
|
||||
@@ -5,7 +5,7 @@
|
||||
<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="one">Suppression réussie du tunnel %d</item>
|
||||
<item quantity="other">Supprimé avec succès %d tunnels</item>
|
||||
</plurals>
|
||||
<plurals name="delete_title">
|
||||
@@ -191,7 +191,7 @@
|
||||
<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">Données transférées</string>
|
||||
<string name="transfer_bytes">%d Octets</string>
|
||||
<string name="transfer_gibibytes">%.2f Go</string>
|
||||
<string name="transfer_kibibytes">%.2f Ko</string>
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="delete_error">
|
||||
<item quantity="one">%d टनल हटाने में असमर्थ: %s</item>
|
||||
<item quantity="other">%d टनलस को हटाने में असमर्थ: %s</item>
|
||||
</plurals>
|
||||
<plurals name="delete_success">
|
||||
<item quantity="one">%d टनल को सफलतापूर्वक हटा दिया गया</item>
|
||||
<item quantity="other">%d टनलस को सफलतापूर्वक हटा दिया गया</item>
|
||||
</plurals>
|
||||
<plurals name="delete_title">
|
||||
<item quantity="one">%d टनल चयनित</item>
|
||||
<item quantity="other">%d टनलस का चयन किया गया</item>
|
||||
</plurals>
|
||||
<plurals name="import_partial_success">
|
||||
<item quantity="one">आयातित %d %d टनल</item>
|
||||
<item quantity="other">आयातित %d %d टनलस</item>
|
||||
</plurals>
|
||||
<plurals name="import_total_success">
|
||||
<item quantity="one">आयातित %d टनल</item>
|
||||
<item quantity="other">आयातित %d टनलस</item>
|
||||
</plurals>
|
||||
<plurals name="set_excluded_applications">
|
||||
<item quantity="one">%d बहिष्कृत अनुप्रयोग</item>
|
||||
<item quantity="other">%d बहिष्कृत अनुप्रयोग</item>
|
||||
</plurals>
|
||||
<plurals name="set_included_applications">
|
||||
<item quantity="one">%d ऐप्स शामिल</item>
|
||||
<item quantity="other">%d ऐप्स शामिल किये गए</item>
|
||||
</plurals>
|
||||
<plurals name="n_excluded_applications">
|
||||
<item quantity="one">%d अपवर्जित</item>
|
||||
<item quantity="other">%d अपवर्जित</item>
|
||||
</plurals>
|
||||
<plurals name="n_included_applications">
|
||||
<item quantity="one">%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="other">%d ऐप्स शामिल करें</item>
|
||||
</plurals>
|
||||
<plurals name="exclude_n_applications">
|
||||
<item quantity="one">%d ऐप को बाहर करें</item>
|
||||
<item quantity="other">%d ऐप्स को बाहर करें</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_unit">
|
||||
<item quantity="one">हर सेकंड</item>
|
||||
<item quantity="other">हर %d सेकंड्स</item>
|
||||
</plurals>
|
||||
<plurals name="persistent_keepalive_seconds_suffix">
|
||||
<item quantity="one">सेकंड</item>
|
||||
<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>
|
||||
<string name="allowed_ips">अनुमत आईपी</string>
|
||||
<string name="app_name">WireGuard</string>
|
||||
<string name="bad_config_explanation_pka">: सकारात्मक होना चाहिए और 65535 से अधिक नहीं होना चाहिए</string>
|
||||
<string name="bad_config_explanation_positive_number">: सकारात्मक होना चाहिए</string>
|
||||
<string name="bad_config_explanation_udp_port">: एक वैध यूडीपी पोर्ट नंबर होना चाहिए</string>
|
||||
<string name="bad_config_reason_invalid_key">अमान्य चाबी</string>
|
||||
<string name="bad_config_reason_invalid_number">अमान्य संख्या</string>
|
||||
<string name="bad_config_reason_invalid_value">अमान्य मूल्य</string>
|
||||
<string name="bad_config_reason_missing_attribute">गुम विशेषता</string>
|
||||
<string name="bad_config_reason_missing_section">छूटा हुआ भाग</string>
|
||||
<string name="bad_config_reason_syntax_error">वक्य रचना त्रुटि</string>
|
||||
<string name="bad_config_reason_unknown_attribute">अज्ञात एट्रिब्यूट</string>
|
||||
<string name="bad_config_reason_unknown_section">अज्ञात एट्रिब्यूट </string>
|
||||
<string name="bad_config_reason_value_out_of_range">मूल्य सीमा से बाहर</string>
|
||||
<string name="bad_extension_error">फ़ाइल .conf या .zip होनी चाहिए</string>
|
||||
<string name="cancel">रद्द</string>
|
||||
<string name="config_delete_error">कॉन्फ़िगरेशन फ़ाइल %s को नहीं हटा सकता</string>
|
||||
<string name="config_exists_error">“%s” के लिए कॉन्फ़िगरेशन पहले से मौजूद है</string>
|
||||
<string name="config_file_exists_error">कॉन्फ़िगरेशन फ़ाइल “%s” पहले से मौजूद है</string>
|
||||
<string name="config_not_found_error">कॉन्फ़िगरेशन फ़ाइल “%s” नहीं मिली</string>
|
||||
<string name="config_rename_error">कॉन्फ़िगरेशन फ़ाइल “%s” का नाम नहीं बदल सकता</string>
|
||||
<string name="config_save_error">“%1$s” के लिए कॉन्फ़िगरेशन को नहीं बचा सकता: %2$s</string>
|
||||
<string name="config_save_success">“%s” के लिए सफलतापूर्वक सहेजा गया कॉन्फ़िगरेशन</string>
|
||||
<string name="create_activity_title">वायरगार्ड टनल बनाएं</string>
|
||||
<string name="create_bin_dir_error">स्थानीय बाइनरी निर्देशिका नहीं बना सकते</string>
|
||||
<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_output_dir_error">आउटपुट निर्देशिका नहीं बना सकता</string>
|
||||
<string name="create_temp_dir_error">स्थानीय अस्थायी निर्देशिका नहीं बना सकते</string>
|
||||
<string name="create_tunnel">टनल बनाए</string>
|
||||
<string name="dark_theme_summary_off">अभी प्रकाश (दिन) थीम का उपयोग कर रहे हैं</string>
|
||||
<string name="dark_theme_summary_on">अभी डार्क (रात) थीम का उपयोग कर रहे हैं</string>
|
||||
<string name="dark_theme_title">डार्क थीम का इस्तेमाल करें</string>
|
||||
<string name="delete">हटाएं</string>
|
||||
<string name="dns_servers">डीएनएस सर्वर</string>
|
||||
<string name="edit">संपादित करें</string>
|
||||
<string name="endpoint">अंतिम</string>
|
||||
<string name="error_down">टनल को लाने में त्रुटि: %s</string>
|
||||
<string name="error_fetching_apps">ऐप्स सूची लाने में त्रुटि: %s</string>
|
||||
<string name="error_root">कृपया रूट एक्सेस प्राप्त करें और पुनः प्रयास करें</string>
|
||||
<string name="error_up">टनल को लाने में त्रुटि: %s</string>
|
||||
<string name="exclude_private_ips">निजी आईपी को छोड़ दें</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_optional_discouraged">(वैकल्पिक, अनुशंसित नहीं)</string>
|
||||
<string name="hint_random">(क्रमरहित)</string>
|
||||
<string name="illegal_filename_error">अवैध फ़ाइल नाम “%s”</string>
|
||||
<string name="import_error">टनल को आयात करने में असमर्थ: %s</string>
|
||||
<string name="import_from_qr_code">क्यूआर कोड से टनल को आयात करें</string>
|
||||
<string name="import_success">आयातित “%s”</string>
|
||||
<string name="interface_title">इंटरफेस</string>
|
||||
<string name="key_contents_error">चाबी में खराब वर्ण</string>
|
||||
<string name="key_length_error">चाबी की लम्बाई गलत </string>
|
||||
<string name="key_length_explanation_base64">: वायरगार्ड बेस 64 कीज़ में 44 अक्षर (32 बाइट्स) होने चाहिए</string>
|
||||
<string name="key_length_explanation_binary">: वायरगार्ड कीज 32 बाइट होनी चाहिए</string>
|
||||
<string name="key_length_explanation_hex">: वायरगार्ड हेक्स कीज़ 64 अक्षरों की होनी चाहिए (32 बाइट्स)</string>
|
||||
<string name="listen_port">पोर्ट सूने</string>
|
||||
<string name="log_export_error">लॉग निर्यात करने में असमर्थ: %s</string>
|
||||
<string name="log_export_subject">WireGuard एंड्राइड लॉग फ़ाइल</string>
|
||||
<string name="log_export_success">“%s” में सहेजा गया</string>
|
||||
<string name="log_export_title">लॉग फ़ाइल निर्यात करें</string>
|
||||
<string name="log_saver_activity_label">लॉग सहेजे</string>
|
||||
<string name="log_viewer_pref_summary">लॉग डीबगिंग में सहायता कर सकते हैं</string>
|
||||
<string name="log_viewer_pref_title">एप्लिकेशन लॉग देखें</string>
|
||||
<string name="log_viewer_title">लॉग</string>
|
||||
<string name="logcat_error">लॉगकैट चलाने में असमर्थ: </string>
|
||||
<string name="module_disabler_disabled_summary">प्रयोगात्मक कर्नेल मॉड्यूल प्रदर्शन में सुधार कर सकता है</string>
|
||||
<string name="module_disabler_disabled_title">कर्नेल मॉड्यूल बैकएंड सक्षम करें</string>
|
||||
<string name="module_disabler_enabled_summary">धीमे यूजरस्पेस बैकएंड में स्थिरता में सुधार हो सकता है</string>
|
||||
<string name="module_disabler_enabled_title">कर्नेल मॉड्यूल बैकएंड को अक्षम करें</string>
|
||||
<string name="module_installer_error">कुछ गलत हो गया। कृपया पुन: प्रयास करें</string>
|
||||
<string name="module_installer_initial">प्रयोगात्मक कर्नेल मॉड्यूल प्रदर्शन में सुधार कर सकता है</string>
|
||||
<string name="module_installer_not_found">आपके डिवाइस के लिए कोई मॉड्यूल उपलब्ध नहीं हैं</string>
|
||||
<string name="module_installer_title">कर्नेल मॉड्यूल डाउनलोड और इंस्टॉल करें</string>
|
||||
<string name="module_installer_working">डाउनलोड कर रहा है और स्थापित कर रहा है…</string>
|
||||
<string name="module_version_error">कर्नेल मॉड्यूल संस्करण निर्धारित करने में असमर्थ</string>
|
||||
<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="name">नाम</string>
|
||||
<string name="no_config_error">बिना किसी कॉन्फ़िगरेशन के एक टनल को लाने की कोशिश करना</string>
|
||||
<string name="no_configs_error">कोई कॉन्फ़िगरेशन नहीं मिला</string>
|
||||
<string name="no_tunnels_error">कोई टनल मौजूद नहीं है</string>
|
||||
<string name="parse_error_generic">पाठ</string>
|
||||
<string name="parse_error_inet_address">आईपी पता</string>
|
||||
<string name="parse_error_inet_endpoint">समाप्त</string>
|
||||
<string name="parse_error_inet_network">आईपी नेटवर्क</string>
|
||||
<string name="parse_error_integer">संख्या</string>
|
||||
<string name="parse_error_reason">%1$s “%2$s” को पार्स नहीं कर सकता</string>
|
||||
<string name="peer">पीयर</string>
|
||||
<string name="permission_description">वायरगार्ड टनल्स को नियंत्रित करना, टनल्स को सक्षम और अक्षम करना, संभवतः इंटरनेट ट्रैफ़िक को गलत तरीके से अक्षम करना है</string>
|
||||
<string name="permission_label">वायरगार्ड टनलस को नियंत्रित करें</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="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="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_error">वायरगार्ड टनल टॉगल करने में त्रुटि: %s</string>
|
||||
<string name="tools_installer_already">wg और wg-quick पहले से इंस्टॉल हैं</string>
|
||||
<string name="tools_installer_failure">कमांड-लाइन टूल स्थापित करने में असमर्थ (कोई रूट नहीं)</string>
|
||||
<string name="tools_installer_initial">स्क्रिप्टिंग के लिए वैकल्पिक उपकरण स्थापित करें</string>
|
||||
<string name="tools_installer_initial_magisk">Magisk मॉड्यूल के रूप में स्क्रिप्टिंग के लिए वैकल्पिक उपकरण स्थापित करें</string>
|
||||
<string name="tools_installer_initial_system">सिस्टम विभाजन में स्क्रिप्टिंग के लिए वैकल्पिक उपकरण स्थापित करें</string>
|
||||
<string name="tools_installer_success_magisk">wg और wg-quick को मैजिक मॉड्यूल के रूप में स्थापित किया गया है (रिबूट आवश्यक)</string>
|
||||
<string name="tools_installer_success_system">wg और wg-quick सिस्टम विभाजन में स्थापित है</string>
|
||||
<string name="tools_installer_title">कमांड लाइन उपकरण स्थापित करें</string>
|
||||
<string name="tools_installer_working">Wg और wg-quick इंस्टॉल करना</string>
|
||||
<string name="tools_unavailable_error">आवश्यक उपकरण अनुपलब्ध हैं</string>
|
||||
<string name="transfer">स्थानांतरण</string>
|
||||
<string name="tun_create_error">ट्यून डिवाइस बनाने में असमर्थ</string>
|
||||
<string name="tunnel_config_error">टनल को कॉन्फ़िगर करने में असमर्थ (wg-quick लौटा %d)</string>
|
||||
<string name="tunnel_create_error">टनल बनाने में असमर्थ: %s</string>
|
||||
<string name="tunnel_create_success">सफलतापूर्वक बनाया गया टनल “%s”</string>
|
||||
<string name="tunnel_error_already_exists">टनल “%s” पहले से मौजूद है</string>
|
||||
<string name="tunnel_error_invalid_name">गलत नाम</string>
|
||||
<string name="tunnel_list_placeholder">नीले बटन का उपयोग करके एक टनल को जोड़ें</string>
|
||||
<string name="tunnel_name">टनल का नाम</string>
|
||||
<string name="tunnel_on_error">टनल चालू करने में असमर्थ (wgTurnOn लौटा %d)</string>
|
||||
<string name="tunnel_rename_error">टनल का नाम बदलने में असमर्थ: %s</string>
|
||||
<string name="tunnel_rename_success">सफलतापूर्वक टनल का नाम बदलकर “%s” कर दिया गया</string>
|
||||
<string name="type_name_go_userspace">userspace पे जाए </string>
|
||||
<string name="type_name_kernel_module">कर्नेल मॉड्यूल</string>
|
||||
<string name="unknown_error">अज्ञात त्रुटि</string>
|
||||
<string name="version_summary">%1$s बैकएंड v%2$s</string>
|
||||
<string name="version_summary_checking">%s बैकएंड संस्करण की जाँच कर रहा है</string>
|
||||
<string name="version_summary_unknown">अज्ञात %s संस्करण</string>
|
||||
<string name="version_title">WireGuard for Android v%s</string>
|
||||
<string name="vpn_not_authorized_error">वीपीएन सेवा उपयोगकर्ता द्वारा अधिकृत नहीं है</string>
|
||||
<string name="vpn_start_error">एंड्रॉयड वीपीएन सेवा प्रारंभ करने में असमर्थ</string>
|
||||
<string name="zip_export_error">टनल का निर्यात करने में असमर्थ: %s</string>
|
||||
<string name="zip_export_success">“%s” पर सहेजा गया</string>
|
||||
<string name="zip_export_summary">ज़िप फ़ाइल को डाउनलोड फ़ोल्डर में सहेजा जाएगा</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>
|
||||
@@ -105,7 +105,7 @@
|
||||
<string name="hint_random">(ランダム)</string>
|
||||
<string name="illegal_filename_error">不正なファイル名 “%s”</string>
|
||||
<string name="import_error">トンネル設定をインポートできません: %s</string>
|
||||
<string name="import_from_qr_code">QR コードからトンネル設定をインポートできません</string>
|
||||
<string name="import_from_qr_code">QR コードからトンネル設定をインポートします</string>
|
||||
<string name="import_success">“%s” をインポートしました</string>
|
||||
<string name="interface_title">インターフェース</string>
|
||||
<string name="key_contents_error">鍵に不正な文字があります</string>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user