Compare commits

...

30 Commits

Author SHA1 Message Date
Jason A. Donenfeld 8671acc1a6 version: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-05-10 20:55:56 +02:00
Jason A. Donenfeld fb08bb6bd4 strings: sync with crowdin
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-05-10 20:54:29 +02:00
Jason A. Donenfeld 70f8e5ab3a ui: add shortcut for adding quick settings tile
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-05-10 20:48:11 +02:00
Jason A. Donenfeld e53a8d76f9 gradle: hack around 8.1.0-beta02 api 21 outlining bug
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-05-10 19:56:08 +02:00
Jason A. Donenfeld e0b4551bca gradle: generate locales for per-app picking
Enable the per-app locale picking menu by adding the right metadata
automatically to the manifest with new AGP automation.

This attempts to follow:
https://developer.android.com/guide/topics/resources/app-languages#use-localeconfig

Using the new AGP feature:
https://developer.android.com/studio/preview/features#automatic-per-app-languages

It appears to work, and it also should squelch the "W AppCompatDelegate:
Checking for metadata for AppLocalesMetadataHolderService : " warnings
on old Android that syphyr pointed out.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-05-10 19:56:04 +02:00
Jason A. Donenfeld 7b9757bf3b gradle: bump to agp 8.1.0-beta02
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-05-10 19:56:00 +02:00
Jason A. Donenfeld 4156b83b84 ui: bifurcate only parts of release but nag about wrong context
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-05-10 19:10:53 +02:00
Jason A. Donenfeld f829b8abe0 gradle: exclude version meta files in release build
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-05-10 16:54:12 +02:00
Harsh Shandilya 838e0bd6b0 gradle: bump coroutines and MDC
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2023-05-10 18:47:16 +05:30
Jason A. Donenfeld c3f6906309 version: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-05-08 16:10:40 +02:00
Jason A. Donenfeld 5cafd1ebe4 ui: use debug key in debug builds
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-05-08 14:00:09 +02:00
Jason A. Donenfeld 51991324a9 ui: only register contracts in constructors
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-05-08 13:31:33 +02:00
Jason A. Donenfeld 33a8a51962 gradle: restore play runtime detection
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-05-08 13:08:09 +02:00
Jason A. Donenfeld 40eaa54cf0 ui: reformat all code
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-05-05 13:18:30 +02:00
Jason A. Donenfeld a3bfa6f1ab ui: collect logs from IO thread only
Otherwise the pop() from the producer might causes an OOB read in the
consumer:

Exception java.lang.ArrayIndexOutOfBoundsException:
  at androidx.collection.CircularArray.get (CircularArray.java)
  at com.wireguard.android.activity.LogViewerActivity.rawLogBytes (LogViewerActivity.java)
  at com.wireguard.android.activity.LogViewerActivity.onCreate$lambda$3 (LogViewerActivity.java:133)
  at android.view.View.performClick (View.java:6935)
  at android.view.View$PerformClick.run (View.java:26214)
  at android.os.Handler.handleCallback (Handler.java:790)
  at android.os.Handler.dispatchMessage (Handler.java:99)
  at android.os.Looper.loop (Looper.java:164)
  at android.app.ActivityThread.main (ActivityThread.java:7000)
  at java.lang.reflect.Method.invoke
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:441)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1408)

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-05-05 02:56:51 +02:00
Jason A. Donenfeld 7778ff63b5 gradle: use matching fallback instead of build type
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-05-05 02:43:36 +02:00
Jason A. Donenfeld 79c5eb41c3 version: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-05-04 16:48:23 +02:00
Jason A. Donenfeld 9e30c7d2e9 strings: sync with crowdin
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-05-03 15:41:41 +02:00
Jason A. Donenfeld d80129ba46 gradle: use += when possible
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-05-03 15:37:06 +02:00
Jason A. Donenfeld 6907663f07 gradle: don't encourage sticking credentials in files
We've only ever used this via environment variables, so keep it that
way.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-05-03 14:19:19 +02:00
Jason A. Donenfeld dca96a52b4 gradle: reabstract package name
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-05-03 14:09:11 +02:00
Jason A. Donenfeld cf4fca8086 gradle: remove runtime detection of play store
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-05-04 16:45:01 +02:00
Jason A. Donenfeld 318f36a205 README: account for java version updates
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-05-03 14:23:54 +02:00
Jason A. Donenfeld 87aa73cc64 gradle: use newer desugaring
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-05-03 14:23:38 +02:00
Jason A. Donenfeld 4e28d57aee gradle: use Java 17
It appears to work, even on API 21.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-05-03 14:01:05 +02:00
Harsh Shandilya 6d67a3d5f5 gradle: upgrade Kotlin and AGP
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2023-05-03 00:52:52 +05:30
Harsh Shandilya 1b5f4d6dfd gradle: move JVM target up to 11
1.8 is being deprecated across the board

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2023-05-03 00:49:47 +05:30
Harsh Shandilya bed2f2e5d6 gradle: convert build files to Kotlin
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2023-05-03 00:43:18 +05:30
Harsh Shandilya 7d91666860 gradle: update wrapper to 8.1.1
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2023-04-24 11:22:21 +05:30
Jason A. Donenfeld 8800f861ed ui: update cleanups
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2023-05-03 14:28:40 +02:00
74 changed files with 1007 additions and 716 deletions
+1
View File
@@ -468,6 +468,7 @@
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="RIGHT_MARGIN" value="160" />
</codeStyleSettings>
</code_scheme>
</component>
+3 -3
View File
@@ -26,12 +26,12 @@ The library makes use of Java 8 features, so be sure to support those in your gr
```
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
coreLibraryDesugaringEnabled = true
}
dependencies {
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.1.5"
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.3"
}
```
-33
View File
@@ -1,33 +0,0 @@
buildscript {
ext {
activityVersion = '1.7.1'
annotationsVersion = '1.6.0'
appcompatVersion = '1.6.1'
biometricVersion = '1.1.0'
collectionVersion = '1.2.0'
constraintLayoutVersion = '2.1.4'
coordinatorLayoutVersion = '1.2.0'
coreKtxVersion = '1.10.0'
coroutinesVersion = '1.6.4'
datastoreVersion = '1.0.0'
desugarVersion = '1.1.5'
fragmentVersion = '1.5.7'
jsr305Version = '3.0.2'
junitVersion = '4.13.2'
lifecycleRuntimeKtxVersion = '2.6.1'
materialComponentsVersion = '1.8.0'
preferenceVersion = '1.2.0'
zxingEmbeddedVersion = '4.3.0'
groupName = 'com.wireguard.android'
}
}
tasks {
wrapper {
gradleVersion = "8.0.2"
distributionSha256Sum = "ff7bf6a86f09b9b2c40bb8f48b25fc19cf2b2664fd1d220cd7ab833ec758d0d7"
}
}
apply from: "version.gradle"
+13
View File
@@ -0,0 +1,13 @@
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.kapt) apply false
}
tasks {
wrapper {
gradleVersion = "8.1.1"
distributionSha256Sum = "e111cb9948407e26351227dabce49822fb88c37ee72f1d1582a69c68af2e702f"
}
}
+4
View File
@@ -1,3 +1,7 @@
wireguardVersionCode=504
wireguardVersionName=1.0.20230510
wireguardPackageName=com.wireguard.android
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+29
View File
@@ -0,0 +1,29 @@
[versions]
agp = "8.1.0-beta02"
kotlin = "1.8.21"
[libraries]
androidx-activity-ktx = "androidx.activity:activity-ktx:1.7.1"
androidx-annotation = "androidx.annotation:annotation:1.6.0"
androidx-appcompat = "androidx.appcompat:appcompat:1.6.1"
androidx-biometric = "androidx.biometric:biometric:1.1.0"
androidx-collection = "androidx.collection:collection:1.2.0"
androidx-constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4"
androidx-coordinatorlayout = "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
androidx-core-ktx = "androidx.core:core-ktx:1.10.0"
androidx-datastore-preferences = "androidx.datastore:datastore-preferences:1.0.0"
androidx-fragment-ktx = "androidx.fragment:fragment-ktx:1.5.7"
androidx-lifecycle-runtime-ktx = "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
androidx-preference-ktx = "androidx.preference:preference-ktx:1.2.0"
desugarJdkLibs = "com.android.tools:desugar_jdk_libs:2.0.3"
google-material = "com.google.android.material:material:1.9.0"
jsr305 = "com.google.code.findbugs:jsr305:3.0.2"
junit = "junit:junit:4.13.2"
kotlinx-coroutines-android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0"
zxing-android-embedded = "com.journeyapps:zxing-android-embedded:4.3.0"
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
Binary file not shown.
+2 -2
View File
@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=ff7bf6a86f09b9b2c40bb8f48b25fc19cf2b2664fd1d220cd7ab833ec758d0d7
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip
distributionSha256Sum=e111cb9948407e26351227dabce49822fb88c37ee72f1d1582a69c68af2e702f
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+4 -3
View File
@@ -85,9 +85,6 @@ done
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -197,6 +194,10 @@ if "$cygwin" || "$msys" ; then
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
-28
View File
@@ -1,28 +0,0 @@
pluginManagement {
def agpVersion = '8.0.0'
def kotlinVersion = "1.8.0"
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
plugins {
id("com.android.application") version "$agpVersion"
id("com.android.library") version "$agpVersion"
id("org.jetbrains.kotlin.android") version "$kotlinVersion"
id("org.jetbrains.kotlin.kapt") version "$kotlinVersion"
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "wireguard-android"
include ':tunnel'
include ':ui'
+22
View File
@@ -0,0 +1,22 @@
@file:Suppress("UnstableApiUsage")
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "wireguard-android"
include(":tunnel")
include(":ui")
-72
View File
@@ -1,72 +0,0 @@
plugins {
id 'com.android.library'
}
version wireguardVersionName
group groupName
android {
compileSdk 33
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
namespace 'com.wireguard.android.tunnel'
defaultConfig {
minSdkVersion 21
targetSdkVersion 33
versionCode wireguardVersionCode
versionName wireguardVersionName
}
externalNativeBuild {
cmake {
path 'tools/CMakeLists.txt'
}
}
testOptions.unitTests.all {
testLogging {
events 'passed', 'skipped', 'failed', 'standardOut', 'standardError'
}
}
buildTypes {
all {
externalNativeBuild {
cmake {
targets 'libwg-go.so', 'libwg.so', 'libwg-quick.so'
arguments "-DGRADLE_USER_HOME=${project.gradle.gradleUserHomeDir}"
}
}
}
release {
externalNativeBuild {
cmake {
arguments "-DANDROID_PACKAGE_NAME=${groupName}"
}
}
}
debug {
externalNativeBuild {
cmake {
arguments "-DANDROID_PACKAGE_NAME=${groupName}.debug"
}
}
}
}
lint {
disable 'LongLogTag', 'NewApi'
}
publishing {
multipleVariants("release") {
allVariants()
}
}
}
dependencies {
implementation "androidx.annotation:annotation:$annotationsVersion"
implementation "androidx.collection:collection:$collectionVersion"
compileOnly "com.google.code.findbugs:jsr305:$jsr305Version"
testImplementation "junit:junit:$junitVersion"
}
apply from: "publish.gradle"
+128
View File
@@ -0,0 +1,128 @@
@file:Suppress("UnstableApiUsage")
import org.gradle.api.tasks.testing.logging.TestLogEvent
val pkg: String = providers.gradleProperty("wireguardPackageName").get()
plugins {
alias(libs.plugins.android.library)
`maven-publish`
signing
}
android {
compileSdk = 33
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
namespace = "${pkg}.tunnel"
defaultConfig {
minSdk = 21
}
externalNativeBuild {
cmake {
path("tools/CMakeLists.txt")
}
}
testOptions.unitTests.all {
it.testLogging { events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) }
}
buildTypes {
all {
externalNativeBuild {
cmake {
targets("libwg-go.so", "libwg.so", "libwg-quick.so")
arguments("-DGRADLE_USER_HOME=${project.gradle.gradleUserHomeDir}")
}
}
}
release {
externalNativeBuild {
cmake {
arguments("-DANDROID_PACKAGE_NAME=${pkg}")
}
}
}
debug {
externalNativeBuild {
cmake {
arguments("-DANDROID_PACKAGE_NAME=${pkg}.debug")
}
}
}
}
lint {
disable += "LongLogTag"
disable += "NewApi"
}
publishing {
singleVariant("release") {
withJavadocJar()
withSourcesJar()
}
}
}
dependencies {
implementation(libs.androidx.annotation)
implementation(libs.androidx.collection)
compileOnly(libs.jsr305)
testImplementation(libs.junit)
}
publishing {
publications {
register<MavenPublication>("release") {
groupId = pkg
artifactId = "tunnel"
version = providers.gradleProperty("wireguardVersionName").get()
afterEvaluate {
from(components["release"])
}
pom {
name.set("WireGuard Tunnel Library")
description.set("Embeddable tunnel library for WireGuard for Android")
url.set("https://www.wireguard.com/")
licenses {
license {
name.set("The Apache Software License, Version 2.0")
url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
distribution.set("repo")
}
}
scm {
connection.set("scm:git:https://git.zx2c4.com/wireguard-android")
developerConnection.set("scm:git:https://git.zx2c4.com/wireguard-android")
url.set("https://git.zx2c4.com/wireguard-android")
}
developers {
organization {
name.set("WireGuard")
url.set("https://www.wireguard.com/")
}
developer {
name.set("WireGuard")
email.set("team@wireguard.com")
}
}
}
}
}
repositories {
maven {
name = "sonatype"
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
credentials {
username = providers.environmentVariable("SONATYPE_USER").orNull
password = providers.environmentVariable("SONATYPE_PASSWORD").orNull
}
}
}
}
signing {
useGpgCmd()
sign(publishing.publications)
}
-82
View File
@@ -1,82 +0,0 @@
apply plugin: 'maven-publish'
apply plugin: 'signing'
afterEvaluate {
publishing {
publications {
release(MavenPublication) {
groupId = groupName
artifactId = 'tunnel'
version wireguardVersionName
artifact sourcesJar
artifact javadocJar
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/'
}
developer {
name = 'WireGuard'
email = 'team@wireguard.com'
}
}
}
}
}
repositories {
maven {
name = "sonatype"
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
credentials {
username = hasProperty('SONATYPE_USER') ? getProperty('SONATYPE_USER') : System.getenv('SONATYPE_USER')
password = hasProperty('SONATYPE_PASSWORD') ? getProperty('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
}
}
}
}
}
android.libraryVariants.all { variant ->
if (variant.name == 'release') {
task javadoc(type: Javadoc) {
source = variant.javaCompileProvider.get().source
classpath = files((android.bootClasspath.join(File.pathSeparator)))
classpath += variant.javaCompileProvider.get().classpath
title = 'Embeddable WireGuard Tunnel for Android v$wireguardVersionName'
}
task javadocJar(type: Jar, dependsOn: javadoc) {
archiveClassifier = 'javadoc'
from javadoc.destinationDir
}
task sourcesJar(type: Jar) {
archiveClassifier = 'sources'
from android.sourceSets.main.java.srcDirs
}
}
}
signing {
useGpgCmd()
sign publishing.publications
}
-81
View File
@@ -1,81 +0,0 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.kapt'
}
version wireguardVersionName
group groupName
android {
compileSdk 33
buildFeatures {
buildConfig = true
dataBinding = true
viewBinding = true
}
namespace = 'com.wireguard.android'
defaultConfig {
applicationId 'com.wireguard.android'
minSdkVersion 21
targetSdkVersion 33
versionCode wireguardVersionCode
versionName wireguardVersionName
buildConfigField 'int', 'MIN_SDK_VERSION', "$minSdkVersion.apiLevel"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
coreLibraryDesugaringEnabled = true
}
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles "proguard-android-optimize.txt"
packagingOptions {
exclude "DebugProbesKt.bin"
exclude "kotlin-tooling-metadata.json"
}
}
debug {
applicationIdSuffix ".debug"
versionNameSuffix "-debug"
}
}
lint {
disable 'LongLogTag'
warning 'MissingTranslation', 'ImpliedQuantity'
}
}
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"
implementation "androidx.coordinatorlayout:coordinatorlayout:$coordinatorLayoutVersion"
implementation "androidx.biometric:biometric:$biometricVersion"
implementation "androidx.core:core-ktx:$coreKtxVersion"
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.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$desugarVersion"
}
tasks.withType(JavaCompile).configureEach {
options.compilerArgs << '-Xlint:unchecked'
options.deprecation = true
}
tasks.withType(KotlinCompile).configureEach {
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8
}
+96
View File
@@ -0,0 +1,96 @@
@file:Suppress("UnstableApiUsage")
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
// Grotesque workaround for https://issuetracker.google.com/issues/279780940
System.setProperty("com.android.tools.r8.disableApiModeling", "1")
val pkg: String = providers.gradleProperty("wireguardPackageName").get()
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.kapt)
}
android {
compileSdk = 33
buildFeatures {
buildConfig = true
dataBinding = true
viewBinding = true
}
namespace = pkg
defaultConfig {
applicationId = pkg
minSdk = 21
targetSdk = 33
versionCode = providers.gradleProperty("wireguardVersionCode").get().toInt()
versionName = providers.gradleProperty("wireguardVersionName").get()
buildConfigField("int", "MIN_SDK_VERSION", minSdk.toString())
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
}
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles("proguard-android-optimize.txt")
packaging {
resources {
excludes += "DebugProbesKt.bin"
excludes += "kotlin-tooling-metadata.json"
excludes += "META-INF/*.version"
}
}
}
debug {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
}
create("googleplay") {
initWith(getByName("release"))
matchingFallbacks += "release"
}
}
androidResources {
generateLocaleConfig = true
}
lint {
disable += "LongLogTag"
warning += "MissingTranslation"
warning += "ImpliedQuantity"
}
}
dependencies {
implementation(project(":tunnel"))
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.annotation)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.coordinatorlayout)
implementation(libs.androidx.biometric)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.preference.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.datastore.preferences)
implementation(libs.google.material)
implementation(libs.zxing.android.embedded)
implementation(libs.kotlinx.coroutines.android)
coreLibraryDesugaring(libs.desugarJdkLibs)
}
tasks.withType<JavaCompile>().configureEach {
options.compilerArgs.add("-Xlint:unchecked")
options.isDeprecation = true
}
tasks.withType<KotlinCompile>().configureEach {
compilerOptions.jvmTarget.set(JvmTarget.JVM_17)
}
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission
android:name="android.permission.REQUEST_INSTALL_PACKAGES"
tools:node="remove" />
</manifest>
+23 -12
View File
@@ -45,10 +45,12 @@
<activity
android:name=".activity.TunnelToggleActivity"
android:theme="@style/NoBackgroundTheme"
android:excludeFromRecents="true"/>
android:excludeFromRecents="true"
android:theme="@style/NoBackgroundTheme" />
<activity android:name=".activity.MainActivity" android:exported="true">
<activity
android:name=".activity.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -62,8 +64,8 @@
<activity
android:name=".activity.TvMainActivity"
android:theme="@style/TvTheme"
android:exported="true">
android:exported="true"
android:theme="@style/TvTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
@@ -87,8 +89,8 @@
<activity
android:name=".activity.LogViewerActivity"
android:label="@string/log_viewer_title"
android:exported="false">
android:exported="false"
android:label="@string/log_viewer_title">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
</intent-filter>
@@ -100,18 +102,27 @@
android:exported="false"
android:grantUriPermissions="true" />
<receiver android:name=".BootShutdownReceiver" android:exported="true">
<receiver
android:name=".BootShutdownReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.ACTION_SHUTDOWN" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver
android:name=".updater.Updater$AppUpdatedReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<receiver
android:name=".model.TunnelManager$IntentReceiver"
android:permission="${applicationId}.permission.CONTROL_TUNNELS"
android:exported="true">
android:exported="true"
android:permission="${applicationId}.permission.CONTROL_TUNNELS">
<intent-filter>
<action android:name="com.wireguard.android.action.REFRESH_TUNNEL_STATES" />
<action android:name="com.wireguard.android.action.SET_TUNNEL_UP" />
@@ -121,9 +132,9 @@
<service
android:name=".QuickTileService"
android:exported="true"
android:icon="@drawable/ic_tile"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
android:exported="true">
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
@@ -8,25 +8,13 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import com.wireguard.android.activity.MainActivity
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.updater.Updater
import com.wireguard.android.util.applicationScope
import kotlinx.coroutines.launch
class BootShutdownReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action ?: return
if (Intent.ACTION_MY_PACKAGE_REPLACED == action && Updater.installer() == context.packageName) {
/* TODO: does not work because of restrictions placed on broadcast receivers. */
val start = Intent(context, MainActivity::class.java)
start.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
start.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(start)
return
}
applicationScope.launch {
if (Application.getBackend() !is WgQuickBackend) return@launch
val tunnelManager = Application.getTunnelManager()
@@ -75,6 +75,7 @@ class QuickTileService : TileService() {
}
override fun onCreate() {
isAdded = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
iconOn = Icon.createWithResource(this, R.drawable.ic_tile)
iconOff = iconOn
@@ -98,6 +99,11 @@ class QuickTileService : TileService() {
iconOff = Icon.createWithBitmap(b)
}
override fun onDestroy() {
super.onDestroy()
isAdded = false
}
override fun onStartListening() {
Application.getTunnelManager().addOnPropertyChangedCallback(onTunnelChangedCallback)
if (tunnel != null) tunnel!!.addOnPropertyChangedCallback(onStateChangedCallback)
@@ -109,6 +115,14 @@ class QuickTileService : TileService() {
Application.getTunnelManager().removeOnPropertyChangedCallback(onTunnelChangedCallback)
}
override fun onTileAdded() {
isAdded = true
}
override fun onTileRemoved() {
isAdded = false
}
private fun updateTile() {
// Update the tunnel.
val newTunnel = Application.getTunnelManager().lastUsedTunnel
@@ -157,5 +171,7 @@ class QuickTileService : TileService() {
companion object {
private const val TAG = "WireGuard/QuickTileService"
var isAdded: Boolean = false
private set
}
}
@@ -67,7 +67,8 @@ abstract class BaseActivity : AppCompatActivity() {
protected abstract fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?): Boolean
fun removeOnSelectedTunnelChangedListener(
listener: OnSelectedTunnelChangedListener) {
listener: OnSelectedTunnelChangedListener
) {
selectionChangeRegistry.remove(listener)
}
@@ -77,17 +78,17 @@ abstract class BaseActivity : AppCompatActivity() {
private class SelectionChangeNotifier : NotifierCallback<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel>() {
override fun onNotifyCallback(
listener: OnSelectedTunnelChangedListener,
oldTunnel: ObservableTunnel?,
ignored: Int,
newTunnel: ObservableTunnel?
listener: OnSelectedTunnelChangedListener,
oldTunnel: ObservableTunnel?,
ignored: Int,
newTunnel: ObservableTunnel?
) {
listener.onSelectedTunnelChanged(oldTunnel, newTunnel)
}
}
private class SelectionChangeRegistry :
CallbackRegistry<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel>(SelectionChangeNotifier())
CallbackRegistry<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel>(SelectionChangeNotifier())
companion object {
private const val KEY_SELECTED_TUNNEL = "selected_tunnel"
@@ -112,19 +112,21 @@ class LogViewerActivity : AppCompatActivity() {
}
binding.shareFab.setOnClickListener {
revokeLastUri()
val key = KeyPair().privateKey.toHex()
LOGS[key] = rawLogBytes()
lastUri = Uri.parse("content://${BuildConfig.APPLICATION_ID}.exported-log/$key")
val shareIntent = ShareCompat.IntentBuilder(this)
lifecycleScope.launch {
revokeLastUri()
val key = KeyPair().privateKey.toHex()
LOGS[key] = rawLogBytes()
lastUri = Uri.parse("content://${BuildConfig.APPLICATION_ID}.exported-log/$key")
val shareIntent = ShareCompat.IntentBuilder(this@LogViewerActivity)
.setType("text/plain")
.setSubject(getString(R.string.log_export_subject))
.setStream(lastUri)
.setChooserTitle(R.string.log_export_title)
.createChooserIntent()
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
grantUriPermission("android", lastUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
revokeLastActivityResultLauncher.launch(shareIntent)
grantUriPermission("android", lastUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
revokeLastActivityResultLauncher.launch(shareIntent)
}
}
}
@@ -140,22 +142,26 @@ class LogViewerActivity : AppCompatActivity() {
finish()
true
}
R.id.save_log -> {
saveButton?.isEnabled = false
lifecycleScope.launch { saveLog() }
true
}
else -> super.onOptionsItemSelected(item)
}
}
private val downloadsFileSaver = DownloadsFileSaver(this)
private fun rawLogBytes() : ByteArray {
private suspend fun rawLogBytes(): ByteArray {
val builder = StringBuilder()
for (i in 0 until rawLogLines.size()) {
builder.append(rawLogLines[i])
builder.append('\n')
withContext(Dispatchers.IO) {
for (i in 0 until rawLogLines.size()) {
builder.append(rawLogLines[i])
builder.append('\n')
}
}
return builder.toString().toByteArray(Charsets.UTF_8)
}
@@ -175,12 +181,14 @@ class LogViewerActivity : AppCompatActivity() {
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()
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) {
@@ -283,7 +291,8 @@ class LogViewerActivity : AppCompatActivity() {
*
* <pre>05-26 11:02:36.886 5689 5689 D AndroidRuntime: CheckJNI is OFF.</pre>
*/
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 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 TAG = "WireGuard/LogViewerActivity"
}
@@ -306,7 +315,7 @@ class LogViewerActivity : AppCompatActivity() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.log_viewer_entry, parent, false)
.inflate(R.layout.log_viewer_entry, parent, false)
return ViewHolder(view)
}
@@ -317,8 +326,10 @@ class LogViewerActivity : AppCompatActivity() {
else
SpannableString("${line.tag}: ${line.msg}").apply {
setSpan(StyleSpan(BOLD), 0, "${line.tag}:".length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
setSpan(ForegroundColorSpan(levelToColor(line.level)),
0, "${line.tag}:".length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
setSpan(
ForegroundColorSpan(levelToColor(line.level)),
0, "${line.tag}:".length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
holder.layout.apply {
findViewById<MaterialTextView>(R.id.log_date).text = line.time.toString()
@@ -340,11 +351,11 @@ class LogViewerActivity : AppCompatActivity() {
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? =
logForUri(uri)?.let {
val m = MatrixCursor(arrayOf(android.provider.OpenableColumns.DISPLAY_NAME, android.provider.OpenableColumns.SIZE), 1)
m.addRow(arrayOf("wireguard-log.txt", it.size.toLong()))
m
}
logForUri(uri)?.let {
val m = MatrixCursor(arrayOf(android.provider.OpenableColumns.DISPLAY_NAME, android.provider.OpenableColumns.SIZE), 1)
m.addRow(arrayOf("wireguard-log.txt", it.size.toLong()))
m
}
override fun onCreate(): Boolean = true
@@ -354,7 +365,8 @@ class LogViewerActivity : AppCompatActivity() {
override fun getType(uri: Uri): String? = logForUri(uri)?.let { "text/plain" }
override fun getStreamTypes(uri: Uri, mimeTypeFilter: String): Array<String>? = getType(uri)?.let { if (compareMimeTypes(it, mimeTypeFilter)) arrayOf(it) else null }
override fun getStreamTypes(uri: Uri, mimeTypeFilter: String): Array<String>? =
getType(uri)?.let { if (compareMimeTypes(it, mimeTypeFilter)) arrayOf(it) else null }
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
if (mode != "r") return null
@@ -77,6 +77,7 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener
onBackPressedDispatcher.onBackPressed()
true
}
R.id.menu_action_edit -> {
supportFragmentManager.commit {
replace(R.id.detail_container, TunnelEditorFragment())
@@ -91,12 +92,15 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener
startActivity(Intent(this, SettingsActivity::class.java))
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?,
newTunnel: ObservableTunnel?): Boolean {
override fun onSelectedTunnelChanged(
oldTunnel: ObservableTunnel?,
newTunnel: ObservableTunnel?
): Boolean {
val fragmentManager = supportFragmentManager
if (fragmentManager.isStateSaved) {
return false
@@ -4,9 +4,11 @@
*/
package com.wireguard.android.activity
import android.content.ComponentName
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.service.quicksettings.TileService
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.commit
@@ -14,6 +16,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.wireguard.android.Application
import com.wireguard.android.QuickTileService
import com.wireguard.android.R
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.preference.PreferencesPreferenceDataStore
@@ -47,7 +50,13 @@ class SettingsActivity : AppCompatActivity() {
override fun onCreatePreferences(savedInstanceState: Bundle?, key: String?) {
preferenceManager.preferenceDataStore = PreferencesPreferenceDataStore(lifecycleScope, Application.getPreferencesDataStore())
addPreferencesFromResource(R.xml.preferences)
preferenceScreen.initialExpandedChildrenCount = 4
preferenceScreen.initialExpandedChildrenCount = 5
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || QuickTileService.isAdded) {
val quickTile = preferenceManager.findPreference<Preference>("quick_tile")
quickTile?.parent?.removePreference(quickTile)
--preferenceScreen.initialExpandedChildrenCount
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val darkTheme = preferenceManager.findPreference<Preference>("dark_theme")
darkTheme?.parent?.removePreference(darkTheme)
@@ -62,9 +71,9 @@ class SettingsActivity : AppCompatActivity() {
zipExporter?.parent?.removePreference(zipExporter)
}
val wgQuickOnlyPrefs = arrayOf(
preferenceManager.findPreference("tools_installer"),
preferenceManager.findPreference("restore_on_boot"),
preferenceManager.findPreference<Preference>("multiple_tunnels")
preferenceManager.findPreference("tools_installer"),
preferenceManager.findPreference("restore_on_boot"),
preferenceManager.findPreference<Preference>("multiple_tunnels")
).filterNotNull()
wgQuickOnlyPrefs.forEach { it.isVisible = false }
lifecycleScope.launch {
@@ -24,7 +24,8 @@ import kotlinx.coroutines.launch
@RequiresApi(Build.VERSION_CODES.N)
class TunnelToggleActivity : AppCompatActivity() {
private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { toggleTunnelWithPermissionsResult() }
private val permissionActivityResultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { toggleTunnelWithPermissionsResult() }
private fun toggleTunnelWithPermissionsResult() {
val tunnel = Application.getTunnelManager().lastUsedTunnel ?: return
@@ -211,12 +211,13 @@ class TvMainActivity : AppCompatActivity() {
try {
tunnelFileImportResultLauncher.launch("*/*")
} catch (_: Throwable) {
MaterialAlertDialogBuilder(binding.root.context).setMessage(R.string.tv_no_file_picker).setCancelable(false).setPositiveButton(android.R.string.ok) { _, _ ->
try {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://webstoreredirect")))
} catch (_: Throwable) {
}
}.show()
MaterialAlertDialogBuilder(binding.root.context).setMessage(R.string.tv_no_file_picker).setCancelable(false)
.setPositiveButton(android.R.string.ok) { _, _ ->
try {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://webstoreredirect")))
} catch (_: Throwable) {
}
}.show()
}
}
}
@@ -359,6 +360,7 @@ class TvMainActivity : AppCompatActivity() {
binding.tunnelList.requestFocus()
}
}
filesRoot.get()?.isNotEmpty() == true -> {
files.clear()
filesRoot.set("")
@@ -372,7 +374,7 @@ class TvMainActivity : AppCompatActivity() {
private suspend fun updateStats() {
binding.tunnelList.forEach { viewItem ->
val listItem = DataBindingUtil.findBinding<TvTunnelListItemBinding>(viewItem)
?: return@forEach
?: return@forEach
try {
val tunnel = listItem.item!!
if (tunnel.state != Tunnel.State.UP || isDeleting.get()) {
@@ -40,9 +40,9 @@ class FileConfigStore(private val context: Context) : ConfigStore {
override fun enumerate(): Set<String> {
return context.fileList()
.filter { it.endsWith(".conf") }
.map { it.substring(0, it.length - ".conf".length) }
.toSet()
.filter { it.endsWith(".conf") }
.map { it.substring(0, it.length - ".conf".length) }
.toSet()
}
private fun fileFor(name: String): File {
@@ -47,9 +47,11 @@ object BindingAdapters {
@JvmStatic
@BindingAdapter("items", "layout", "fragment")
fun <E> setItems(view: LinearLayout,
oldList: ObservableList<E>?, oldLayoutId: Int, @Suppress("UNUSED_PARAMETER") oldFragment: Fragment?,
newList: ObservableList<E>?, newLayoutId: Int, newFragment: Fragment?) {
fun <E> setItems(
view: LinearLayout,
oldList: ObservableList<E>?, oldLayoutId: Int, @Suppress("UNUSED_PARAMETER") oldFragment: Fragment?,
newList: ObservableList<E>?, newLayoutId: Int, newFragment: Fragment?
) {
if (oldList === newList && oldLayoutId == newLayoutId)
return
var listener: ItemChangeListener<E>? = ListenerUtil.getListener(view, R.id.item_change_listener)
@@ -73,9 +75,11 @@ object BindingAdapters {
@JvmStatic
@BindingAdapter("items", "layout")
fun <E> setItems(view: LinearLayout,
oldList: Iterable<E>?, oldLayoutId: Int,
newList: Iterable<E>?, newLayoutId: Int) {
fun <E> setItems(
view: LinearLayout,
oldList: Iterable<E>?, oldLayoutId: Int,
newList: Iterable<E>?, newLayoutId: Int
) {
if (oldList === newList && oldLayoutId == newLayoutId)
return
view.removeAllViews()
@@ -93,11 +97,13 @@ object BindingAdapters {
@JvmStatic
@BindingAdapter(requireAll = false, value = ["items", "layout", "configurationHandler"])
fun <K, E : Keyed<out K>> setItems(view: RecyclerView,
oldList: ObservableKeyedArrayList<K, E>?, oldLayoutId: Int,
@Suppress("UNUSED_PARAMETER") oldRowConfigurationHandler: RowConfigurationHandler<*, *>?,
newList: ObservableKeyedArrayList<K, E>?, newLayoutId: Int,
newRowConfigurationHandler: RowConfigurationHandler<*, *>?) {
fun <K, E : Keyed<out K>> setItems(
view: RecyclerView,
oldList: ObservableKeyedArrayList<K, E>?, oldLayoutId: Int,
@Suppress("UNUSED_PARAMETER") oldRowConfigurationHandler: RowConfigurationHandler<*, *>?,
newList: ObservableKeyedArrayList<K, E>?, newLayoutId: Int,
newRowConfigurationHandler: RowConfigurationHandler<*, *>?
) {
if (view.layoutManager == null)
view.layoutManager = LinearLayoutManager(view.context, RecyclerView.VERTICAL, false)
if (oldList === newList && oldLayoutId == newLayoutId)
@@ -123,16 +129,20 @@ object BindingAdapters {
@JvmStatic
@BindingAdapter("onBeforeCheckedChanged")
fun setOnBeforeCheckedChanged(view: ToggleSwitch,
listener: OnBeforeCheckedChangeListener?) {
fun setOnBeforeCheckedChanged(
view: ToggleSwitch,
listener: OnBeforeCheckedChangeListener?
) {
view.setOnBeforeCheckedChangeListener(listener)
}
@JvmStatic
@BindingAdapter("onFocusChange")
fun setOnFocusChange(view: EditText,
listener: View.OnFocusChangeListener?) {
view.setOnFocusChangeListener(listener)
fun setOnFocusChange(
view: EditText,
listener: View.OnFocusChangeListener?
) {
view.onFocusChangeListener = listener
}
@JvmStatic
@@ -61,8 +61,10 @@ internal class ItemChangeListener<T>(private val container: ViewGroup, private v
}
}
override fun onItemRangeChanged(sender: ObservableList<T>, positionStart: Int,
itemCount: Int) {
override fun onItemRangeChanged(
sender: ObservableList<T>, positionStart: Int,
itemCount: Int
) {
val listener = weakListener.get()
if (listener != null) {
for (i in positionStart until positionStart + itemCount) {
@@ -75,8 +77,10 @@ internal class ItemChangeListener<T>(private val container: ViewGroup, private v
}
}
override fun onItemRangeInserted(sender: ObservableList<T>, positionStart: Int,
itemCount: Int) {
override fun onItemRangeInserted(
sender: ObservableList<T>, positionStart: Int,
itemCount: Int
) {
val listener = weakListener.get()
if (listener != null) {
for (i in positionStart until positionStart + itemCount)
@@ -86,8 +90,10 @@ internal class ItemChangeListener<T>(private val container: ViewGroup, private v
}
}
override fun onItemRangeMoved(sender: ObservableList<T>, fromPosition: Int,
toPosition: Int, itemCount: Int) {
override fun onItemRangeMoved(
sender: ObservableList<T>, fromPosition: Int,
toPosition: Int, itemCount: Int
) {
val listener = weakListener.get()
if (listener != null) {
val views = arrayOfNulls<View>(itemCount)
@@ -99,8 +105,10 @@ internal class ItemChangeListener<T>(private val container: ViewGroup, private v
}
}
override fun onItemRangeRemoved(sender: ObservableList<T>, positionStart: Int,
itemCount: Int) {
override fun onItemRangeRemoved(
sender: ObservableList<T>, positionStart: Int,
itemCount: Int
) {
val listener = weakListener.get()
if (listener != null) {
listener.container.removeViews(positionStart, itemCount)
@@ -49,7 +49,8 @@ class AppListDialogFragment : DialogFragment() {
packageInfos.forEach {
val packageName = it.packageName
val appInfo = it.applicationInfo
val appData = ApplicationData(appInfo.loadIcon(pm), appInfo.loadLabel(pm).toString(), packageName, currentlySelectedApps.contains(packageName))
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) {
@@ -143,10 +144,12 @@ class AppListDialogFragment : DialogFragment() {
selectedApps.add(data.packageName)
}
}
setFragmentResult(REQUEST_SELECTION, bundleOf(
setFragmentResult(
REQUEST_SELECTION, bundleOf(
KEY_SELECTED_APPS to selectedApps.toTypedArray(),
KEY_IS_EXCLUDED to (tabs?.selectedTabPosition == 0)
))
)
)
dismiss()
}
@@ -99,8 +99,8 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
val view = view
if (view != null)
Snackbar.make(view, message, Snackbar.LENGTH_LONG)
.setAnchorView(view.findViewById(R.id.create_fab))
.show()
.setAnchorView(view.findViewById(R.id.create_fab))
.show()
else
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
Log.e(TAG, message, e)
@@ -37,6 +37,7 @@ class ConfigNamingDialogFragment : DialogFragment() {
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val configText = requireArguments().getString(KEY_CONFIG_TEXT)
@@ -40,8 +40,10 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider {
menuInflater.inflate(R.menu.tunnel_detail, menu)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState)
binding = TunnelDetailFragmentBinding.inflate(inflater, container, false)
binding?.executePendingBindings()
@@ -110,16 +112,18 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider {
val statistics = tunnel.getStatisticsAsync()
for (i in 0 until binding.peersLayout.childCount) {
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i))
?: continue
?: continue
val publicKey = peer.item!!.publicKey
val peerStats = statistics.peer(publicKey)
if (peerStats == null || (peerStats.rxBytes == 0L && peerStats.txBytes == 0L)) {
peer.transferLabel.visibility = View.GONE
peer.transferText.visibility = View.GONE
} else {
peer.transferText.text = getString(R.string.transfer_rx_tx,
peer.transferText.text = getString(
R.string.transfer_rx_tx,
QuantityFormatter.formatBytes(peerStats.rxBytes),
QuantityFormatter.formatBytes(peerStats.txBytes))
QuantityFormatter.formatBytes(peerStats.txBytes)
)
peer.transferLabel.visibility = View.VISIBLE
peer.transferText.visibility = View.VISIBLE
}
@@ -135,7 +139,7 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider {
} catch (e: Throwable) {
for (i in 0 until binding.peersLayout.childCount) {
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i))
?: continue
?: continue
peer.transferLabel.visibility = View.GONE
peer.transferText.visibility = View.GONE
peer.latestHandshakeLabel.visibility = View.GONE
@@ -5,7 +5,6 @@
package com.wireguard.android.fragment
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.util.Log
@@ -67,16 +66,14 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.config_editor, menu)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState)
binding = TunnelEditorFragmentBinding.inflate(inflater, container, false)
binding?.apply {
@@ -103,8 +100,10 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
val focusedView = activity.currentFocus
if (focusedView != null) {
val inputManager = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
inputManager?.hideSoftInputFromWindow(focusedView.windowToken,
InputMethodManager.HIDE_NOT_ALWAYS)
inputManager?.hideSoftInputFromWindow(
focusedView.windowToken,
InputMethodManager.HIDE_NOT_ALWAYS
)
}
parentFragmentManager.popBackStackImmediate()
@@ -138,6 +137,7 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
onTunnelCreated(null, e)
}
}
tunnel!!.name != binding!!.name -> {
Log.d(TAG, "Attempting to rename tunnel to " + binding!!.name)
try {
@@ -147,6 +147,7 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
onTunnelRenamed(tunnel!!, newConfig, e)
}
}
else -> {
Log.d(TAG, "Attempting to save config of " + tunnel!!.name)
try {
@@ -202,8 +203,10 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
super.onSaveInstanceState(outState)
}
override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?,
newTunnel: ObservableTunnel?) {
override fun onSelectedTunnelChanged(
oldTunnel: ObservableTunnel?,
newTunnel: ObservableTunnel?
) {
tunnel = newTunnel
if (binding == null) return
binding!!.config = ConfigProxy()
@@ -240,8 +243,10 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
}
}
private suspend fun onTunnelRenamed(renamedTunnel: ObservableTunnel, newConfig: Config,
throwable: Throwable?) {
private suspend fun onTunnelRenamed(
renamedTunnel: ObservableTunnel, newConfig: Config,
throwable: Throwable?
) {
val ctx = activity ?: Application.get()
if (throwable == null) {
val message = ctx.getString(R.string.tunnel_rename_success, renamedTunnel.name)
@@ -298,13 +303,15 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
haveShownKeys = true
showPrivateKey(edit)
}
is BiometricAuthenticator.Result.Failure -> {
Snackbar.make(
binding!!.mainContainer,
it.message,
Snackbar.LENGTH_SHORT
binding!!.mainContainer,
it.message,
Snackbar.LENGTH_SHORT
).show()
}
is BiometricAuthenticator.Result.Cancelled -> {}
}
}
@@ -81,6 +81,8 @@ class TunnelListFragment : BaseFragment() {
}
}
private val snackbarUpdateShower = SnackbarUpdateShower(this)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (savedInstanceState != null) {
@@ -91,8 +93,10 @@ class TunnelListFragment : BaseFragment() {
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState)
binding = TunnelListFragmentBinding.inflate(inflater, container, false)
val bottomSheet = AddTunnelsSheet()
@@ -105,26 +109,29 @@ class TunnelListFragment : BaseFragment() {
AddTunnelsSheet.REQUEST_CREATE -> {
startActivity(Intent(requireActivity(), TunnelCreatorActivity::class.java))
}
AddTunnelsSheet.REQUEST_IMPORT -> {
tunnelFileImportResultLauncher.launch("*/*")
}
AddTunnelsSheet.REQUEST_SCAN -> {
qrImportResultLauncher.launch(ScanOptions()
qrImportResultLauncher.launch(
ScanOptions()
.setOrientationLocked(false)
.setBeepEnabled(false)
.setPrompt(getString(R.string.qr_code_hint)))
.setPrompt(getString(R.string.qr_code_hint))
)
}
}
}
bottomSheet.showNow(childFragmentManager, "BOTTOM_SHEET")
}
executePendingBindings()
snackbarUpdateShower.attach(mainContainer, createFab)
}
backPressedCallback = requireActivity().onBackPressedDispatcher.addCallback(this) { actionMode?.finish() }
backPressedCallback?.isEnabled = false
SnackbarUpdateShower.attachToActivity(requireActivity(), binding?.mainContainer!!, binding?.createFab)
return binding?.root
}
@@ -191,8 +198,8 @@ class TunnelListFragment : BaseFragment() {
val binding = binding
if (binding != null)
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG)
.setAnchorView(binding.createFab)
.show()
.setAnchorView(binding.createFab)
.show()
else
Toast.makeText(activity ?: Application.get(), message, Toast.LENGTH_SHORT).show()
}
@@ -234,6 +241,7 @@ class TunnelListFragment : BaseFragment() {
mode.finish()
true
}
R.id.menu_action_select_all -> {
lifecycleScope.launch {
val tunnels = Application.getTunnelManager().getTunnels()
@@ -243,6 +251,7 @@ class TunnelListFragment : BaseFragment() {
}
true
}
else -> false
}
}
@@ -308,7 +317,7 @@ class TunnelListFragment : BaseFragment() {
private fun animateFab(view: View?, show: Boolean) {
view ?: return
val animation = AnimationUtils.loadAnimation(
context, if (show) R.anim.scale_up else R.anim.scale_down
context, if (show) R.anim.scale_up else R.anim.scale_down
)
animation.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationRepeat(animation: Animation?) {
@@ -21,10 +21,10 @@ import kotlinx.coroutines.withContext
* Encapsulates the volatile and nonvolatile state of a WireGuard tunnel.
*/
class ObservableTunnel internal constructor(
private val manager: TunnelManager,
private var name: String,
config: Config?,
state: Tunnel.State
private val manager: TunnelManager,
private var name: String,
config: Config?,
state: Tunnel.State
) : BaseObservable(), Keyed<String>, Tunnel {
override val key
get() = name
@@ -140,7 +140,8 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
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()
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))
}
@@ -8,7 +8,6 @@ package com.wireguard.android.preference
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.util.AttributeSet
import android.widget.Toast
import androidx.preference.Preference
@@ -24,7 +23,7 @@ class DonatePreference(context: Context, attrs: AttributeSet?) : Preference(cont
override fun onClick() {
/* Google Play Store forbids links to our donation page. */
if (Updater.installerIsGooglePlay()) {
if (Updater.installerIsGooglePlay(context)) {
MaterialAlertDialogBuilder(context)
.setTitle(R.string.donate_title)
.setMessage(R.string.donate_google_play_disappointment)
@@ -0,0 +1,50 @@
/*
* Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference
import android.app.StatusBarManager
import android.content.ComponentName
import android.content.Context
import android.graphics.drawable.Icon
import android.os.Build
import android.util.AttributeSet
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.preference.Preference
import com.wireguard.android.QuickTileService
import com.wireguard.android.R
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
class QuickTilePreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
override fun getSummary() = context.getString(R.string.quick_settings_tile_add_summary)
override fun getTitle() = context.getString(R.string.quick_settings_tile_add_title)
override fun onClick() {
val statusBarManager = context.getSystemService(StatusBarManager::class.java)
statusBarManager.requestAddTileService(
ComponentName(context, QuickTileService::class.java),
context.getString(R.string.quick_settings_tile_action),
Icon.createWithResource(context, R.drawable.ic_tile),
context.mainExecutor
) {
when (it) {
StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED,
StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED -> {
parent?.removePreference(this)
--preferenceManager.preferenceScreen.initialExpandedChildrenCount
}
StatusBarManager.TILE_ADD_REQUEST_ERROR_MISMATCHED_PACKAGE,
StatusBarManager.TILE_ADD_REQUEST_ERROR_REQUEST_IN_PROGRESS,
StatusBarManager.TILE_ADD_REQUEST_ERROR_BAD_COMPONENT,
StatusBarManager.TILE_ADD_REQUEST_ERROR_NOT_CURRENT_USER,
StatusBarManager.TILE_ADD_REQUEST_ERROR_APP_NOT_IN_FOREGROUND,
StatusBarManager.TILE_ADD_REQUEST_ERROR_NO_STATUS_BAR_SERVICE ->
Toast.makeText(context, context.getString(R.string.quick_settings_tile_add_failure, it), Toast.LENGTH_SHORT).show()
}
}
}
}
@@ -70,14 +70,16 @@ class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference
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()
activity.findViewById(android.R.id.content),
message, Snackbar.LENGTH_LONG
).show()
isEnabled = true
}
}
}
override fun getSummary() = if (exportedFilePath == null) context.getString(R.string.zip_export_summary) else context.getString(R.string.zip_export_success, exportedFilePath)
override fun getSummary() =
if (exportedFilePath == null) context.getString(R.string.zip_export_summary) else context.getString(R.string.zip_export_success, exportedFilePath)
override fun getTitle() = context.getString(R.string.zip_export_title)
@@ -91,13 +93,15 @@ class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference
isEnabled = false
exportZip()
}
is BiometricAuthenticator.Result.Failure -> {
Snackbar.make(
activity.findViewById(android.R.id.content),
it.message,
Snackbar.LENGTH_SHORT
activity.findViewById(android.R.id.content),
it.message,
Snackbar.LENGTH_SHORT
).show()
}
is BiometricAuthenticator.Result.Cancelled -> {}
}
}
@@ -5,9 +5,12 @@
package com.wireguard.android.updater
import android.content.Intent
import android.net.Uri
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
@@ -20,15 +23,21 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds
object SnackbarUpdateShower {
private class SwapableSnackbar(activity: FragmentActivity, view: View, anchor: View?) {
val actionSnackbar = makeSnackbar(activity, view, anchor)
val statusSnackbar = makeSnackbar(activity, view, anchor)
var showingAction: Boolean = false
var showingStatus: Boolean = false
class SnackbarUpdateShower(private val fragment: Fragment) {
private var lastUserIntervention: Updater.Progress.NeedsUserIntervention? = null
private val intentLauncher = fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
lastUserIntervention?.markAsDone()
}
private fun makeSnackbar(activity: FragmentActivity, view: View, anchor: View?): Snackbar {
val snackbar = Snackbar.make(activity, view, "", Snackbar.LENGTH_INDEFINITE)
private class SwapableSnackbar(fragment: Fragment, view: View, anchor: View?) {
private val actionSnackbar = makeSnackbar(fragment, view, anchor)
private val statusSnackbar = makeSnackbar(fragment, view, anchor)
private var showingAction: Boolean = false
private var showingStatus: Boolean = false
private var permanentAction: Boolean = false
private fun makeSnackbar(fragment: Fragment, view: View, anchor: View?): Snackbar {
val snackbar = Snackbar.make(fragment.requireContext(), view, "", Snackbar.LENGTH_INDEFINITE)
if (anchor != null)
snackbar.anchorView = anchor
snackbar.setTextMaxLines(6)
@@ -40,12 +49,11 @@ object SnackbarUpdateShower {
snackbar.addCallback(object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
override fun onDismissed(snackbar: Snackbar?, @DismissEvent event: Int) {
super.onDismissed(snackbar, event)
if (event == DISMISS_EVENT_MANUAL || event == DISMISS_EVENT_ACTION ||
(snackbar == actionSnackbar && !showingAction) ||
(snackbar == statusSnackbar && !showingStatus)
if (event == DISMISS_EVENT_MANUAL || (event == DISMISS_EVENT_ACTION && !permanentAction) ||
(snackbar == actionSnackbar && !showingAction) || (snackbar == statusSnackbar && !showingStatus)
)
return
activity.lifecycleScope.launch {
fragment.lifecycleScope.launch {
delay(5.seconds)
snackbar?.show()
}
@@ -54,11 +62,12 @@ object SnackbarUpdateShower {
return snackbar
}
fun showAction(text: String, action: String, listener: View.OnClickListener) {
fun showAction(text: String, action: String, permanent: Boolean = false, listener: View.OnClickListener) {
if (showingStatus) {
showingStatus = false
statusSnackbar.dismiss()
}
permanentAction = permanent
actionSnackbar.setText(text)
actionSnackbar.setAction(action, listener)
if (!showingAction) {
@@ -87,14 +96,9 @@ object SnackbarUpdateShower {
}
}
fun attachToActivity(activity: FragmentActivity, view: View, anchor: View?) {
val snackbar = SwapableSnackbar(activity, view, anchor)
val context = activity.applicationContext
var lastUserIntervention: Updater.Progress.NeedsUserIntervention? = null
val intentLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
lastUserIntervention?.markAsDone()
}
fun attach(view: View, anchor: View?) {
val snackbar = SwapableSnackbar(fragment, view, anchor)
val context = fragment.requireContext()
Updater.state.onEach { progress ->
when (progress) {
@@ -102,10 +106,7 @@ object SnackbarUpdateShower {
snackbar.dismiss()
is Updater.Progress.Available ->
snackbar.showAction(
context.getString(R.string.updater_avalable),
context.getString(R.string.updater_action)
) {
snackbar.showAction(context.getString(R.string.updater_avalable), context.getString(R.string.updater_action)) {
progress.update()
}
@@ -141,16 +142,23 @@ object SnackbarUpdateShower {
}
is Updater.Progress.Failure -> {
snackbar.showText(
context.getString(
R.string.updater_failure,
ErrorMessages[progress.error]
)
)
snackbar.showText(context.getString(R.string.updater_failure, ErrorMessages[progress.error]))
delay(5.seconds)
progress.retry()
}
is Updater.Progress.Corrupt -> {
snackbar.showAction(context.getString(R.string.updater_corrupt), context.getString(R.string.updater_corrupt_navigate), true) {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(progress.downloadUrl)
try {
context.startActivity(intent)
} catch (e: Throwable) {
Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_SHORT).show()
}
}
}
}
}.launchIn(activity.lifecycleScope)
}.launchIn(fragment.lifecycleScope)
}
}
@@ -4,12 +4,14 @@
*/
package com.wireguard.android.updater
import android.Manifest
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import android.os.Build
import android.util.Base64
import android.util.Log
@@ -17,8 +19,11 @@ import androidx.core.content.ContextCompat
import androidx.core.content.IntentCompat
import com.wireguard.android.Application
import com.wireguard.android.BuildConfig
import com.wireguard.android.activity.MainActivity
import com.wireguard.android.util.UserKnobs
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -41,12 +46,31 @@ import kotlin.time.Duration.Companion.seconds
object Updater {
private const val TAG = "WireGuard/Updater"
private const val LATEST_VERSION_URL = "https://download.wireguard.com/android-client/latest.sig"
private const val APK_PATH_URL = "https://download.wireguard.com/android-client/%s"
private const val UPDATE_URL_FMT = "https://download.wireguard.com/android-client/%s"
private const val APK_NAME_PREFIX = BuildConfig.APPLICATION_ID + "-"
private const val APK_NAME_SUFFIX = ".apk"
private const val RELEASE_PUBLIC_KEY_BASE64 = "RWTAzwGRYr3EC9px0Ia3fbttz8WcVN6wrOwWp2delz4el6SI8XmkKSMp"
private val CURRENT_VERSION = BuildConfig.VERSION_NAME.removeSuffix("-debug")
private val CURRENT_VERSION = Version(BuildConfig.VERSION_NAME.removeSuffix("-debug"))
private val LATEST_FILE = if (BuildConfig.DEBUG) "latest-debug.sig" else "latest.sig"
private val RELEASE_PUBLIC_KEY_BASE64 =
if (BuildConfig.DEBUG) "RWTKFCNaLimMkuolGwAw12XT6sx+nnS7KE1wmhJ7YHXvAPedtPR/rofU"
else "RWTAzwGRYr3EC9px0Ia3fbttz8WcVN6wrOwWp2delz4el6SI8XmkKSMp"
private val updaterScope = CoroutineScope(Job() + Dispatchers.IO)
private fun installer(context: Context): String = try {
val packageName = context.packageName
val pm = context.packageManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
pm.getInstallSourceInfo(packageName).installingPackageName ?: ""
} else {
@Suppress("DEPRECATION")
pm.getInstallerPackageName(packageName) ?: ""
}
} catch (_: Throwable) {
""
}
fun installerIsGooglePlay(context: Context): Boolean = installer(context) == "com.android.vending"
sealed class Progress {
object Complete : Progress()
@@ -89,11 +113,16 @@ object Updater {
class Failure(val error: Throwable) : Progress() {
fun retry() {
Application.getCoroutineScope().launch {
updaterScope.launch {
downloadAndUpdateWrapErrors()
}
}
}
class Corrupt(private val betterFile: String?) : Progress() {
val downloadUrl: String
get() = UPDATE_URL_FMT.format(betterFile ?: "")
}
}
private val mutableState = MutableStateFlow<Progress>(Progress.Complete)
@@ -104,29 +133,61 @@ object Updater {
mutableState.emit(progress)
}
private fun versionIsNewer(lhs: String, rhs: String): Boolean {
val lhsParts = lhs.split(".")
val rhsParts = rhs.split(".")
if (lhsParts.isEmpty() || rhsParts.isEmpty())
throw InvalidParameterException("Version is empty")
private class Sha256Digest(hex: String) {
val bytes: ByteArray
for (i in 0 until max(lhsParts.size, rhsParts.size)) {
val lhsPart = if (i < lhsParts.size) lhsParts[i].toULong() else 0UL
val rhsPart = if (i < rhsParts.size) rhsParts[i].toULong() else 0UL
if (lhsPart == rhsPart)
continue
return lhsPart > rhsPart
init {
if (hex.length != 64)
throw InvalidParameterException("SHA256 hashes must be 32 bytes long")
bytes = hex.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
}
return false
}
private fun versionOfFile(name: String): String? {
@OptIn(ExperimentalUnsignedTypes::class)
private class Version(version: String) : Comparable<Version> {
val parts: ULongArray
init {
val strParts = version.split(".")
if (strParts.isEmpty())
throw InvalidParameterException("Version has no parts")
parts = ULongArray(strParts.size)
for (i in parts.indices) {
parts[i] = strParts[i].toULong()
}
}
override fun toString(): String {
return parts.joinToString(".")
}
override fun compareTo(other: Version): Int {
for (i in 0 until max(parts.size, other.parts.size)) {
val lhsPart = if (i < parts.size) parts[i] else 0UL
val rhsPart = if (i < other.parts.size) other.parts[i] else 0UL
if (lhsPart > rhsPart)
return 1
else if (lhsPart < rhsPart)
return -1
}
return 0
}
}
private class Update(val fileName: String, val version: Version, val hash: Sha256Digest)
private fun versionOfFile(name: String): Version? {
if (!name.startsWith(APK_NAME_PREFIX) || !name.endsWith(APK_NAME_SUFFIX))
return null
return name.substring(APK_NAME_PREFIX.length, name.length - APK_NAME_SUFFIX.length)
return try {
Version(name.substring(APK_NAME_PREFIX.length, name.length - APK_NAME_SUFFIX.length))
} catch (_: Throwable) {
null
}
}
private fun verifySignedFileList(signifyDigest: String): Map<String, Sha256Digest> {
private fun verifySignedFileList(signifyDigest: String): List<Update> {
val updates = ArrayList<Update>(1)
val publicKeyBytes = Base64.decode(RELEASE_PUBLIC_KEY_BASE64, Base64.DEFAULT)
if (publicKeyBytes == null || publicKeyBytes.size != 32 + 10 || publicKeyBytes[0] != 'E'.code.toByte() || publicKeyBytes[1] != 'd'.code.toByte())
throw InvalidKeyException("Invalid public key")
@@ -149,32 +210,23 @@ object Updater {
)
)
throw SecurityException("Invalid signature")
val hashes: MutableMap<String, Sha256Digest> = HashMap()
for (line in lines[2].split("\n").dropLastWhile { it.isEmpty() }) {
val components = line.split(" ", limit = 2)
if (components.size != 2)
throw InvalidParameterException("Invalid file list format: too few components")
hashes[components[1]] = Sha256Digest(components[0])
/* If version is null, it's not a file we understand, but still a legitimate entry, so don't throw. */
val version = versionOfFile(components[1]) ?: continue
updates.add(Update(components[1], version, Sha256Digest(components[0])))
}
return hashes
return updates
}
private class Sha256Digest(hex: String) {
val bytes: ByteArray
init {
if (hex.length != 64)
throw InvalidParameterException("SHA256 hashes must be 32 bytes long")
bytes = hex.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
}
}
private fun checkForUpdates(): Pair<String, Sha256Digest> {
val connection = URL(LATEST_VERSION_URL).openConnection() as HttpURLConnection
private fun checkForUpdates(): Update? {
val connection = URL(UPDATE_URL_FMT.format(LATEST_FILE)).openConnection() as HttpURLConnection
connection.setRequestProperty("User-Agent", Application.USER_AGENT)
connection.connect()
if (connection.responseCode != HttpURLConnection.HTTP_OK)
throw IOException("File list could not be fetched: ${connection.responseCode}")
throw IOException(connection.responseMessage)
var fileListBytes = ByteArray(1024 * 512 /* 512 KiB */)
connection.inputStream.use {
val len = it.read(fileListBytes)
@@ -182,38 +234,14 @@ object Updater {
throw IOException("File list is empty")
fileListBytes = fileListBytes.sliceArray(0 until len)
}
val fileList = verifySignedFileList(fileListBytes.decodeToString())
if (fileList.isEmpty())
throw InvalidParameterException("File list is empty")
var newestFile: String? = null
var newestVersion: String? = null
var newestFileHash: Sha256Digest? = null
for (file in fileList) {
val fileVersion = versionOfFile(file.key)
try {
if (fileVersion != null && (newestVersion == null || versionIsNewer(fileVersion, newestVersion))) {
newestVersion = fileVersion
newestFile = file.key
newestFileHash = file.value
}
} catch (_: Throwable) {
}
}
if (newestFile == null || newestFileHash == null)
throw InvalidParameterException("File list is empty")
return Pair(newestFile, newestFileHash)
return verifySignedFileList(fileListBytes.decodeToString()).maxByOrNull { it.version }
}
private suspend fun downloadAndUpdate() = withContext(Dispatchers.IO) {
val receiver = InstallReceiver()
val context = Application.get().applicationContext
val pendingIntent = withContext(Dispatchers.Main) {
ContextCompat.registerReceiver(
context,
receiver,
IntentFilter(receiver.sessionId),
ContextCompat.RECEIVER_NOT_EXPORTED
)
ContextCompat.registerReceiver(context, receiver, IntentFilter(receiver.sessionId), ContextCompat.RECEIVER_NOT_EXPORTED)
PendingIntent.getBroadcast(
context,
0,
@@ -224,23 +252,20 @@ object Updater {
emitProgress(Progress.Rechecking)
val update = checkForUpdates()
val updateVersion = versionOfFile(checkForUpdates().first) ?: throw Exception("No versions returned")
if (!versionIsNewer(updateVersion, CURRENT_VERSION)) {
if (update == null || update.version <= CURRENT_VERSION) {
emitProgress(Progress.Complete)
return@withContext
}
emitProgress(Progress.Downloading(0UL, 0UL), true)
val connection = URL(APK_PATH_URL.format(update.first)).openConnection() as HttpURLConnection
val connection = URL(UPDATE_URL_FMT.format(update.fileName)).openConnection() as HttpURLConnection
connection.setRequestProperty("User-Agent", Application.USER_AGENT)
connection.connect()
if (connection.responseCode != HttpURLConnection.HTTP_OK)
throw IOException("Update could not be fetched: ${connection.responseCode}")
var downloadedByteLen: ULong = 0UL
val totalByteLen =
(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) connection.contentLengthLong else connection.contentLength).toLong()
.toULong()
val totalByteLen = (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) connection.contentLengthLong else connection.contentLength).toLong().toULong()
val fileBytes = ByteArray(1024 * 32 /* 32 KiB */)
val digest = MessageDigest.getInstance("SHA-256")
emitProgress(Progress.Downloading(downloadedByteLen, totalByteLen), true)
@@ -275,7 +300,7 @@ object Updater {
}
emitProgress(Progress.Installing)
if (!digest.digest().contentEquals(update.second.bytes))
if (!digest.digest().contentEquals(update.hash.bytes))
throw SecurityException("Update has invalid hash")
sessionFailure = false
} finally {
@@ -304,8 +329,7 @@ object Updater {
if (sessionId != intent.action)
return
when (val status =
intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE_INVALID)) {
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE_INVALID)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val id = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, 0)
val userIntervention = IntentCompat.getParcelableExtra(intent, Intent.EXTRA_INTENT, Intent::class.java)!!
@@ -327,8 +351,7 @@ object Updater {
context.applicationContext.packageManager.packageInstaller.abandonSession(id)
} catch (_: SecurityException) {
}
val message =
intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) ?: "Installation error $status"
val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) ?: "Installation error $status"
Application.getCoroutineScope().launch {
val e = Exception(message)
Log.e(TAG, "Update failure", e)
@@ -341,24 +364,43 @@ object Updater {
}
fun monitorForUpdates() {
if (installerIsGooglePlay())
val context = Application.get()
if (installerIsGooglePlay(context))
return
Application.getCoroutineScope().launch(Dispatchers.IO) {
if (UserKnobs.updaterNewerVersionSeen.firstOrNull()?.let { versionIsNewer(it, CURRENT_VERSION) } == true)
if (!if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
@Suppress("DEPRECATION")
context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS)
} else {
context.packageManager.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong()))
}.requestedPermissions.contains(Manifest.permission.REQUEST_INSTALL_PACKAGES)
) {
updaterScope.launch {
val update = try {
checkForUpdates()
} catch (_: Throwable) {
null
}
emitProgress(Progress.Corrupt(update?.fileName))
}
return
}
updaterScope.launch {
if (UserKnobs.updaterNewerVersionSeen.firstOrNull()?.let { Version(it) > CURRENT_VERSION } == true)
return@launch
var waitTime = 15
while (true) {
try {
val updateVersion = versionOfFile(checkForUpdates().first) ?: throw IllegalStateException("No versions returned")
if (versionIsNewer(updateVersion, CURRENT_VERSION)) {
Log.i(TAG, "Update available: $updateVersion")
UserKnobs.setUpdaterNewerVersionSeen(updateVersion)
val update = checkForUpdates() ?: continue
if (update.version > CURRENT_VERSION) {
Log.i(TAG, "Update available: ${update.version}")
UserKnobs.setUpdaterNewerVersionSeen(update.version.toString())
return@launch
}
} catch (e: Throwable) {
Log.e(TAG, "Failed to check for updates", e)
} catch (_: Throwable) {
}
delay(waitTime.minutes)
waitTime = 45
@@ -366,36 +408,35 @@ object Updater {
}
UserKnobs.updaterNewerVersionSeen.onEach { ver ->
if (ver != null && versionIsNewer(
ver,
CURRENT_VERSION
) && UserKnobs.updaterNewerVersionConsented.firstOrNull()
?.let { versionIsNewer(it, CURRENT_VERSION) } != true
if (
ver != null &&
Version(ver) > CURRENT_VERSION &&
UserKnobs.updaterNewerVersionConsented.firstOrNull()?.let { Version(it) > CURRENT_VERSION } != true
)
emitProgress(Progress.Available(ver))
}.launchIn(Application.getCoroutineScope())
UserKnobs.updaterNewerVersionConsented.onEach { ver ->
if (ver != null && versionIsNewer(ver, CURRENT_VERSION))
downloadAndUpdateWrapErrors()
if (ver != null && Version(ver) > CURRENT_VERSION)
updaterScope.launch {
downloadAndUpdateWrapErrors()
}
}.launchIn(Application.getCoroutineScope())
}
fun installer(): String {
val context = Application.get().applicationContext
return try {
val packageName = context.packageName
val pm = context.packageManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
pm.getInstallSourceInfo(packageName).installingPackageName ?: ""
} else {
@Suppress("DEPRECATION")
pm.getInstallerPackageName(packageName) ?: ""
}
} catch (_: Throwable) {
""
class AppUpdatedReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED)
return
if (installer(context) != context.packageName)
return
/* TODO: does not work because of restrictions placed on broadcast receivers. */
val start = Intent(context, MainActivity::class.java)
start.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
start.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(start)
}
}
fun installerIsGooglePlay(): Boolean = installer() == "com.android.vending"
}
@@ -13,5 +13,5 @@ object AdminKnobs {
private val restrictions: RestrictionsManager? = Application.get().getSystemService()
val disableConfigExport: Boolean
get() = restrictions?.applicationRestrictions?.getBoolean("disable_config_export", false)
?: false
?: false
}
@@ -31,25 +31,29 @@ object BiometricAuthenticator {
}
fun authenticate(
@StringRes dialogTitleRes: Int,
fragment: Fragment,
callback: (Result) -> Unit
@StringRes dialogTitleRes: Int,
fragment: Fragment,
callback: (Result) -> Unit
) {
val authCallback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
Log.d(TAG, "BiometricAuthentication error: errorCode=$errorCode, msg=$errString")
callback(when (errorCode) {
BiometricPrompt.ERROR_CANCELED, BiometricPrompt.ERROR_USER_CANCELED,
BiometricPrompt.ERROR_NEGATIVE_BUTTON -> {
Result.Cancelled
callback(
when (errorCode) {
BiometricPrompt.ERROR_CANCELED, BiometricPrompt.ERROR_USER_CANCELED,
BiometricPrompt.ERROR_NEGATIVE_BUTTON -> {
Result.Cancelled
}
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))
}
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))
})
)
}
override fun onAuthenticationFailed() {
@@ -64,9 +68,9 @@ object BiometricAuthenticator {
}
val biometricPrompt = BiometricPrompt(fragment, { Handler(Looper.getMainLooper()).post(it) }, authCallback)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(fragment.getString(dialogTitleRes))
.setAllowedAuthenticators(allowedAuthenticators)
.build()
.setTitle(fragment.getString(dialogTitleRes))
.setAllowedAuthenticators(allowedAuthenticators)
.build()
if (BiometricManager.from(fragment.requireContext()).canAuthenticate(allowedAuthenticators) == BiometricManager.BIOMETRIC_SUCCESS) {
biometricPrompt.authenticate(promptInfo)
} else {
@@ -46,9 +46,9 @@ class DownloadsFileSaver(private val context: ComponentActivity) {
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))
?: 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))
?: 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) {
@@ -22,47 +22,47 @@ import java.net.InetAddress
object ErrorMessages {
private val BCE_REASON_MAP = mapOf(
BadConfigException.Reason.INVALID_KEY to R.string.bad_config_reason_invalid_key,
BadConfigException.Reason.INVALID_NUMBER to R.string.bad_config_reason_invalid_number,
BadConfigException.Reason.INVALID_VALUE to R.string.bad_config_reason_invalid_value,
BadConfigException.Reason.MISSING_ATTRIBUTE to R.string.bad_config_reason_missing_attribute,
BadConfigException.Reason.MISSING_SECTION to R.string.bad_config_reason_missing_section,
BadConfigException.Reason.SYNTAX_ERROR to R.string.bad_config_reason_syntax_error,
BadConfigException.Reason.UNKNOWN_ATTRIBUTE to R.string.bad_config_reason_unknown_attribute,
BadConfigException.Reason.UNKNOWN_SECTION to R.string.bad_config_reason_unknown_section
BadConfigException.Reason.INVALID_KEY to R.string.bad_config_reason_invalid_key,
BadConfigException.Reason.INVALID_NUMBER to R.string.bad_config_reason_invalid_number,
BadConfigException.Reason.INVALID_VALUE to R.string.bad_config_reason_invalid_value,
BadConfigException.Reason.MISSING_ATTRIBUTE to R.string.bad_config_reason_missing_attribute,
BadConfigException.Reason.MISSING_SECTION to R.string.bad_config_reason_missing_section,
BadConfigException.Reason.SYNTAX_ERROR to R.string.bad_config_reason_syntax_error,
BadConfigException.Reason.UNKNOWN_ATTRIBUTE to R.string.bad_config_reason_unknown_attribute,
BadConfigException.Reason.UNKNOWN_SECTION to R.string.bad_config_reason_unknown_section
)
private val BE_REASON_MAP = mapOf(
BackendException.Reason.UNKNOWN_KERNEL_MODULE_NAME to R.string.module_version_error,
BackendException.Reason.WG_QUICK_CONFIG_ERROR_CODE to R.string.tunnel_config_error,
BackendException.Reason.TUNNEL_MISSING_CONFIG to R.string.no_config_error,
BackendException.Reason.VPN_NOT_AUTHORIZED to R.string.vpn_not_authorized_error,
BackendException.Reason.UNABLE_TO_START_VPN to R.string.vpn_start_error,
BackendException.Reason.TUN_CREATION_ERROR to R.string.tun_create_error,
BackendException.Reason.GO_ACTIVATION_ERROR_CODE to R.string.tunnel_on_error,
BackendException.Reason.DNS_RESOLUTION_FAILURE to R.string.tunnel_dns_failure
BackendException.Reason.UNKNOWN_KERNEL_MODULE_NAME to R.string.module_version_error,
BackendException.Reason.WG_QUICK_CONFIG_ERROR_CODE to R.string.tunnel_config_error,
BackendException.Reason.TUNNEL_MISSING_CONFIG to R.string.no_config_error,
BackendException.Reason.VPN_NOT_AUTHORIZED to R.string.vpn_not_authorized_error,
BackendException.Reason.UNABLE_TO_START_VPN to R.string.vpn_start_error,
BackendException.Reason.TUN_CREATION_ERROR to R.string.tun_create_error,
BackendException.Reason.GO_ACTIVATION_ERROR_CODE to R.string.tunnel_on_error,
BackendException.Reason.DNS_RESOLUTION_FAILURE to R.string.tunnel_dns_failure
)
private val KFE_FORMAT_MAP = mapOf(
Key.Format.BASE64 to R.string.key_length_explanation_base64,
Key.Format.BINARY to R.string.key_length_explanation_binary,
Key.Format.HEX to R.string.key_length_explanation_hex
Key.Format.BASE64 to R.string.key_length_explanation_base64,
Key.Format.BINARY to R.string.key_length_explanation_binary,
Key.Format.HEX to R.string.key_length_explanation_hex
)
private val KFE_TYPE_MAP = mapOf(
KeyFormatException.Type.CONTENTS to R.string.key_contents_error,
KeyFormatException.Type.LENGTH to R.string.key_length_error
KeyFormatException.Type.CONTENTS to R.string.key_contents_error,
KeyFormatException.Type.LENGTH to R.string.key_length_error
)
private val PE_CLASS_MAP = mapOf(
InetAddress::class.java to R.string.parse_error_inet_address,
InetEndpoint::class.java to R.string.parse_error_inet_endpoint,
InetNetwork::class.java to R.string.parse_error_inet_network,
Int::class.java to R.string.parse_error_integer
InetAddress::class.java to R.string.parse_error_inet_address,
InetEndpoint::class.java to R.string.parse_error_inet_endpoint,
InetNetwork::class.java to R.string.parse_error_inet_network,
Int::class.java to R.string.parse_error_integer
)
private val RSE_REASON_MAP = mapOf(
RootShellException.Reason.NO_ROOT_ACCESS to R.string.error_root,
RootShellException.Reason.SHELL_MARKER_COUNT_ERROR to R.string.shell_marker_count_error,
RootShellException.Reason.SHELL_EXIT_STATUS_READ_ERROR to R.string.shell_exit_status_read_error,
RootShellException.Reason.SHELL_START_ERROR to R.string.shell_start_error,
RootShellException.Reason.CREATE_BIN_DIR_ERROR to R.string.create_bin_dir_error,
RootShellException.Reason.CREATE_TEMP_DIR_ERROR to R.string.create_temp_dir_error
RootShellException.Reason.NO_ROOT_ACCESS to R.string.error_root,
RootShellException.Reason.SHELL_MARKER_COUNT_ERROR to R.string.shell_marker_count_error,
RootShellException.Reason.SHELL_EXIT_STATUS_READ_ERROR to R.string.shell_exit_status_read_error,
RootShellException.Reason.SHELL_START_ERROR to R.string.shell_start_error,
RootShellException.Reason.CREATE_BIN_DIR_ERROR to R.string.create_bin_dir_error,
RootShellException.Reason.CREATE_TEMP_DIR_ERROR to R.string.create_temp_dir_error
)
operator fun get(throwable: Throwable?): String {
@@ -80,21 +80,27 @@ object ErrorMessages {
val explanation = getBadConfigExceptionExplanation(resources, rootCause)
resources.getString(R.string.bad_config_error, reason, context) + explanation
}
rootCause is BackendException -> {
resources.getString(BE_REASON_MAP.getValue(rootCause.reason), *rootCause.format)
}
rootCause is RootShellException -> {
resources.getString(RSE_REASON_MAP.getValue(rootCause.reason), *rootCause.format)
}
rootCause is NotFoundException -> {
resources.getString(R.string.error_no_qr_found)
}
rootCause is ChecksumException -> {
resources.getString(R.string.error_qr_checksum)
}
rootCause.localizedMessage != null -> {
rootCause.localizedMessage!!
}
else -> {
val errorType = rootCause.javaClass.simpleName
resources.getString(R.string.generic_error, errorType)
@@ -102,8 +108,10 @@ object ErrorMessages {
}
}
private fun getBadConfigExceptionExplanation(resources: Resources,
bce: BadConfigException): String {
private fun getBadConfigExceptionExplanation(
resources: Resources,
bce: BadConfigException
): String {
if (bce.cause is KeyFormatException) {
val kfe = bce.cause as KeyFormatException?
if (kfe!!.type == KeyFormatException.Type.LENGTH) return resources.getString(KFE_FORMAT_MAP.getValue(kfe.format))
@@ -120,8 +128,10 @@ object ErrorMessages {
return ""
}
private fun getBadConfigExceptionReason(resources: Resources,
bce: BadConfigException): String {
private fun getBadConfigExceptionReason(
resources: Resources,
bce: BadConfigException
): String {
if (bce.cause is KeyFormatException) {
val kfe = bce.cause as KeyFormatException?
return resources.getString(KFE_TYPE_MAP.getValue(kfe!!.type))
@@ -137,7 +147,8 @@ object ErrorMessages {
var cause = throwable
while (cause.cause != null) {
if (cause is BadConfigException || cause is BackendException ||
cause is RootShellException) break
cause is RootShellException
) break
val nextCause = cause.cause!!
if (nextCause is RemoteException) break
cause = nextCause
@@ -25,7 +25,7 @@ val Any.applicationScope: CoroutineScope
val Preference.activity: SettingsActivity
get() = context as? SettingsActivity
?: throw IllegalStateException("Failed to resolve SettingsActivity")
?: throw IllegalStateException("Failed to resolve SettingsActivity")
val Preference.lifecycleScope: CoroutineScope
get() = activity.lifecycleScope
@@ -55,11 +55,13 @@ class QrCodeFromFileScanner(
multFactor = originalWidth.toFloat() / originalHeight.toFloat()
newWidth = (newHeight * multFactor).toInt()
}
originalWidth > originalHeight -> {
newWidth = scaledSize
multFactor = originalHeight.toFloat() / originalWidth.toFloat()
newHeight = (newWidth * multFactor).toInt()
}
originalHeight == originalWidth -> {
newHeight = scaledSize
newWidth = scaledSize
@@ -15,7 +15,6 @@ import com.wireguard.android.Application
import com.wireguard.android.R
import java.util.Locale
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit
object QuantityFormatter {
fun formatBytes(bytes: Long): String {
@@ -24,7 +24,6 @@ import java.io.BufferedReader
import java.io.ByteArrayInputStream
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
import java.util.ArrayList
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
@@ -135,12 +134,16 @@ object TunnelImporter {
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)
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)
message = context.resources.getQuantityString(
R.plurals.import_partial_success,
tunnels.size + throwables.size,
tunnels.size, tunnels.size + throwables.size
)
messageCallback(message)
}
@@ -55,9 +55,9 @@ class ConfigProxy : Parcelable {
val resolvedPeers: MutableCollection<Peer> = ArrayList()
peers.forEach { resolvedPeers.add(it.resolve()) }
return Config.Builder()
.setInterface(`interface`.resolve())
.addPeers(resolvedPeers)
.build()
.setInterface(`interface`.resolve())
.addPeers(resolvedPeers)
.build()
}
override fun writeToParcel(dest: Parcel, flags: Int) {
@@ -16,8 +16,6 @@ import com.wireguard.config.Attribute
import com.wireguard.config.BadConfigException
import com.wireguard.config.Peer
import java.lang.ref.WeakReference
import java.util.ArrayList
import java.util.LinkedHashSet
class PeerProxy : BaseObservable, Parcelable {
private val dnsRoutes: MutableList<String?> = ArrayList()
@@ -240,24 +238,32 @@ class PeerProxy : BaseObservable, Parcelable {
peerProxy.setTotalPeers(sender.size)
}
override fun onItemRangeChanged(sender: ObservableList<PeerProxy?>,
positionStart: Int, itemCount: Int) {
override fun onItemRangeChanged(
sender: ObservableList<PeerProxy?>,
positionStart: Int, itemCount: Int
) {
// Do nothing.
}
override fun onItemRangeInserted(sender: ObservableList<PeerProxy?>,
positionStart: Int, itemCount: Int) {
override fun onItemRangeInserted(
sender: ObservableList<PeerProxy?>,
positionStart: Int, itemCount: Int
) {
onChanged(sender)
}
override fun onItemRangeMoved(sender: ObservableList<PeerProxy?>,
fromPosition: Int, toPosition: Int,
itemCount: Int) {
override fun onItemRangeMoved(
sender: ObservableList<PeerProxy?>,
fromPosition: Int, toPosition: Int,
itemCount: Int
) {
// Do nothing.
}
override fun onItemRangeRemoved(sender: ObservableList<PeerProxy?>,
positionStart: Int, itemCount: Int) {
override fun onItemRangeRemoved(
sender: ObservableList<PeerProxy?>,
positionStart: Int, itemCount: Int
) {
onChanged(sender)
}
}
@@ -276,12 +282,12 @@ class PeerProxy : BaseObservable, Parcelable {
@JvmField
val CREATOR: Parcelable.Creator<PeerProxy> = PeerProxyCreator()
private val IPV4_PUBLIC_NETWORKS = setOf(
"0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
"64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
"172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
"176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
"192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
"193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
"0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
"64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
"172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
"176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
"192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
"193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
)
private val IPV4_WILDCARD = setOf("0.0.0.0/0")
}
@@ -13,10 +13,12 @@ import com.wireguard.crypto.Key
* InputFilter for entering WireGuard private/public keys encoded with base64.
*/
class KeyInputFilter : InputFilter {
override fun filter(source: CharSequence,
sStart: Int, sEnd: Int,
dest: Spanned,
dStart: Int, dEnd: Int): CharSequence? {
override fun filter(
source: CharSequence,
sStart: Int, sEnd: Int,
dest: Spanned,
dStart: Int, dEnd: Int
): CharSequence? {
var replacement: SpannableStringBuilder? = null
var rIndex = 0
val dLength = dest.length
@@ -26,8 +28,9 @@ class KeyInputFilter : InputFilter {
// Restrict characters to the base64 character set.
// Ensure adding this character does not push the length over the limit.
if ((dIndex + 1 < Key.Format.BASE64.length && isAllowed(c) ||
dIndex + 1 == Key.Format.BASE64.length && c == '=') &&
dLength + (sIndex - sStart) < Key.Format.BASE64.length) {
dIndex + 1 == Key.Format.BASE64.length && c == '=') &&
dLength + (sIndex - sStart) < Key.Format.BASE64.length
) {
++rIndex
} else {
if (replacement == null) replacement = SpannableStringBuilder(source, sStart, sEnd)
@@ -11,10 +11,10 @@ import android.widget.RelativeLayout
import com.wireguard.android.R
class MultiselectableRelativeLayout @JvmOverloads constructor(
context: Context? = null,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
context: Context? = null,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr, defStyleRes) {
private var multiselected = false
@@ -13,10 +13,12 @@ import com.wireguard.android.backend.Tunnel
* InputFilter for entering WireGuard configuration names (Linux interface names).
*/
class NameInputFilter : InputFilter {
override fun filter(source: CharSequence,
sStart: Int, sEnd: Int,
dest: Spanned,
dStart: Int, dEnd: Int): CharSequence? {
override fun filter(
source: CharSequence,
sStart: Int, sEnd: Int,
dest: Spanned,
dStart: Int, dEnd: Int
): CharSequence? {
var replacement: SpannableStringBuilder? = null
var rIndex = 0
val dLength = dest.length
@@ -26,7 +28,8 @@ class NameInputFilter : InputFilter {
// Restrict characters to those valid in interfaces.
// Ensure adding this character does not push the length over the limit.
if (dIndex < Tunnel.NAME_MAX_LENGTH && isAllowed(c) &&
dLength + (sIndex - sStart) < Tunnel.NAME_MAX_LENGTH) {
dLength + (sIndex - sStart) < Tunnel.NAME_MAX_LENGTH
) {
++rIndex
} else {
if (replacement == null) replacement = SpannableStringBuilder(source, sStart, sEnd)
@@ -35,10 +35,10 @@ class SlashDrawable(private val mDrawable: Drawable) : Drawable() {
val radiusX = scale(CORNER_RADIUS, width)
val radiusY = scale(CORNER_RADIUS, height)
updateRect(
scale(LEFT, width),
scale(TOP, height),
scale(RIGHT, width),
scale(TOP + mCurrentSlashLength, height)
scale(LEFT, width),
scale(TOP, height),
scale(RIGHT, width),
scale(TOP + mCurrentSlashLength, height)
)
mPath.reset()
// Draw the slash vertically
@@ -8,8 +8,7 @@
app:state_multiselected="true">
<color android:color="?attr/colorSurfaceVariant" />
</item>
<item
android:state_activated="true">
<item android:state_activated="true">
<color android:color="?attr/colorControlHighlight" />
</item>
</selector>
@@ -3,6 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="com.wireguard.android.widget.NameInputFilter" />
</data>
@@ -24,7 +25,8 @@
android:imeOptions="actionDone"
android:inputType="textNoSuggestions|textVisiblePassword"
app:filter="@{NameInputFilter.newInstance()}">
<requestFocus/>
<requestFocus />
</com.google.android.material.textfield.TextInputEditText>
</com.google.android.material.textfield.TextInputLayout>
@@ -60,11 +60,11 @@
android:src="@mipmap/ic_launcher" />
<TextView
android:layout_marginStart="@dimen/tunnel_list_placeholder_margin"
android:layout_marginEnd="@dimen/tunnel_list_placeholder_margin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="@dimen/tunnel_list_placeholder_margin"
android:layout_marginEnd="@dimen/tunnel_list_placeholder_margin"
android:text="@string/tunnel_list_placeholder"
android:textSize="20sp" />
</LinearLayout>
+1
View File
@@ -0,0 +1 @@
unqualifiedResLocale=en-US
+11
View File
@@ -119,6 +119,7 @@
<string name="error_down">Fehler beim Abschalten des Tunnels: %s</string>
<string name="error_fetching_apps">Fehler beim Abrufen der App-Liste: %s</string>
<string name="error_root">Bitte root-Zugriff anfordern und erneut versuchen</string>
<string name="error_prepare">Fehler beim Vorbereiten des Tunnels: %s</string>
<string name="error_up">Fehler beim Starten des Tunnels: %s</string>
<string name="exclude_private_ips">Private IPs ausschließen</string>
<string name="generate_new_private_key">Neuen privaten Schlüssel generieren</string>
@@ -138,6 +139,8 @@
<string name="key_length_explanation_base64">: WireGuard base64-Schlüssel müssen 44 Zeichen enthalten (32 Bytes)</string>
<string name="key_length_explanation_binary">: WireGuard-Schlüssel müssen 32 Bytes groß sein</string>
<string name="key_length_explanation_hex">: WireGuard Hex-Schlüssel müssen 64 Zeichen (32 Bytes) groß sein</string>
<string name="latest_handshake">Letzter Handshake</string>
<string name="latest_handshake_ago">vor %s</string>
<string name="listen_port">Eingangs-Port</string>
<string name="log_export_error">Konnte Protokoll nicht exportieren: %s</string>
<string name="log_export_subject">WireGuard Android Protokolldatei</string>
@@ -216,6 +219,7 @@
<string name="tunnel_create_success">Tunnel „%s “ erfolgreich erstellt</string>
<string name="tunnel_error_already_exists">Tunnel „%s“ existiert bereits</string>
<string name="tunnel_error_invalid_name">Ungültiger Name</string>
<string name="tunnel_list_placeholder">Füge einen Tunnel mit der Schaltfläche unten hinzu</string>
<string name="tunnel_name">Tunnelname</string>
<string name="tunnel_on_error">Tunnel kann nicht eingeschaltet werden (wgTurnOn gab %d zurück)</string>
<string name="tunnel_dns_failure">DNS-Hostname kann nicht aufgelöst werden: „%s“</string>
@@ -224,6 +228,13 @@
<string name="type_name_go_userspace">Go userspace</string>
<string name="type_name_kernel_module">Kernelmodul</string>
<string name="unknown_error">Unbekannter Fehler</string>
<string name="updater_avalable">Ein Anwendungsupdate ist verfügbar. Bitte jetzt aktualisieren.</string>
<string name="updater_action">Download &amp; Update</string>
<string name="updater_rechecking">Update-Metadaten abrufen…</string>
<string name="updater_download_progress">Update wird heruntergeladen: %1$s / %2$s (%3$.2f%%)</string>
<string name="updater_download_progress_nototal">Update wird heruntergeladen: %s</string>
<string name="updater_installing">Installiere Update…</string>
<string name="updater_failure">Fehler beim Aktualisieren: %s. Versuche es in Kürze erneut…</string>
<string name="version_summary">%1$s backend %2$s</string>
<string name="version_summary_checking">Überprüfe %s Backend-Version</string>
<string name="version_summary_unknown">Unbekannte %s Version</string>
@@ -107,6 +107,9 @@
<string name="tv_select_a_storage_drive">Selecciones un dispositivo de almacenamiento</string>
<string name="tv_no_file_picker">Por favor, instale una herramienta de gestión de archivos para navegar por archivos</string>
<string name="tv_add_tunnel_get_started">Agregue un túnel para empezar</string>
<string name="donate_title">♥ Donar al Proyecto WireGuard</string>
<string name="donate_summary">Todas las contribuciones ayudan</string>
<string name="donate_google_play_disappointment">¡Gracias por apoyar el proyecto WireGuard!\n\nLamentablemente, debido a las políticas de Google, no estamos autorizados a enlazar a la parte de la página web del proyecto donde puedes hacer una donación. ¡Esperemos que puedas averiguar esto!\n\nGracias de nuevo por tu contribución.</string>
<string name="disable_config_export_title">Inhabilitar la exportación de configuración</string>
<string name="disable_config_export_description">Desactivar la exportación de configuración hace que las claves privadas sean menos accesibles</string>
<string name="dns_servers">Servidores DNS</string>
@@ -116,6 +119,7 @@
<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_prepare">Error al preparar el túnel: %s</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>
@@ -135,6 +139,8 @@
<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="latest_handshake">Última comunicación</string>
<string name="latest_handshake_ago">hace %s</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>
@@ -213,6 +219,7 @@
<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 de abajo</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_dns_failure">No se puede resolver el nombre de host DNS: “%s”</string>
+25 -10
View File
@@ -25,7 +25,7 @@
<item quantity="other">%d välistatud rakendust</item>
</plurals>
<plurals name="set_included_applications">
<item quantity="one">%d kaasatud rakendust</item>
<item quantity="one">%d kaasatud rakendus</item>
<item quantity="other">%d kaasatud rakendust</item>
</plurals>
<plurals name="n_excluded_applications">
@@ -79,8 +79,8 @@
<string name="bad_config_reason_unknown_section">Tundmatu sektsioon</string>
<string name="bad_config_reason_value_out_of_range">Väärtus lubatud vahemikust väljas</string>
<string name="bad_extension_error">Faililaiend peab olema .conf või .zip</string>
<string name="error_no_qr_found">QR koodi ei leitud pildilt</string>
<string name="error_qr_checksum">QR koodi kontrollsumma verifitseerimine ebaõnnestus</string>
<string name="error_no_qr_found">QR-koodi ei leitud pildilt</string>
<string name="error_qr_checksum">QR-koodi kontrollsumma verifitseerimine ebaõnnestus</string>
<string name="cancel">Tühista</string>
<string name="config_delete_error">Seadistusfaili %s kustutamine ebaõnnestus</string>
<string name="config_exists_error">\"%s\" seadistus on juba olemas</string>
@@ -90,8 +90,8 @@
<string name="config_save_error">\"%1$s\" seadistuse salvestamine ebaõnnestus: %2$s</string>
<string name="config_save_success">\"%s\" seadistus salvestatud</string>
<string name="create_activity_title">Loo WireGuard tunnel</string>
<string name="create_bin_dir_error">Ei saa luua lokaalset binary kataloogi</string>
<string name="create_downloads_file_error">Ei saa luua faili allalaadimiste kataloogis</string>
<string name="create_bin_dir_error">Lokaalse programmfaili kataloogi tekitamine ebaõnnestus</string>
<string name="create_downloads_file_error">Faili loomine allalaadimiste kataloogis ebaõnnestus</string>
<string name="create_empty">Sisesta käsitsi</string>
<string name="create_from_file">Impordi failist või arhiivist</string>
<string name="create_from_qr_code">Skaneeri QR-koodist</string>
@@ -107,15 +107,20 @@
<string name="tv_select_a_storage_drive">Vali salvestusseade</string>
<string name="tv_no_file_picker">Failide vaatamiseks paigalda failide haldusvahend</string>
<string name="tv_add_tunnel_get_started">Alustamiseks lisa uus tunnel</string>
<string name="donate_title">♥ Anneta WireGuard\'i projektile</string>
<string name="donate_summary">Iga panus aitab</string>
<string name="donate_google_play_disappointment">Aitäh, et toetad WireGuard\'i projekti!\n\nKahjuks ei saa me Google\'i eeskirjade tõttu linkida projekti veebilehele, kus saab annetusi teha. Loodetavasti leiad selle ise!\n\n
Aitäh veelkord sinu panuse eest.</string>
<string name="disable_config_export_title">Keela seadistuste eksportimine</string>
<string name="disable_config_export_description">Seadistuste eksportimise keelamine teeb privaatvõtmetele ligipääsu keerulisemaks</string>
<string name="dns_servers">DNS serverid</string>
<string name="dns_servers">DNS-serverid</string>
<string name="dns_search_domains">DNS-i otsingudomeenid</string>
<string name="edit">Muuda</string>
<string name="endpoint">Lõpp-punkt</string>
<string name="error_down">Viga tunneli väljalülitamisel: %s</string>
<string name="error_fetching_apps">Viga rakenduste nimekirja pärimisel: %s</string>
<string name="error_root">Hangi juurkasutaja õigused ja proovi uuesti</string>
<string name="error_prepare">Viga tunneli ettevalmistamisel: %s</string>
<string name="error_up">Viga tunneli sisselülitamisel: %s</string>
<string name="exclude_private_ips">Keela privaatsed IP\'d</string>
<string name="generate_new_private_key">Tekita uus privaatvõti</string>
@@ -135,6 +140,8 @@
<string name="key_length_explanation_base64">: WireGuard base64 võti peab olema 44 sümbolit (32 baiti) pikk</string>
<string name="key_length_explanation_binary">: WireGuard võtmed peavad olema 32 baiti</string>
<string name="key_length_explanation_hex">: WireGuard hex võti peab olema 64 sümbolit (32 baiti) pikk</string>
<string name="latest_handshake">Viimane kätlus</string>
<string name="latest_handshake_ago">%s tagasi</string>
<string name="listen_port">Kuulamisport</string>
<string name="log_export_error">Logifaili eksportimine ebaõnnestus: %s</string>
<string name="log_export_subject">WireGuard Android logifail</string>
@@ -164,7 +171,7 @@
<string name="no_configs_error">Seadistusi ei leitud</string>
<string name="no_tunnels_error">Tunneleid ei ole</string>
<string name="parse_error_generic">string</string>
<string name="parse_error_inet_address">IP aadress</string>
<string name="parse_error_inet_address">IP-aadress</string>
<string name="parse_error_inet_endpoint">lõpp-punkt</string>
<string name="parse_error_inet_network">IP võrk</string>
<string name="parse_error_integer">number</string>
@@ -184,9 +191,9 @@
<string name="save">Salvesta</string>
<string name="select_all">Vali kõik</string>
<string name="settings">Seaded</string>
<string name="shell_exit_status_read_error">Kest, shell, ei saa lugeda väljumise olekut</string>
<string name="shell_marker_count_error">Kest, shell, ootas 4 markerit, saadi %d</string>
<string name="shell_start_error">Kest, shell, ebaõnnestus käivitamisel: %d</string>
<string name="shell_exit_status_read_error">Kest ei saa lugeda väljundstaatust</string>
<string name="shell_marker_count_error">Kest ootas 4 markerit, saadi %d</string>
<string name="shell_start_error">Kesta käivitumine ebaõnnestus: %d</string>
<string name="success_application_will_restart">Tegevus õnnestus. Rakendus taaskäivitub…</string>
<string name="toggle_all">Vaheta kõik</string>
<string name="toggle_error">WireGuard tunneli lülitamine ebaõnnestus: %s</string>
@@ -213,6 +220,7 @@
<string name="tunnel_create_success">Tunnel \"%s\" lisatud</string>
<string name="tunnel_error_already_exists">Tunnel \"%s\" on juba olemas</string>
<string name="tunnel_error_invalid_name">Sobimatu nimi</string>
<string name="tunnel_list_placeholder">Lisa tunnel alloleva nupu abil</string>
<string name="tunnel_name">Tunneli nimi</string>
<string name="tunnel_on_error">Tunneli sisselülitamine ebaõnnestus (wgTurnOn tagastas %d)</string>
<string name="tunnel_dns_failure">Ei saa lahendada DNS hostinime: \"%s\"</string>
@@ -221,6 +229,13 @@
<string name="type_name_go_userspace">Go kasutajamaa</string>
<string name="type_name_kernel_module">Tuumamoodul</string>
<string name="unknown_error">Tundmatu viga</string>
<string name="updater_avalable">Rakenduse uuendus on saadaval. Palun uuenda nüüd.</string>
<string name="updater_action">Laadi alla ja uuenda</string>
<string name="updater_rechecking">Uuenduse andmete laadimine…</string>
<string name="updater_download_progress">Uuenduse allalaadimine: %1$s / %2$s (%3$.2f%%)</string>
<string name="updater_download_progress_nototal">Uuenduse allalaadimine: %s</string>
<string name="updater_installing">Uuenduse paigaldamine…</string>
<string name="updater_failure">Uuendamine ebaõnnestus: %s. Uus katse hetke pärast…</string>
<string name="version_summary">%1$s taustsüsteem %2$s</string>
<string name="version_summary_checking">Kontrollin %s taustsüsteemi versiooni</string>
<string name="version_summary_unknown">Tundmatu %s versioon</string>
+9 -1
View File
@@ -66,6 +66,8 @@
<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="error_no_qr_found">QRコードが見つかりません</string>
<string name="error_qr_checksum">QRコードのチェックサムの確認に失敗しました</string>
<string name="cancel">キャンセル</string>
<string name="config_delete_error">設定ファイル %s を削除できません</string>
<string name="config_exists_error">\"%s\" の定義はすでに存在します</string>
@@ -91,7 +93,9 @@
<string name="tv_delete">削除するトンネルを選択</string>
<string name="tv_select_a_storage_drive">ストレージを選択</string>
<string name="tv_no_file_picker">ファイルを参照するにはファイル管理アプリをインストールしてください</string>
<string name="tv_add_tunnel_get_started">Add a tunnel to get started</string>
<string name="tv_add_tunnel_get_started">トンネルを追加して開始する</string>
<string name="donate_title">♥ WireGuard プロジェクトに寄付する</string>
<string name="donate_summary">すべての貢献が役立ちます</string>
<string name="disable_config_export_title">設定のエクスポートを無効にする</string>
<string name="disable_config_export_description">設定のエクスポートを無効にすると、秘密鍵にアクセスされにくくなります</string>
<string name="dns_servers">DNS サーバ</string>
@@ -101,6 +105,7 @@
<string name="error_down">トンネル停止時エラー: %s</string>
<string name="error_fetching_apps">アプリ一覧取得エラー: %s</string>
<string name="error_root">root 権限を取得して再試行してください</string>
<string name="error_prepare">トンネル準備中エラー: %s</string>
<string name="error_up">トンネル起動時エラー: %s</string>
<string name="exclude_private_ips">プライベート IP アドレスを除外</string>
<string name="generate_new_private_key">新しい秘密鍵を生成する</string>
@@ -120,6 +125,8 @@
<string name="key_length_explanation_base64">: WireGuard base64 鍵は44文字 (32バイト) でなければなりません</string>
<string name="key_length_explanation_binary">: WireGuard 鍵は32バイトでなければなりません</string>
<string name="key_length_explanation_hex">: WireGuard hex 鍵は64文字 (32バイト) でなければなりません</string>
<string name="latest_handshake">直近のハンドシェイク</string>
<string name="latest_handshake_ago">%s 前</string>
<string name="listen_port">Listen ポート</string>
<string name="log_export_error">ログをエクスポートできません: %s</string>
<string name="log_export_subject">WireGuard Android ログファイル</string>
@@ -198,6 +205,7 @@
<string name="tunnel_create_success">トンネル \"%s\" を作成しました</string>
<string name="tunnel_error_already_exists">トンネル “%s” はすでに存在します</string>
<string name="tunnel_error_invalid_name">不正な名前</string>
<string name="tunnel_list_placeholder">下のボタンを使用してトンネルを追加</string>
<string name="tunnel_name">トンネル名</string>
<string name="tunnel_on_error">トンネルを有効にできません (wgTurnOn が %d を返却)</string>
<string name="tunnel_dns_failure">DNSホスト名を解決できませんでした: “%s”</string>
+1 -1
View File
@@ -1,5 +1,5 @@
<resources>
<style name="WireGuardTheme" parent="Theme.Material3.Dark">
<item name="colorPrimary">@color/md_theme_dark_primary</item>
<item name="colorOnPrimary">@color/md_theme_dark_onPrimary</item>
+7
View File
@@ -254,6 +254,13 @@
<string name="type_name_go_userspace">Пользовательское пространство Go</string>
<string name="type_name_kernel_module">Модуль ядра</string>
<string name="unknown_error">Неизвестная ошибка</string>
<string name="updater_avalable">Доступно обновление приложения. Пожалуйста, обновите.</string>
<string name="updater_action">Загрузить и установить</string>
<string name="updater_rechecking">Получение метаданных обновления…</string>
<string name="updater_download_progress">Загрузка обновления: %1$s / %2$s (%3$.2f%%)</string>
<string name="updater_download_progress_nototal">Загрузка обновления: %s</string>
<string name="updater_installing">Установка обновления…</string>
<string name="updater_failure">Ошибка обновления: %s. Повторите попытку…</string>
<string name="version_summary">%1$s бэкенд %2$s</string>
<string name="version_summary_checking">Проверка версии бэкэнда %s</string>
<string name="version_summary_unknown">Неизвестная версия %s</string>
@@ -133,6 +133,8 @@
<string name="tv_select_a_storage_drive">Виберіть диск зберігання</string>
<string name="tv_no_file_picker">Будь ласка, встановіть провідник для перегляду файлів</string>
<string name="tv_add_tunnel_get_started">Додайте тунель, щоб почати</string>
<string name="donate_title">♥️ Пожертвуйте на проект WireGuard</string>
<string name="donate_summary">Кожен внесок допомагає</string>
<string name="disable_config_export_title">Вимкнути експорт конфігурації</string>
<string name="disable_config_export_description">Вимкнення експорту налаштувань робить приватні ключі менш доступними</string>
<string name="dns_servers">DNS-сервери</string>
@@ -142,6 +144,7 @@
<string name="error_down">Помилка вимкнення тунелю: %s</string>
<string name="error_fetching_apps">Помилка при отриманні списку додатків: %s</string>
<string name="error_root">Будь ласка, отримайте root-доступ і спробуйте ще раз</string>
<string name="error_prepare">Помилка підготовки тунелю: %s</string>
<string name="error_up">Помилка при увімкненні тунелю: %s</string>
<string name="exclude_private_ips">Виключити приватні IP</string>
<string name="generate_new_private_key">Згенерувати новий приватний ключ</string>
@@ -161,6 +164,8 @@
<string name="key_length_explanation_base64">: Ключі WireGuard base64 повинні мати довжину 44 символи (32 байти)</string>
<string name="key_length_explanation_binary">: Ключі WireGuard повинні мати довжину 32 байти</string>
<string name="key_length_explanation_hex">: hex ключі WireGuard повинні мати довжину 64 символи (32 байти)</string>
<string name="latest_handshake">Останнє рукостискання</string>
<string name="latest_handshake_ago">%s тому</string>
<string name="listen_port">Порт</string>
<string name="log_export_error">Не вдалося експортувати журнал: %s</string>
<string name="log_export_subject">Файл журналу WireGuard</string>
@@ -239,6 +244,7 @@
<string name="tunnel_create_success">Тунель успішно створено “%s”</string>
<string name="tunnel_error_already_exists">Тунель \"%s\" вже існує</string>
<string name="tunnel_error_invalid_name">Неприпустиме ім\'я</string>
<string name="tunnel_list_placeholder">Додайте тунель, використовуючи кнопку нижче</string>
<string name="tunnel_name">Назва тунелю</string>
<string name="tunnel_on_error">Неможливо ввімкнути тунель (wgTurnon returned %d)</string>
<string name="tunnel_dns_failure">Не вдалося знайти DNS хост: “%s</string>
+1
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="AppTheme" parent="AppThemeBase">
<item name="android:statusBarColor">?android:colorBackground</item>
<item name="android:windowLightStatusBar">@bool/light_status_bar</item>
+1
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="AppTheme" parent="AppThemeBase">
<item name="android:statusBarColor">?android:colorBackground</item>
<item name="android:windowLightStatusBar">@bool/light_status_bar</item>
@@ -215,6 +215,13 @@
<string name="type_name_go_userspace">Go userspace</string>
<string name="type_name_kernel_module">Kernel module</string>
<string name="unknown_error">未知错误</string>
<string name="updater_avalable">有可用的应用程序更新。请立即更新。</string>
<string name="updater_action">下载 &amp; 更新</string>
<string name="updater_rechecking">正获取更新元数据…</string>
<string name="updater_download_progress">正在下载更新: %1$s / %2$s (%3$.2f%%)</string>
<string name="updater_download_progress_nototal">正在下载更新: %s</string>
<string name="updater_installing">安装更新中...</string>
<string name="updater_failure">更新失败: %s。不久后将重试…</string>
<string name="version_summary">%1$s backend %2$s</string>
<string name="version_summary_checking">正在检查 %s backend 版本</string>
<string name="version_summary_unknown">未知的 %s 版本</string>
+6
View File
@@ -185,6 +185,10 @@
<string name="private_key">Private key</string>
<string name="public_key">Public key</string>
<string name="qr_code_hint">Tip: generate with `qrencode -t ansiutf8 &lt; tunnel.conf`.</string>
<string name="quick_settings_tile_add_title">Add tile to quick settings panel</string>
<string name="quick_settings_tile_add_summary">The shortcut tile toggles the most recent tunnel</string>
<string name="quick_settings_tile_add_failure">Unable to add shortcut tile: error %d</string>
<string name="quick_settings_tile_action">Toggle tunnel</string>
<string name="restore_on_boot_summary_off">Will not bring up enabled tunnels at boot</string>
<string name="restore_on_boot_summary_on">Will bring up enabled tunnels at boot</string>
<string name="restore_on_boot_title">Restore on boot</string>
@@ -236,6 +240,8 @@
<string name="updater_download_progress_nototal">Downloading update: %s</string>
<string name="updater_installing">Installing update…</string>
<string name="updater_failure">Update failure: %s. Will retry momentarily…</string>
<string name="updater_corrupt">This application is corrupt. Please re-download it.</string>
<string name="updater_corrupt_navigate">Open Website</string>
<string name="version_summary">%1$s backend %2$s</string>
<string name="version_summary_checking">Checking %s backend version</string>
<string name="version_summary_unknown">Unknown %s version</string>
+1 -1
View File
@@ -1,5 +1,5 @@
<resources>
<style name="WireGuardTheme" parent="Theme.Material3.Light">
<item name="colorPrimary">@color/md_theme_light_primary</item>
<item name="colorOnPrimary">@color/md_theme_light_onPrimary</item>
+2 -2
View File
@@ -12,6 +12,7 @@
android:summaryOn="@string/restore_on_boot_summary_on"
android:title="@string/restore_on_boot_title" />
<com.wireguard.android.preference.ZipExporterPreference android:key="zip_exporter" />
<com.wireguard.android.preference.QuickTilePreference android:key="quick_tile" />
<Preference
android:key="log_viewer"
android:singleLineTitle="false"
@@ -40,6 +41,5 @@
android:summaryOff="@string/allow_remote_control_intents_summary_off"
android:summaryOn="@string/allow_remote_control_intents_summary_on"
android:title="@string/allow_remote_control_intents_title" />
<com.wireguard.android.preference.DonatePreference
android:singleLineTitle="false" />
<com.wireguard.android.preference.DonatePreference android:singleLineTitle="false" />
</androidx.preference.PreferenceScreen>
-6
View File
@@ -1,6 +0,0 @@
buildscript {
ext {
wireguardVersionCode = 501
wireguardVersionName = '1.0.20230502'
}
}