mirror of
https://github.com/amnezia-vpn/amneziawg-android.git
synced 2026-07-03 14:05:13 +02:00
Compare commits
295 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0750c29c7f | |||
| 639ee6b5b2 | |||
| f764f2aaa9 | |||
| d2a9c56d9c | |||
| 4c3289aa52 | |||
| 79376bca64 | |||
| b709d36c01 | |||
| 0301c85e2c | |||
| 7e979817e2 | |||
| 13cc4436ca | |||
| bcfbf37ed1 | |||
| 74ff870cf5 | |||
| b4f54e8958 | |||
| d179a3af15 | |||
| 2bac2feeb5 | |||
| 261236c8ce | |||
| 4a7969ef24 | |||
| f670ff22c6 | |||
| cb3194f10a | |||
| f6b2bbf433 | |||
| 56a4862442 | |||
| 20390d65c8 | |||
| 177457e67b | |||
| 8caec4d739 | |||
| fb819b99a4 | |||
| fe82037f06 | |||
| c2aa1b21f8 | |||
| d69415b55a | |||
| 4fae2d1255 | |||
| a300f269f1 | |||
| 961cba3f7c | |||
| 6bc7386bff | |||
| 5fa08f286e | |||
| 35f868733c | |||
| e71b3d2583 | |||
| 755148242c | |||
| 15fea6f02f | |||
| cb2842e8ef | |||
| 106b67d892 | |||
| 996587f792 | |||
| 3a4bf35c77 | |||
| 46b37c0c26 | |||
| 5b5ba88a97 | |||
| ceb3095a0a | |||
| a31f0cf788 | |||
| 1dc74b171c | |||
| 9266487fe5 | |||
| 5d7ce139bc | |||
| ddb6c87ebf | |||
| 8a6f8f73cd | |||
| f4fc15538d | |||
| 938399d881 | |||
| 53ca421a85 | |||
| 32778d1c03 | |||
| a870bf6e04 | |||
| 7a8f708157 | |||
| e729c5dc51 | |||
| 4bf34c49b7 | |||
| 05511d4900 | |||
| 15da17b595 | |||
| b3c43e428f | |||
| 7bec539722 | |||
| a8dfebb086 | |||
| e72b4fc144 | |||
| 03189e7b20 | |||
| 10bb413187 | |||
| 1c814310b9 | |||
| 3fe9e3162f | |||
| 6da6f7886a | |||
| 8c2029870f | |||
| a5031a44a0 | |||
| 44b27fe472 | |||
| 93fb3b345b | |||
| 8b596697b7 | |||
| c536bbb7e9 | |||
| a978aac129 | |||
| eb8cab4110 | |||
| 0a36d9a5e9 | |||
| 309571039d | |||
| d56f2fb1bb | |||
| 9df8e5e239 | |||
| 444a86cc9f | |||
| 382e10e103 | |||
| dc002d77fa | |||
| aaa55c0dcc | |||
| 0ad3781ae5 | |||
| d738161a2e | |||
| 52c2e9cd24 | |||
| 5fd1a32ae4 | |||
| 655a853857 | |||
| 847da23300 | |||
| d5c07374ff | |||
| 3785752364 | |||
| 398d8a1e41 | |||
| dfd8ca6f79 | |||
| 7cff4367d7 | |||
| 9eaed5e745 | |||
| 68350bb4df | |||
| 12be972fcd | |||
| d200437813 | |||
| 3ffe7a5e68 | |||
| 08ff9f5ece | |||
| 4bee579e48 | |||
| a906c478c9 | |||
| 306d0648c6 | |||
| e99ccf9013 | |||
| 59935a12b9 | |||
| a9ec828506 | |||
| eebeece856 | |||
| 746ab00794 | |||
| b877593d55 | |||
| dcd596907a | |||
| 44c2afbfba | |||
| bd1679b7e0 | |||
| ff7d7e0edd | |||
| 2f088938c6 | |||
| 53adb0e9a6 | |||
| 6789c11a7b | |||
| c56065fcfe | |||
| 52a2ae36f6 | |||
| abcb51d2a6 | |||
| 8b9a40b3d7 | |||
| 4b36df504c | |||
| 35fe5bd5f0 | |||
| 79ae85c728 | |||
| 49ac61304e | |||
| d79cdb0d41 | |||
| a3726b07bf | |||
| 80c35a2053 | |||
| 601b58b670 | |||
| 9cf049775f | |||
| 92122e60c6 | |||
| f20d0f0659 | |||
| 9346a63753 | |||
| bab70ab51e | |||
| 2fc0bb1a03 | |||
| dd0ff8fe60 | |||
| 45a179580d | |||
| 0bcee7f9cc | |||
| af10b117b4 | |||
| 7aa7825209 | |||
| 8b7617294e | |||
| 9985b9b08e | |||
| 840d65881e | |||
| c18f6818e8 | |||
| dcd91cad1b | |||
| 898bb679d2 | |||
| 348d430cd3 | |||
| e3d98633fb | |||
| b451920408 | |||
| 1fa15e76e3 | |||
| 8a58270e03 | |||
| e08d873cc3 | |||
| 3748a1da88 | |||
| 9597d719ac | |||
| 041ce2134f | |||
| 4c17fc51e7 | |||
| 6cf7439053 | |||
| 814ab4937d | |||
| 3d088411e2 | |||
| 39e0c861e2 | |||
| d60efcd7c7 | |||
| 4dff537d21 | |||
| 761c2ade4a | |||
| 115a87af32 | |||
| c7ecaeb145 | |||
| 8e2d63db75 | |||
| 3208bac987 | |||
| fc1f2132fe | |||
| 09125e1e31 | |||
| 6b1164ad8d | |||
| c68a5c776b | |||
| f3ac787f5a | |||
| 751be2c469 | |||
| cfb7f1b016 | |||
| 33e07628db | |||
| b1b7d1d90c | |||
| 6cb9548f72 | |||
| 8207ae1e8d | |||
| 48e0e427eb | |||
| 03e95d2dd3 | |||
| 58b14cc650 | |||
| 3fa8e09545 | |||
| 9d76b354f0 | |||
| 1856a50c56 | |||
| ba0f3ece04 | |||
| e8ad75d924 | |||
| b858284b1e | |||
| 3c6e06f8d5 | |||
| 0d33a00e72 | |||
| 830d0992a7 | |||
| e4192ea172 | |||
| 4bbb1a0fcd | |||
| 77b5937fbb | |||
| 3144d36056 | |||
| 521bbb4b4c | |||
| 0a55e10b94 | |||
| d6e5fd9301 | |||
| 2c625f56fd | |||
| 7db0fa915e | |||
| e424765a61 | |||
| 1ca4dbf1a2 | |||
| 065893e31d | |||
| ef70aa88e1 | |||
| 6a11a199ca | |||
| 7b5ceac9f7 | |||
| 9fe0019ec1 | |||
| 84654a0302 | |||
| cd43444d1f | |||
| b7028896c7 | |||
| c1e86acb3c | |||
| 7d31bd2be9 | |||
| 2e573a66a4 | |||
| 8d128cf2e9 | |||
| d5ffa08480 | |||
| 43ce69bef4 | |||
| 44a0f53e58 | |||
| d74b988f75 | |||
| 10e910186e | |||
| 017f420d42 | |||
| bc186fe6ad | |||
| 09b40cdec7 | |||
| d2721f2d7d | |||
| 3095e19e13 | |||
| c547d033c3 | |||
| 4d4764eefb | |||
| d44a83faaa | |||
| 2337fe37be | |||
| c5b71cb484 | |||
| 480c95d4d6 | |||
| 8a45e965eb | |||
| c9717693d0 | |||
| 0fa3fe3d43 | |||
| ed090f7ecb | |||
| f916f96761 | |||
| 79e766c4e1 | |||
| 183273dcf5 | |||
| b3bb7c694b | |||
| 456a74db05 | |||
| fde724a658 | |||
| 574ee5d0bb | |||
| 03a838ba2d | |||
| b00aacbc41 | |||
| 532c33a13b | |||
| 6a7396bc1d | |||
| 870b2bf36d | |||
| 07b69be7bf | |||
| b41640837c | |||
| bee6ebe3b4 | |||
| 5989298d3f | |||
| 0235f19543 | |||
| cd6c2f68ca | |||
| 75252cf9d5 | |||
| 1da714852f | |||
| fe6b788f6b | |||
| fb3fec299f | |||
| d2f435b265 | |||
| 3a163acd6d | |||
| df4bf9b688 | |||
| 0f67a2f194 | |||
| b75946af46 | |||
| b9b188693c | |||
| 9fe008d407 | |||
| e905c355f9 | |||
| 12821fb70d | |||
| 2aaa316280 | |||
| 918d9b8b1f | |||
| 536a6f3f83 | |||
| b9fd3d37f2 | |||
| e0b87c3ff2 | |||
| 48a9fd46a6 | |||
| 8669c01eaa | |||
| 37949ba1ec | |||
| b2bbaf050c | |||
| 4d6837ea53 | |||
| c8ac970d11 | |||
| a3a429bc41 | |||
| 0726b1b4d9 | |||
| defc4f45ff | |||
| a984127e28 | |||
| 85dd303c88 | |||
| 2958144fd0 | |||
| ade8f18a95 | |||
| 46e2e29ead | |||
| 63a395125a | |||
| 6f973afa36 | |||
| 1ad0ef3f61 | |||
| 90bf46e8d3 | |||
| c1c285db86 | |||
| f1b541a1eb | |||
| 585257c995 | |||
| 93f80cdc50 | |||
| a832193010 | |||
| 383659fb8a | |||
| 4725e55090 |
@@ -15,3 +15,4 @@ build/
|
||||
*.iml
|
||||
*.jks
|
||||
keystore.properties
|
||||
gradlew.bat
|
||||
|
||||
Generated
+10
-1
@@ -59,7 +59,16 @@
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
<value>
|
||||
<package name="kotlinx.android.synthetic" withSubpackages="true" static="false" />
|
||||
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="PACKAGES_IMPORT_LAYOUT">
|
||||
<value>
|
||||
<package name="" alias="false" withSubpackages="true" />
|
||||
<package name="java" alias="false" withSubpackages="true" />
|
||||
<package name="javax" alias="false" withSubpackages="true" />
|
||||
<package name="kotlin" alias="false" withSubpackages="true" />
|
||||
<package name="" alias="true" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="10" />
|
||||
|
||||
@@ -12,19 +12,29 @@ $ cd wireguard-android
|
||||
$ ./gradlew assembleRelease
|
||||
```
|
||||
|
||||
macOS users may need [flock(1)](https://github.com/discoteq/flock).
|
||||
|
||||
## Embedding
|
||||
|
||||
The tunnel library is [on JCenter](https://bintray.com/wireguard/wireguard-android/wireguard-android/_latestVersion):
|
||||
The tunnel library is [on JCenter](https://bintray.com/wireguard/wireguard-android/wireguard-android/_latestVersion), alongside [extensive class library documentation](https://javadoc.io/doc/com.wireguard.android/tunnel).
|
||||
|
||||
```
|
||||
implementation 'com.wireguard.android:tunnel:$wireguardTunnelVersion'
|
||||
```
|
||||
|
||||
The library makes use of Java 8 features, so be sure to support those in your gradle configuration:
|
||||
The library makes use of Java 8 features, so be sure to support those in your gradle configuration with desugaring:
|
||||
|
||||
```
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
coreLibraryDesugaringEnabled = true
|
||||
}
|
||||
dependencies {
|
||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.0.10"
|
||||
}
|
||||
```
|
||||
|
||||
## Translating
|
||||
|
||||
Please help us translate the app into several languages on [our translation platform](https://crowdin.com/project/WireGuard).
|
||||
|
||||
+66
-29
@@ -1,47 +1,83 @@
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
}
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
activityVersion = '1.2.0-rc01'
|
||||
agpVersion = '4.1.2'
|
||||
annotationsVersion = '1.1.0'
|
||||
appcompatVersion = '1.1.0'
|
||||
cardviewVersion = '1.0.0'
|
||||
appcompatVersion = '1.2.0'
|
||||
biometricVersion = '1.1.0-rc01'
|
||||
collectionVersion = '1.1.0'
|
||||
coreKtxVersion = '1.2.0'
|
||||
constraintLayoutVersion = '1.1.3'
|
||||
constraintLayoutVersion = '2.0.4'
|
||||
coordinatorLayoutVersion = '1.1.0'
|
||||
agpVersion = '3.6.1'
|
||||
fragmentVersion = '1.2.2'
|
||||
materialComponentsVersion = '1.1.0'
|
||||
coreKtxVersion = '1.3.2'
|
||||
coroutinesVersion = '1.4.2'
|
||||
datastoreVersion = '1.0.0-alpha02'
|
||||
desugarVersion = '1.0.10'
|
||||
fragmentVersion = '1.3.0-rc02'
|
||||
jsr305Version = '3.0.2'
|
||||
kotlinVersion = '1.3.70'
|
||||
preferenceVersion = '1.1.0'
|
||||
streamsupportVersion = '1.7.1'
|
||||
threetenabpVersion = '1.2.2'
|
||||
// ZXING switched minSdk to 24 so we cannot upgrade to 4.0.2 without following suit.
|
||||
// If you choose to upgrade to minSDK 24 then you should also disable Jetifier from
|
||||
// gradle.properties.
|
||||
junitVersion = '4.13.1'
|
||||
kotlinVersion = '1.4.21-2'
|
||||
lifecycleRuntimeKtxVersion = '2.3.0-rc01'
|
||||
materialComponentsVersion = '1.3.0-rc01'
|
||||
preferenceVersion = '1.1.1'
|
||||
zxingEmbeddedVersion = '3.6.0'
|
||||
eddsaVersion = '0.3.0'
|
||||
bintrayPluginVersion = '1.8.4'
|
||||
mavenPluginVersion = '2.1'
|
||||
junitVersion = '4.13'
|
||||
|
||||
groupName = 'com.wireguard.android'
|
||||
}
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:$agpVersion"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
classpath "com.github.dcendents:android-maven-gradle-plugin:$mavenPluginVersion"
|
||||
classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:$bintrayPluginVersion"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "de.undercouch.download" version "4.1.1"
|
||||
}
|
||||
|
||||
task downloadCrowdin(type: Download) {
|
||||
src 'https://crowdin.com/backend/download/project/wireguard.zip'
|
||||
dest file('build/translations.zip')
|
||||
overwrite true
|
||||
}
|
||||
|
||||
task cleanCrowdin(type: Delete) {
|
||||
delete 'ui/src/main/res/values-*/strings.xml'
|
||||
}
|
||||
|
||||
task extractCrowdin(type: Copy, dependsOn: ['downloadCrowdin', 'cleanCrowdin']) {
|
||||
mustRunAfter 'downloadCrowdin'
|
||||
from zipTree(file('build/translations.zip'))
|
||||
into file('build/translations')
|
||||
doFirst {
|
||||
delete 'build/translations'
|
||||
}
|
||||
}
|
||||
|
||||
task crowdin(type: Copy, dependsOn: ['extractCrowdin']) {
|
||||
mustRunAfter 'extractCrowdin'
|
||||
from 'build/translations/wireguard-android/ui/src/main/res'
|
||||
into 'ui/src/main/res/'
|
||||
doLast {
|
||||
delete 'build/translations'
|
||||
delete 'build/translations.zip'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
jcenter() {
|
||||
content {
|
||||
// https://github.com/journeyapps/zxing-android-embedded/issues/606
|
||||
includeModule("com.journeyapps", "zxing-android-embedded")
|
||||
// https://youtrack.jetbrains.com/issue/IDEA-261387
|
||||
includeModule("org.jetbrains.trove4j", "trove4j")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,8 +87,9 @@ task clean(type: Delete) {
|
||||
|
||||
tasks {
|
||||
wrapper {
|
||||
gradleVersion = "6.2.2"
|
||||
gradleVersion = "6.8.2"
|
||||
distributionType = Wrapper.DistributionType.ALL
|
||||
distributionSha256Sum = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,3 +17,14 @@ org.gradle.jvmargs=-Xmx1536m
|
||||
# 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
|
||||
# org.gradle.parallel=true
|
||||
|
||||
# https://jakewharton.com/increased-accuracy-of-aapt2-keep-rules/
|
||||
android.useMinimalKeepRules=true
|
||||
|
||||
# Enable rudimentary R class namespacing where each library only contains
|
||||
# references to the resources it declares instead of declarations plus all
|
||||
# transitive dependency references.
|
||||
android.namespacedRClass=true
|
||||
|
||||
# Suppress warnings for some features that aren't yet stabilized
|
||||
android.suppressUnsupportedOptionWarnings=android.enableR8.fullMode,android.useMinimalKeepRules,android.namespacedRClass,android.suppressUnsupportedOptionWarnings
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+2
-1
@@ -1,5 +1,6 @@
|
||||
#Thu Feb 11 16:09:16 CET 2021
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.2-all.zip
|
||||
|
||||
@@ -82,6 +82,7 @@ esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
@@ -129,6 +130,7 @@ fi
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
set -ex
|
||||
curl -Lo - https://crowdin.com/backend/download/project/wireguard.zip | bsdtar -C ui/src/main/res -x -f - --strip-components 5 wireguard-android
|
||||
find ui/src/main/res -name strings.xml -exec bash -c '[[ $(xmllint --xpath "count(//resources/*)" {}) -ne 0 ]] || rm -rf "$(dirname {})"' \;
|
||||
+25
-6
@@ -4,15 +4,14 @@ version wireguardVersionName
|
||||
group groupName
|
||||
|
||||
android {
|
||||
buildToolsVersion '29.0.3'
|
||||
compileSdkVersion 29
|
||||
compileSdkVersion 30
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
targetSdkVersion 30
|
||||
versionCode wireguardVersionCode
|
||||
versionName wireguardVersionName
|
||||
}
|
||||
@@ -21,20 +20,40 @@ android {
|
||||
path 'tools/CMakeLists.txt'
|
||||
}
|
||||
}
|
||||
libraryVariants.all {
|
||||
it.generateBuildConfigProvider.configure { enabled = false }
|
||||
}
|
||||
testOptions.unitTests.all {
|
||||
testLogging {
|
||||
events 'passed', 'skipped', 'failed', 'standardOut', 'standardError'
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments "-DANDROID_PACKAGE_NAME=${groupName}", "-DGRADLE_USER_HOME=${project.gradle.gradleUserHomeDir}"
|
||||
}
|
||||
}
|
||||
}
|
||||
debug {
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments "-DANDROID_PACKAGE_NAME=${groupName}.debug", "-DGRADLE_USER_HOME=${project.gradle.gradleUserHomeDir}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lintOptions {
|
||||
disable('LongLogTag')
|
||||
disable('NewApi') // Desugaring!
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion"
|
||||
implementation "androidx.annotation:annotation:$annotationsVersion"
|
||||
implementation "androidx.collection:collection:$collectionVersion"
|
||||
implementation "com.google.code.findbugs:jsr305:$jsr305Version"
|
||||
implementation "com.jakewharton.threetenabp:threetenabp:$threetenabpVersion"
|
||||
implementation "net.i2p.crypto:eddsa:$eddsaVersion"
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
}
|
||||
|
||||
|
||||
+54
-48
@@ -1,53 +1,58 @@
|
||||
apply plugin: 'com.github.dcendents.android-maven'
|
||||
apply plugin: 'com.jfrog.bintray'
|
||||
apply plugin: 'maven-publish'
|
||||
apply plugin: 'signing'
|
||||
|
||||
install {
|
||||
repositories.mavenInstaller {
|
||||
pom.project {
|
||||
name 'WireGuard Tunnel Library'
|
||||
description 'Embeddable tunnel library for WireGuard for Android'
|
||||
url 'https://www.wireguard.com/'
|
||||
afterEvaluate {
|
||||
publishing {
|
||||
publications {
|
||||
release(MavenPublication) {
|
||||
groupId = groupName
|
||||
artifactId = 'tunnel'
|
||||
version wireguardVersionName
|
||||
|
||||
packaging 'aar'
|
||||
groupId groupName
|
||||
artifactId 'tunnel'
|
||||
version wireguardVersionName
|
||||
artifact sourcesJar
|
||||
artifact javadocJar
|
||||
|
||||
licenses {
|
||||
license {
|
||||
name 'The Apache Software License, Version 2.0'
|
||||
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
|
||||
distribution 'repo'
|
||||
from components.getByName("release")
|
||||
|
||||
pom {
|
||||
name = 'WireGuard Tunnel Library'
|
||||
description = 'Embeddable tunnel library for WireGuard for Android'
|
||||
url = 'https://www.wireguard.com/'
|
||||
|
||||
licenses {
|
||||
license {
|
||||
name = 'The Apache Software License, Version 2.0'
|
||||
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
|
||||
distribution = 'repo'
|
||||
}
|
||||
}
|
||||
scm {
|
||||
connection = 'scm:git:https://git.zx2c4.com/wireguard-android'
|
||||
developerConnection = 'scm:git:https://git.zx2c4.com/wireguard-android'
|
||||
url = 'https://git.zx2c4.com/wireguard-android'
|
||||
}
|
||||
developers {
|
||||
organization {
|
||||
name = 'WireGuard'
|
||||
url = 'https://www.wireguard.com/'
|
||||
}
|
||||
developer {
|
||||
name = 'WireGuard'
|
||||
email = 'team@wireguard.com'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
scm {
|
||||
connection 'scm:git:https://git.zx2c4.com/wireguard-android'
|
||||
url 'https://git.zx2c4.com/wireguard-android'
|
||||
}
|
||||
organization {
|
||||
name 'WireGuard'
|
||||
url 'https://www.wireguard.com/'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bintray {
|
||||
user = hasProperty('BINTRAY_USER') ? getProperty('BINTRAY_USER') : System.getenv('BINTRAY_USER')
|
||||
key = hasProperty('BINTRAY_KEY') ? getProperty('BINTRAY_KEY') : System.getenv('BINTRAY_KEY')
|
||||
|
||||
configurations = [ 'archives' ]
|
||||
|
||||
pkg {
|
||||
repo = 'wireguard-android'
|
||||
name = 'wireguard-android'
|
||||
userOrg = 'wireguard'
|
||||
licenses = [ 'Apache-2.0' ]
|
||||
vcsUrl = 'https://git.zx2c4.com/wireguard-android'
|
||||
publish = true
|
||||
|
||||
version {
|
||||
name = wireguardVersionName
|
||||
repositories {
|
||||
maven {
|
||||
name = "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')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,9 +73,10 @@ android.libraryVariants.all { variant ->
|
||||
archiveClassifier = 'sources'
|
||||
from android.sourceSets.main.java.srcDirs
|
||||
}
|
||||
artifacts {
|
||||
archives sourcesJar
|
||||
archives javadocJar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signing {
|
||||
useGpgCmd()
|
||||
sign publishing.publications
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.wireguard.android.tunnel">
|
||||
|
||||
<application>
|
||||
<service
|
||||
<service
|
||||
android:name="com.wireguard.android.backend.GoBackend$VpnService"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
<intent-filter>
|
||||
|
||||
@@ -30,6 +30,7 @@ public interface Backend {
|
||||
*
|
||||
* @param tunnel The tunnel to examine the state of.
|
||||
* @return The state of the tunnel.
|
||||
* @throws Exception Exception raised when retrieving tunnel's state.
|
||||
*/
|
||||
Tunnel.State getState(Tunnel tunnel) throws Exception;
|
||||
|
||||
@@ -39,6 +40,7 @@ public interface Backend {
|
||||
*
|
||||
* @param tunnel The tunnel to retrieve statistics for.
|
||||
* @return The statistics for the tunnel.
|
||||
* @throws Exception Exception raised when retrieving statistics.
|
||||
*/
|
||||
Statistics getStatistics(Tunnel tunnel) throws Exception;
|
||||
|
||||
@@ -46,7 +48,7 @@ public interface Backend {
|
||||
* Determine version of underlying backend.
|
||||
*
|
||||
* @return The version of the backend.
|
||||
* @throws Exception
|
||||
* @throws Exception Exception raised while retrieving version.
|
||||
*/
|
||||
String getVersion() throws Exception;
|
||||
|
||||
@@ -59,6 +61,7 @@ public interface Backend {
|
||||
* {@code TOGGLE}.
|
||||
* @param config The configuration for this tunnel, may be null if state is {@code DOWN}.
|
||||
* @return The updated state of the tunnel.
|
||||
* @throws Exception Exception raised while changing state.
|
||||
*/
|
||||
Tunnel.State setState(Tunnel tunnel, Tunnel.State state, @Nullable Config config) throws Exception;
|
||||
}
|
||||
|
||||
@@ -7,23 +7,47 @@ package com.wireguard.android.backend;
|
||||
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
/**
|
||||
* A subclass of {@link Exception} that encapsulates the reasons for a failure originating in
|
||||
* implementations of {@link Backend}.
|
||||
*/
|
||||
@NonNullForAll
|
||||
public final class BackendException extends Exception {
|
||||
private final Object[] format;
|
||||
private final Reason reason;
|
||||
|
||||
/**
|
||||
* Public constructor for BackendException.
|
||||
*
|
||||
* @param reason The {@link Reason} which caused this exception to be thrown
|
||||
* @param format Format string values used when converting exceptions to user-facing strings.
|
||||
*/
|
||||
public BackendException(final Reason reason, final Object... format) {
|
||||
this.reason = reason;
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the format string values associated with the instance.
|
||||
*
|
||||
* @return Array of {@link Object} for string formatting purposes
|
||||
*/
|
||||
public Object[] getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reason for this exception.
|
||||
*
|
||||
* @return Associated {@link Reason} for this exception.
|
||||
*/
|
||||
public Reason getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum class containing all known reasons for why a {@link BackendException} might be thrown.
|
||||
*/
|
||||
public enum Reason {
|
||||
UNKNOWN_KERNEL_MODULE_NAME,
|
||||
WG_QUICK_CONFIG_ERROR_CODE,
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.system.OsConstants;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.backend.BackendException.Reason;
|
||||
@@ -33,20 +34,36 @@ import java.util.concurrent.TimeoutException;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.collection.ArraySet;
|
||||
|
||||
/**
|
||||
* Implementation of {@link Backend} that uses the wireguard-go userspace implementation to provide
|
||||
* WireGuard tunnels.
|
||||
*/
|
||||
@NonNullForAll
|
||||
public final class GoBackend implements Backend {
|
||||
private static final String TAG = "WireGuard/" + GoBackend.class.getSimpleName();
|
||||
private static final String TAG = "WireGuard/GoBackend";
|
||||
@Nullable private static AlwaysOnCallback alwaysOnCallback;
|
||||
private static GhettoCompletableFuture<VpnService> vpnService = new GhettoCompletableFuture<>();
|
||||
private final Context context;
|
||||
@Nullable private Config currentConfig;
|
||||
@Nullable private Tunnel currentTunnel;
|
||||
private int currentTunnelHandle = -1;
|
||||
|
||||
/**
|
||||
* Public constructor for GoBackend.
|
||||
*
|
||||
* @param context An Android {@link Context}
|
||||
*/
|
||||
public GoBackend(final Context context) {
|
||||
SharedLibraryLoader.loadSharedLibrary(context, "wg-go");
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a {@link AlwaysOnCallback} to be invoked when {@link VpnService} is started by the
|
||||
* system's Always-On VPN mode.
|
||||
*
|
||||
* @param cb Callback to be invoked
|
||||
*/
|
||||
public static void setAlwaysOnCallback(final AlwaysOnCallback cb) {
|
||||
alwaysOnCallback = cb;
|
||||
}
|
||||
@@ -63,6 +80,11 @@ public final class GoBackend implements Backend {
|
||||
|
||||
private static native String wgVersion();
|
||||
|
||||
/**
|
||||
* Method to get the names of running tunnels.
|
||||
*
|
||||
* @return A set of string values denoting names of running tunnels.
|
||||
*/
|
||||
@Override
|
||||
public Set<String> getRunningTunnelNames() {
|
||||
if (currentTunnel != null) {
|
||||
@@ -73,11 +95,23 @@ public final class GoBackend implements Backend {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the associated {@link State} for a given {@link Tunnel}.
|
||||
*
|
||||
* @param tunnel The tunnel to examine the state of.
|
||||
* @return {@link State} associated with the given tunnel.
|
||||
*/
|
||||
@Override
|
||||
public State getState(final Tunnel tunnel) {
|
||||
return currentTunnel == tunnel ? State.UP : State.DOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the associated {@link Statistics} for a given {@link Tunnel}.
|
||||
*
|
||||
* @param tunnel The tunnel to retrieve statistics for.
|
||||
* @return {@link Statistics} associated with the given tunnel.
|
||||
*/
|
||||
@Override
|
||||
public Statistics getStatistics(final Tunnel tunnel) {
|
||||
final Statistics stats = new Statistics();
|
||||
@@ -122,11 +156,26 @@ public final class GoBackend implements Backend {
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version of the underlying wireguard-go library.
|
||||
*
|
||||
* @return {@link String} value of the version of the wireguard-go library.
|
||||
*/
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return wgVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the state of a given {@link Tunnel}, optionally applying a given {@link Config}.
|
||||
*
|
||||
* @param tunnel The tunnel to control the state of.
|
||||
* @param state The new state for this tunnel. Must be {@code UP}, {@code DOWN}, or
|
||||
* {@code TOGGLE}.
|
||||
* @param config The configuration for this tunnel, may be null if state is {@code DOWN}.
|
||||
* @return {@link State} of the tunnel after state changes are applied.
|
||||
* @throws Exception Exception raised while changing tunnel state.
|
||||
*/
|
||||
@Override
|
||||
public State setState(final Tunnel tunnel, State state, @Nullable final Config config) throws Exception {
|
||||
final State originalState = getState(tunnel);
|
||||
@@ -165,8 +214,10 @@ public final class GoBackend implements Backend {
|
||||
throw new BackendException(Reason.VPN_NOT_AUTHORIZED);
|
||||
|
||||
final VpnService service;
|
||||
if (!vpnService.isDone())
|
||||
startVpnService();
|
||||
if (!vpnService.isDone()) {
|
||||
Log.d(TAG, "Requesting to start VpnService");
|
||||
context.startService(new Intent(context, VpnService.class));
|
||||
}
|
||||
|
||||
try {
|
||||
service = vpnService.get(2, TimeUnit.SECONDS);
|
||||
@@ -192,15 +243,28 @@ public final class GoBackend implements Backend {
|
||||
for (final String excludedApplication : config.getInterface().getExcludedApplications())
|
||||
builder.addDisallowedApplication(excludedApplication);
|
||||
|
||||
for (final String includedApplication : config.getInterface().getIncludedApplications())
|
||||
builder.addAllowedApplication(includedApplication);
|
||||
|
||||
for (final InetNetwork addr : config.getInterface().getAddresses())
|
||||
builder.addAddress(addr.getAddress(), addr.getMask());
|
||||
|
||||
for (final InetAddress addr : config.getInterface().getDnsServers())
|
||||
builder.addDnsServer(addr.getHostAddress());
|
||||
|
||||
boolean sawDefaultRoute = false;
|
||||
for (final Peer peer : config.getPeers()) {
|
||||
for (final InetNetwork addr : peer.getAllowedIps())
|
||||
for (final InetNetwork addr : peer.getAllowedIps()) {
|
||||
if (addr.getMask() == 0)
|
||||
sawDefaultRoute = true;
|
||||
builder.addRoute(addr.getAddress(), addr.getMask());
|
||||
}
|
||||
}
|
||||
|
||||
// "Kill-switch" semantics
|
||||
if (!(sawDefaultRoute && config.getPeers().size() == 1)) {
|
||||
builder.allowFamily(OsConstants.AF_INET);
|
||||
builder.allowFamily(OsConstants.AF_INET6);
|
||||
}
|
||||
|
||||
builder.setMtu(config.getInterface().getMtu().orElse(1280));
|
||||
@@ -240,11 +304,10 @@ public final class GoBackend implements Backend {
|
||||
tunnel.onStateChange(state);
|
||||
}
|
||||
|
||||
private void startVpnService() {
|
||||
Log.d(TAG, "Requesting to start VpnService");
|
||||
context.startService(new Intent(context, VpnService.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for {@link GoBackend} that is invoked when {@link VpnService} is started by the
|
||||
* system's Always-On VPN mode.
|
||||
*/
|
||||
public interface AlwaysOnCallback {
|
||||
void alwaysOnTriggered();
|
||||
}
|
||||
@@ -278,6 +341,9 @@ public final class GoBackend implements Backend {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link android.net.VpnService} implementation for {@link GoBackend}
|
||||
*/
|
||||
public static class VpnService extends android.net.VpnService {
|
||||
@Nullable private GoBackend owner;
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ import com.wireguard.util.NonNullForAll;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Class representing transfer statistics for a {@link Tunnel} instance.
|
||||
*/
|
||||
@NonNullForAll
|
||||
public class Statistics {
|
||||
private final Map<Key, Pair<Long, Long>> peerBytes = new HashMap<>();
|
||||
@@ -22,31 +25,72 @@ public class Statistics {
|
||||
Statistics() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a peer and its current data usage to the internal map.
|
||||
*
|
||||
* @param key A WireGuard public key bound to a particular peer
|
||||
* @param rx The received traffic for the {@link com.wireguard.config.Peer} referenced by
|
||||
* the provided {@link Key}. This value is in bytes
|
||||
* @param tx The transmitted traffic for the {@link com.wireguard.config.Peer} referenced by
|
||||
* the provided {@link Key}. This value is in bytes.
|
||||
*/
|
||||
void add(final Key key, final long rx, final long tx) {
|
||||
peerBytes.put(key, Pair.create(rx, tx));
|
||||
lastTouched = SystemClock.elapsedRealtime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the statistics are stale, indicating the need for the {@link Backend} to update them.
|
||||
*
|
||||
* @return boolean indicating if the current statistics instance has stale values.
|
||||
*/
|
||||
public boolean isStale() {
|
||||
return SystemClock.elapsedRealtime() - lastTouched > 900;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the received traffic (in bytes) for the {@link com.wireguard.config.Peer} referenced by
|
||||
* the provided {@link Key}
|
||||
*
|
||||
* @param peer A {@link Key} representing a {@link com.wireguard.config.Peer}.
|
||||
* @return a long representing the number of bytes received by this peer.
|
||||
*/
|
||||
public long peerRx(final Key peer) {
|
||||
if (!peerBytes.containsKey(peer))
|
||||
final Pair<Long, Long> rxTx = peerBytes.get(peer);
|
||||
if (rxTx == null)
|
||||
return 0;
|
||||
return peerBytes.get(peer).first;
|
||||
return rxTx.first;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the transmitted traffic (in bytes) for the {@link com.wireguard.config.Peer} referenced by
|
||||
* the provided {@link Key}
|
||||
*
|
||||
* @param peer A {@link Key} representing a {@link com.wireguard.config.Peer}.
|
||||
* @return a long representing the number of bytes transmitted by this peer.
|
||||
*/
|
||||
public long peerTx(final Key peer) {
|
||||
if (!peerBytes.containsKey(peer))
|
||||
final Pair<Long, Long> rxTx = peerBytes.get(peer);
|
||||
if (rxTx == null)
|
||||
return 0;
|
||||
return peerBytes.get(peer).second;
|
||||
return rxTx.second;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of peers being tracked by this instance.
|
||||
*
|
||||
* @return An array of {@link Key} instances representing WireGuard
|
||||
* {@link com.wireguard.config.Peer}s
|
||||
*/
|
||||
public Key[] peers() {
|
||||
return peerBytes.keySet().toArray(new Key[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total received traffic by all the peers being tracked by this instance
|
||||
*
|
||||
* @return a long representing the number of bytes received by the peers being tracked.
|
||||
*/
|
||||
public long totalRx() {
|
||||
long rx = 0;
|
||||
for (final Pair<Long, Long> val : peerBytes.values()) {
|
||||
@@ -55,6 +99,11 @@ public class Statistics {
|
||||
return rx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total transmitted traffic by all the peers being tracked by this instance
|
||||
*
|
||||
* @return a long representing the number of bytes transmitted by the peers being tracked.
|
||||
*/
|
||||
public long totalTx() {
|
||||
long tx = 0;
|
||||
for (final Pair<Long, Long> val : peerBytes.values()) {
|
||||
|
||||
@@ -36,11 +36,20 @@ public interface Tunnel {
|
||||
*/
|
||||
void onStateChange(State newState);
|
||||
|
||||
/**
|
||||
* Enum class to represent all possible states of a {@link Tunnel}.
|
||||
*/
|
||||
enum State {
|
||||
DOWN,
|
||||
TOGGLE,
|
||||
UP;
|
||||
|
||||
/**
|
||||
* Get the state of a {@link Tunnel}
|
||||
*
|
||||
* @param running boolean indicating if the tunnel is running.
|
||||
* @return State of the tunnel based on whether or not it is running.
|
||||
*/
|
||||
public static State of(final boolean running) {
|
||||
return running ? UP : DOWN;
|
||||
}
|
||||
|
||||
@@ -32,16 +32,15 @@ import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java9.util.stream.Collectors;
|
||||
import java9.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* WireGuard backend that uses {@code wg-quick} to implement tunnel configuration.
|
||||
* Implementation of {@link Backend} that uses the kernel module and {@code wg-quick} to provide
|
||||
* WireGuard tunnels.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public final class WgQuickBackend implements Backend {
|
||||
private static final String TAG = "WireGuard/" + WgQuickBackend.class.getSimpleName();
|
||||
private static final String TAG = "WireGuard/WgQuickBackend";
|
||||
private final File localTemporaryDir;
|
||||
private final RootShell rootShell;
|
||||
private final Map<Tunnel, Config> runningConfigs = new HashMap<>();
|
||||
@@ -67,7 +66,7 @@ public final class WgQuickBackend implements Backend {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
// wg puts all interface names on the same line. Split them into separate elements.
|
||||
return Stream.of(output.get(0).split(" ")).collect(Collectors.toUnmodifiableSet());
|
||||
return Set.of(output.get(0).split(" "));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -10,14 +10,9 @@ import android.system.OsConstants;
|
||||
import android.util.Base64;
|
||||
|
||||
import com.wireguard.android.util.RootShell.RootShellException;
|
||||
import com.wireguard.crypto.Ed25519;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import net.i2p.crypto.eddsa.EdDSAEngine;
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey;
|
||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
|
||||
import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec;
|
||||
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
@@ -28,7 +23,6 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidParameterException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.Signature;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
@@ -37,6 +31,10 @@ import java.util.Map;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Class that implements the logic for downloading and loading signed, prebuilt modules for
|
||||
* WireGuard into the running kernel.
|
||||
*/
|
||||
@NonNullForAll
|
||||
@SuppressWarnings("MagicNumber")
|
||||
public class ModuleLoader {
|
||||
@@ -49,6 +47,15 @@ public class ModuleLoader {
|
||||
private final File tmpDir;
|
||||
private final String userAgent;
|
||||
|
||||
/**
|
||||
* Public constructor for ModuleLoader
|
||||
*
|
||||
* @param context A {@link Context} instance.
|
||||
* @param rootShell A {@link RootShell} instance used to run elevated commands required for module
|
||||
* loading.
|
||||
* @param userAgent A {@link String} that represents the User-Agent string used for connections
|
||||
* to the upstream server.
|
||||
*/
|
||||
public ModuleLoader(final Context context, final RootShell rootShell, final String userAgent) {
|
||||
moduleDir = new File(context.getCacheDir(), "kmod");
|
||||
tmpDir = new File(context.getCacheDir(), "tmp");
|
||||
@@ -56,10 +63,23 @@ public class ModuleLoader {
|
||||
this.userAgent = userAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a WireGuard module is already loaded into the kernel.
|
||||
*
|
||||
* @return boolean indicating if WireGuard is already enabled in the kernel.
|
||||
*/
|
||||
public static boolean isModuleLoaded() {
|
||||
return new File("/sys/module/wireguard").exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the correct WireGuard module for the device
|
||||
*
|
||||
* @return {@link OsConstants}.EXIT_SUCCESS if everything succeeds, ENOENT otherwise.
|
||||
* @throws IOException if the remote hash list was not found or empty.
|
||||
* @throws RootShellException if {@link RootShell} has a failure executing elevated commands.
|
||||
* @throws NoSuchAlgorithmException if SHA256 algorithm is not available in device JDK.
|
||||
*/
|
||||
public Integer download() throws IOException, RootShellException, NoSuchAlgorithmException {
|
||||
final List<String> output = new ArrayList<>();
|
||||
rootShell.run(output, "sha256sum /proc/version|cut -d ' ' -f 1");
|
||||
@@ -119,17 +139,28 @@ public class ModuleLoader {
|
||||
return OsConstants.EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the downloaded module. ModuleLoader#download must be called before this.
|
||||
*
|
||||
* @throws IOException if {@link RootShell} has a failure executing elevated commands.
|
||||
* @throws RootShellException if {@link RootShell} has a failure executing elevated commands.
|
||||
*/
|
||||
public void loadModule() throws IOException, RootShellException {
|
||||
rootShell.run(null, String.format("insmod \"%s/wireguard-$(sha256sum /proc/version|cut -d ' ' -f 1).ko\"", moduleDir.getAbsolutePath()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the module might already exist in the app's data.
|
||||
*
|
||||
* @return boolean indicating whether downloadable module might exist already.
|
||||
*/
|
||||
public boolean moduleMightExist() {
|
||||
return moduleDir.exists() && moduleDir.isDirectory();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Map<String, Sha256Digest> verifySignedHashes(final String signifyDigest) {
|
||||
final byte[] publicKeyBytes = Base64.decode(MODULE_PUBLIC_KEY_BASE64, Base64.DEFAULT);
|
||||
byte[] publicKeyBytes = Base64.decode(MODULE_PUBLIC_KEY_BASE64, Base64.DEFAULT);
|
||||
|
||||
if (publicKeyBytes == null || publicKeyBytes.length != 32 + 10 || publicKeyBytes[0] != 'E' || publicKeyBytes[1] != 'd')
|
||||
return null;
|
||||
@@ -140,26 +171,17 @@ public class ModuleLoader {
|
||||
if (!lines[0].startsWith("untrusted comment: "))
|
||||
return null;
|
||||
|
||||
final byte[] signatureBytes = Base64.decode(lines[1], Base64.DEFAULT);
|
||||
byte[] signatureBytes = Base64.decode(lines[1], Base64.DEFAULT);
|
||||
if (signatureBytes == null || signatureBytes.length != 64 + 10)
|
||||
return null;
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
if (signatureBytes[i] != publicKeyBytes[i])
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final EdDSAParameterSpec parameterSpec = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519);
|
||||
final Signature signature = new EdDSAEngine(MessageDigest.getInstance(parameterSpec.getHashAlgorithm()));
|
||||
final byte[] rawPublicKeyBytes = new byte[32];
|
||||
System.arraycopy(publicKeyBytes, 10, rawPublicKeyBytes, 0, 32);
|
||||
signature.initVerify(new EdDSAPublicKey(new EdDSAPublicKeySpec(rawPublicKeyBytes, parameterSpec)));
|
||||
signature.update(lines[2].getBytes(StandardCharsets.UTF_8));
|
||||
if (!signature.verify(signatureBytes, 10, 64))
|
||||
return null;
|
||||
} catch (final Exception ignored) {
|
||||
publicKeyBytes = Arrays.copyOfRange(publicKeyBytes, 10, 10 + 32);
|
||||
signatureBytes = Arrays.copyOfRange(signatureBytes, 10, 10 + 64);
|
||||
if (!Ed25519.verify(lines[2].getBytes(StandardCharsets.UTF_8), signatureBytes, publicKeyBytes))
|
||||
return null;
|
||||
}
|
||||
|
||||
final Map<String, Sha256Digest> hashes = new HashMap<>();
|
||||
for (final String line : lines[2].split("\n")) {
|
||||
|
||||
@@ -29,7 +29,7 @@ import androidx.annotation.Nullable;
|
||||
@NonNullForAll
|
||||
public class RootShell {
|
||||
private static final String SU = "su";
|
||||
private static final String TAG = "WireGuard/" + RootShell.class.getSimpleName();
|
||||
private static final String TAG = "WireGuard/RootShell";
|
||||
|
||||
private final File localBinaryDir;
|
||||
private final File localTemporaryDir;
|
||||
@@ -43,8 +43,11 @@ public class RootShell {
|
||||
public RootShell(final Context context) {
|
||||
localBinaryDir = new File(context.getCodeCacheDir(), "bin");
|
||||
localTemporaryDir = new File(context.getCacheDir(), "tmp");
|
||||
preamble = String.format("export CALLING_PACKAGE=%s PATH=\"%s:$PATH\" TMPDIR='%s'; id -u\n",
|
||||
context.getPackageName(), localBinaryDir, localTemporaryDir);
|
||||
final String packageName = context.getPackageName();
|
||||
if (packageName.contains("'"))
|
||||
throw new RuntimeException("Impossibly invalid package name contains a single quote");
|
||||
preamble = String.format("export CALLING_PACKAGE=%s PATH=\"%s:$PATH\" TMPDIR='%s'; magisk --sqlite \"UPDATE policies SET notification=0, logging=0 WHERE package_name='%s'\" >/dev/null 2>&1; id -u\n",
|
||||
packageName, localBinaryDir, localTemporaryDir, packageName);
|
||||
}
|
||||
|
||||
private static boolean isExecutableInPath(final String name) {
|
||||
@@ -189,6 +192,7 @@ public class RootShell {
|
||||
public static class RootShellException extends Exception {
|
||||
private final Object[] format;
|
||||
private final Reason reason;
|
||||
|
||||
public RootShellException(final Reason reason, final Object... format) {
|
||||
this.reason = reason;
|
||||
this.format = format;
|
||||
|
||||
@@ -27,7 +27,7 @@ import androidx.annotation.RestrictTo.Scope;
|
||||
@NonNullForAll
|
||||
@RestrictTo(Scope.LIBRARY_GROUP)
|
||||
public final class SharedLibraryLoader {
|
||||
private static final String TAG = "WireGuard/" + SharedLibraryLoader.class.getSimpleName();
|
||||
private static final String TAG = "WireGuard/SharedLibraryLoader";
|
||||
|
||||
private SharedLibraryLoader() {
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ public final class ToolsInstaller {
|
||||
new File("/system/bin"),
|
||||
};
|
||||
@Nullable private static final File INSTALL_DIR = getInstallDir();
|
||||
private static final String TAG = "WireGuard/" + ToolsInstaller.class.getSimpleName();
|
||||
private static final String TAG = "WireGuard/ToolsInstaller";
|
||||
|
||||
private final Context context;
|
||||
private final File localBinaryDir;
|
||||
@@ -143,12 +143,12 @@ public final class ToolsInstaller {
|
||||
extract();
|
||||
final StringBuilder script = new StringBuilder("set -ex; ");
|
||||
|
||||
script.append("trap 'rm -rf /sbin/.magisk/img/wireguard' INT TERM EXIT; ");
|
||||
script.append(String.format("rm -rf /sbin/.magisk/img/wireguard/; mkdir -p /sbin/.magisk/img/wireguard%s; ", INSTALL_DIR));
|
||||
script.append("printf 'name=WireGuard Command Line Tools\nversion=1.0\nversionCode=1\nauthor=zx2c4\ndescription=Command line tools for WireGuard\nminMagisk=1500\n' > /sbin/.magisk/img/wireguard/module.prop; ");
|
||||
script.append("touch /sbin/.magisk/img/wireguard/auto_mount; ");
|
||||
script.append("trap 'rm -rf /data/adb/modules/wireguard' INT TERM EXIT; ");
|
||||
script.append(String.format("rm -rf /data/adb/modules/wireguard/; mkdir -p /data/adb/modules/wireguard%s; ", INSTALL_DIR));
|
||||
script.append("printf 'name=WireGuard Command Line Tools\nversion=1.0\nversionCode=1\nauthor=zx2c4\ndescription=Command line tools for WireGuard\nminMagisk=1500\n' > /data/adb/modules/wireguard/module.prop; ");
|
||||
script.append("touch /data/adb/modules/wireguard/auto_mount; ");
|
||||
for (final String name : EXECUTABLES) {
|
||||
final File destination = new File("/sbin/.magisk/img/wireguard" + INSTALL_DIR, name);
|
||||
final File destination = new File("/data/adb/modules/wireguard" + INSTALL_DIR, name);
|
||||
script.append(String.format("cp '%s' '%s'; chmod 755 '%s'; chcon 'u:object_r:system_file:s0' '%s' || true; ",
|
||||
new File(localBinaryDir, name), destination, destination, destination));
|
||||
}
|
||||
@@ -191,7 +191,7 @@ public final class ToolsInstaller {
|
||||
synchronized (lock) {
|
||||
if (installAsMagiskModule == null) {
|
||||
try {
|
||||
installAsMagiskModule = rootShell.run(null, "[ -d /sbin/.magisk/mirror -a -d /sbin/.magisk/img -a ! -f /cache/.disable_magisk ]") == OsConstants.EXIT_SUCCESS;
|
||||
installAsMagiskModule = rootShell.run(null, "[ -d /data/adb/modules -a ! -f /cache/.disable_magisk ]") == OsConstants.EXIT_SUCCESS;
|
||||
} catch (final Exception ignored) {
|
||||
installAsMagiskModule = false;
|
||||
}
|
||||
|
||||
@@ -8,11 +8,10 @@ package com.wireguard.config;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import java9.util.Optional;
|
||||
|
||||
@NonNullForAll
|
||||
public final class Attribute {
|
||||
private static final Pattern LINE_PATTERN = Pattern.compile("(\\w+)\\s*=\\s*([^\\s#][^#]*)");
|
||||
|
||||
@@ -72,6 +72,7 @@ public class BadConfigException extends Exception {
|
||||
DNS("DNS"),
|
||||
ENDPOINT("Endpoint"),
|
||||
EXCLUDED_APPLICATIONS("ExcludedApplications"),
|
||||
INCLUDED_APPLICATIONS("IncludedApplications"),
|
||||
LISTEN_PORT("ListenPort"),
|
||||
MTU("MTU"),
|
||||
PERSISTENT_KEEPALIVE("PersistentKeepalive"),
|
||||
|
||||
@@ -17,10 +17,8 @@ import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -187,7 +185,7 @@ public final class Config {
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
public static final class Builder {
|
||||
// Defaults to an empty set.
|
||||
private final Set<Peer> peers = new LinkedHashSet<>();
|
||||
private final ArrayList<Peer> peers = new ArrayList<>();
|
||||
// No default; must be provided before building.
|
||||
@Nullable private Interface interfaze;
|
||||
|
||||
|
||||
@@ -7,18 +7,17 @@ package com.wireguard.config;
|
||||
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import org.threeten.bp.Duration;
|
||||
import org.threeten.bp.Instant;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java9.util.Optional;
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,13 +20,11 @@ import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java9.util.Lists;
|
||||
import java9.util.Optional;
|
||||
import java9.util.stream.Collectors;
|
||||
import java9.util.stream.StreamSupport;
|
||||
|
||||
/**
|
||||
* Represents the configuration for a WireGuard interface (an [Interface] block). Interfaces must
|
||||
@@ -43,6 +41,7 @@ public final class Interface {
|
||||
private final Set<InetNetwork> addresses;
|
||||
private final Set<InetAddress> dnsServers;
|
||||
private final Set<String> excludedApplications;
|
||||
private final Set<String> includedApplications;
|
||||
private final KeyPair keyPair;
|
||||
private final Optional<Integer> listenPort;
|
||||
private final Optional<Integer> mtu;
|
||||
@@ -52,6 +51,7 @@ public final class Interface {
|
||||
addresses = Collections.unmodifiableSet(new LinkedHashSet<>(builder.addresses));
|
||||
dnsServers = Collections.unmodifiableSet(new LinkedHashSet<>(builder.dnsServers));
|
||||
excludedApplications = Collections.unmodifiableSet(new LinkedHashSet<>(builder.excludedApplications));
|
||||
includedApplications = Collections.unmodifiableSet(new LinkedHashSet<>(builder.includedApplications));
|
||||
keyPair = Objects.requireNonNull(builder.keyPair, "Interfaces must have a private key");
|
||||
listenPort = builder.listenPort;
|
||||
mtu = builder.mtu;
|
||||
@@ -81,6 +81,9 @@ public final class Interface {
|
||||
case "excludedapplications":
|
||||
builder.parseExcludedApplications(attribute.getValue());
|
||||
break;
|
||||
case "includedapplications":
|
||||
builder.parseIncludedApplications(attribute.getValue());
|
||||
break;
|
||||
case "listenport":
|
||||
builder.parseListenPort(attribute.getValue());
|
||||
break;
|
||||
@@ -106,6 +109,7 @@ public final class Interface {
|
||||
return addresses.equals(other.addresses)
|
||||
&& dnsServers.equals(other.dnsServers)
|
||||
&& excludedApplications.equals(other.excludedApplications)
|
||||
&& includedApplications.equals(other.includedApplications)
|
||||
&& keyPair.equals(other.keyPair)
|
||||
&& listenPort.equals(other.listenPort)
|
||||
&& mtu.equals(other.mtu);
|
||||
@@ -141,6 +145,16 @@ public final class Interface {
|
||||
return excludedApplications;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set of applications included exclusively for using the interface.
|
||||
*
|
||||
* @return a set of package names
|
||||
*/
|
||||
public Set<String> getIncludedApplications() {
|
||||
// The collection is already immutable.
|
||||
return includedApplications;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the public/private key pair used by the interface.
|
||||
*
|
||||
@@ -174,6 +188,7 @@ public final class Interface {
|
||||
hash = 31 * hash + addresses.hashCode();
|
||||
hash = 31 * hash + dnsServers.hashCode();
|
||||
hash = 31 * hash + excludedApplications.hashCode();
|
||||
hash = 31 * hash + includedApplications.hashCode();
|
||||
hash = 31 * hash + keyPair.hashCode();
|
||||
hash = 31 * hash + listenPort.hashCode();
|
||||
hash = 31 * hash + mtu.hashCode();
|
||||
@@ -206,13 +221,13 @@ public final class Interface {
|
||||
if (!addresses.isEmpty())
|
||||
sb.append("Address = ").append(Attribute.join(addresses)).append('\n');
|
||||
if (!dnsServers.isEmpty()) {
|
||||
final List<String> dnsServerStrings = StreamSupport.stream(dnsServers)
|
||||
.map(InetAddress::getHostAddress)
|
||||
.collect(Collectors.toUnmodifiableList());
|
||||
final List<String> dnsServerStrings = dnsServers.stream().map(InetAddress::getHostAddress).collect(Collectors.toList());
|
||||
sb.append("DNS = ").append(Attribute.join(dnsServerStrings)).append('\n');
|
||||
}
|
||||
if (!excludedApplications.isEmpty())
|
||||
sb.append("ExcludedApplications = ").append(Attribute.join(excludedApplications)).append('\n');
|
||||
if (!includedApplications.isEmpty())
|
||||
sb.append("IncludedApplications = ").append(Attribute.join(includedApplications)).append('\n');
|
||||
listenPort.ifPresent(lp -> sb.append("ListenPort = ").append(lp).append('\n'));
|
||||
mtu.ifPresent(m -> sb.append("MTU = ").append(m).append('\n'));
|
||||
sb.append("PrivateKey = ").append(keyPair.getPrivateKey().toBase64()).append('\n');
|
||||
@@ -240,6 +255,8 @@ public final class Interface {
|
||||
private final Set<InetAddress> dnsServers = new LinkedHashSet<>();
|
||||
// Defaults to an empty set.
|
||||
private final Set<String> excludedApplications = new LinkedHashSet<>();
|
||||
// Defaults to an empty set.
|
||||
private final Set<String> includedApplications = new LinkedHashSet<>();
|
||||
// No default; must be provided before building.
|
||||
@Nullable private KeyPair keyPair;
|
||||
// Defaults to not present.
|
||||
@@ -271,6 +288,9 @@ public final class Interface {
|
||||
if (keyPair == null)
|
||||
throw new BadConfigException(Section.INTERFACE, Location.PRIVATE_KEY,
|
||||
Reason.MISSING_ATTRIBUTE, null);
|
||||
if (!includedApplications.isEmpty() && !excludedApplications.isEmpty())
|
||||
throw new BadConfigException(Section.INTERFACE, Location.INCLUDED_APPLICATIONS,
|
||||
Reason.INVALID_KEY, null);
|
||||
return new Interface(this);
|
||||
}
|
||||
|
||||
@@ -284,6 +304,16 @@ public final class Interface {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder includeApplication(final String application) {
|
||||
includedApplications.add(application);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder includeApplications(final Collection<String> applications) {
|
||||
includedApplications.addAll(applications);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder parseAddresses(final CharSequence addresses) throws BadConfigException {
|
||||
try {
|
||||
for (final String address : Attribute.split(addresses))
|
||||
@@ -305,7 +335,11 @@ public final class Interface {
|
||||
}
|
||||
|
||||
public Builder parseExcludedApplications(final CharSequence apps) {
|
||||
return excludeApplications(Lists.of(Attribute.split(apps)));
|
||||
return excludeApplications(List.of(Attribute.split(apps)));
|
||||
}
|
||||
|
||||
public Builder parseIncludedApplications(final CharSequence apps) {
|
||||
return includeApplications(List.of(Attribute.split(apps)));
|
||||
}
|
||||
|
||||
public Builder parseListenPort(final String listenPort) throws BadConfigException {
|
||||
|
||||
@@ -17,10 +17,10 @@ import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java9.util.Optional;
|
||||
|
||||
/**
|
||||
* Represents the configuration for a WireGuard peer (a [Peer] block). Peers must have a public key,
|
||||
|
||||
@@ -13,7 +13,7 @@ import java.util.Arrays;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Implementation of the Curve25519 elliptic curve algorithm.
|
||||
* Implementation of Curve25519 ECDH.
|
||||
* <p>
|
||||
* This implementation was imported to WireGuard from noise-java:
|
||||
* https://github.com/rweather/noise-java
|
||||
@@ -28,7 +28,7 @@ import androidx.annotation.Nullable;
|
||||
*/
|
||||
@SuppressWarnings({"MagicNumber", "NonConstantFieldWithUpperCaseName", "SuspiciousNameCombination"})
|
||||
@NonNullForAll
|
||||
final class Curve25519 {
|
||||
public final class Curve25519 {
|
||||
// Numbers modulo 2^255 - 19 are broken up into ten 26-bit words.
|
||||
private static final int NUM_LIMBS_255BIT = 10;
|
||||
private static final int NUM_LIMBS_510BIT = 20;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,9 +18,11 @@ import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class BadConfigExceptionTest {
|
||||
private static final Map<String, InputStream> CONFIG_MAP = new HashMap<>();
|
||||
private static final String[] CONFIG_NAMES = {
|
||||
"invalid-key",
|
||||
"invalid-number",
|
||||
@@ -31,14 +33,6 @@ public class BadConfigExceptionTest {
|
||||
"unknown-attribute",
|
||||
"unknown-section"
|
||||
};
|
||||
private static final Map<String, InputStream> CONFIG_MAP = new HashMap<>();
|
||||
|
||||
@BeforeClass
|
||||
public static void readConfigs() {
|
||||
for (final String config: CONFIG_NAMES) {
|
||||
CONFIG_MAP.put(config, BadConfigExceptionTest.class.getClassLoader().getResourceAsStream(config + ".conf"));
|
||||
}
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void closeStreams() {
|
||||
@@ -50,6 +44,13 @@ public class BadConfigExceptionTest {
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
public static void readConfigs() {
|
||||
for (final String config : CONFIG_NAMES) {
|
||||
CONFIG_MAP.put(config, BadConfigExceptionTest.class.getClassLoader().getResourceAsStream(config + ".conf"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void throws_correctly_with_INVALID_KEY_reason() {
|
||||
try {
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -15,8 +14,20 @@ import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class ConfigTest {
|
||||
|
||||
@Test(expected = BadConfigException.class)
|
||||
public void invalid_config_throws() throws IOException, BadConfigException {
|
||||
try (final InputStream is = Objects.requireNonNull(getClass().getClassLoader()).getResourceAsStream("broken.conf")) {
|
||||
Config.parse(is);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void valid_config_parses_correctly() throws IOException, ParseException {
|
||||
Config config = null;
|
||||
@@ -35,11 +46,4 @@ public class ConfigTest {
|
||||
assertEquals("Test config's allowed IPs are 0.0.0.0/0 and ::0/0", config.getPeers().get(0).getAllowedIps(), expectedAllowedIps);
|
||||
assertEquals("Test config has one DNS server", 1, config.getInterface().getDnsServers().size());
|
||||
}
|
||||
|
||||
@Test(expected = BadConfigException.class)
|
||||
public void invalid_config_throws() throws IOException, BadConfigException {
|
||||
try (final InputStream is = Objects.requireNonNull(getClass().getClassLoader()).getResourceAsStream("broken.conf")) {
|
||||
Config.parse(is);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ target_link_libraries(libwg-quick.so -ldl)
|
||||
|
||||
file(GLOB WG_SOURCES wireguard-tools/src/*.c ndk-compat/compat.c)
|
||||
add_executable(libwg.so ${WG_SOURCES})
|
||||
target_include_directories(libwg.so PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/uapi/" "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/")
|
||||
target_include_directories(libwg.so PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/uapi/linux/" "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/")
|
||||
target_compile_options(libwg.so PUBLIC -O3 -std=gnu11 -D_GNU_SOURCE -include ${CMAKE_CURRENT_SOURCE_DIR}/ndk-compat/compat.h -DHAVE_VISIBILITY_HIDDEN -DRUNSTATEDIR=\"/data/data/${ANDROID_PACKAGE_NAME}/cache\")
|
||||
|
||||
add_custom_target(libwg-go.so WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/libwg-go" COMMENT "Building wireguard-go" VERBATIM COMMAND make
|
||||
@@ -24,6 +24,7 @@ add_custom_target(libwg-go.so WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/lib
|
||||
ANDROID_LLVM_TRIPLE=${ANDROID_LLVM_TRIPLE}
|
||||
ANDROID_SYSROOT=${ANDROID_SYSROOT}
|
||||
ANDROID_PACKAGE_NAME=${ANDROID_PACKAGE_NAME}
|
||||
GRADLE_USER_HOME=${GRADLE_USER_HOME}
|
||||
CFLAGS=${CMAKE_C_FLAGS}\ -Wno-unused-command-line-argument
|
||||
LDFLAGS=${CMAKE_SHARED_LINKER_FLAGS}\ -fuse-ld=gold
|
||||
DESTDIR=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
|
||||
|
||||
@@ -14,22 +14,38 @@ NDK_GO_ARCH_MAP_mips64 := mips64x
|
||||
|
||||
CLANG_FLAGS := --target=$(ANDROID_LLVM_TRIPLE) --gcc-toolchain=$(ANDROID_TOOLCHAIN_ROOT) --sysroot=$(ANDROID_SYSROOT)
|
||||
export CGO_CFLAGS := $(CLANG_FLAGS) $(CFLAGS)
|
||||
export CGO_LDFLAGS := $(CLANG_FLAGS) $(LDFLAGS)
|
||||
export CGO_LDFLAGS := $(CLANG_FLAGS) $(LDFLAGS) -Wl,-soname=libwg-go.so
|
||||
export CC := $(ANDROID_C_COMPILER)
|
||||
export GOARCH := $(NDK_GO_ARCH_MAP_$(ANDROID_ARCH_NAME))
|
||||
export GOOS := android
|
||||
export CGO_ENABLED := 1
|
||||
|
||||
DESIRED_GO_VERSION := 1.14.1
|
||||
GO_VERSION := 1.15.6
|
||||
GO_PLATFORM := $(shell uname -s | tr '[:upper:]' '[:lower:]')-$(NDK_GO_ARCH_MAP_$(shell uname -m))
|
||||
GO_TARBALL := go$(GO_VERSION).$(GO_PLATFORM).tar.gz
|
||||
GO_HASH_darwin-amd64 := 940a73b45993a3bae5792cf324140dded34af97c548af4864d22fd6d49f3bd9f
|
||||
GO_HASH_linux-amd64 := 3918e6cc85e7eaaa6f859f1bdbaac772e7a825b0eb423c63d3ae68b21f84b844
|
||||
|
||||
default: $(DESTDIR)/libwg-go.so
|
||||
|
||||
$(BUILDDIR)/go-$(DESIRED_GO_VERSION)/.prepared:
|
||||
$(GRADLE_USER_HOME)/caches/golang/$(GO_TARBALL):
|
||||
mkdir -p "$(dir $@)"
|
||||
curl "https://dl.google.com/go/go$(DESIRED_GO_VERSION).$(shell uname -s | tr '[:upper:]' '[:lower:]')-$(NDK_GO_ARCH_MAP_$(shell uname -m)).tar.gz" | tar -C "$(dir $@)" --strip-components=1 -xzf -
|
||||
patch -p1 -f -N -r- -d "$(dir $@)" < goruntime-boottime-over-monotonic.diff
|
||||
touch "$@"
|
||||
flock "$@.lock" -c ' \
|
||||
[ -f "$@" ] && exit 0; \
|
||||
curl -o "$@.tmp" "https://dl.google.com/go/$(GO_TARBALL)" && \
|
||||
echo "$(GO_HASH_$(GO_PLATFORM)) $@.tmp" | sha256sum -c && \
|
||||
mv "$@.tmp" "$@"'
|
||||
|
||||
$(DESTDIR)/libwg-go.so: export PATH := $(BUILDDIR)/go-$(DESIRED_GO_VERSION)/bin/:$(PATH)
|
||||
$(DESTDIR)/libwg-go.so: $(BUILDDIR)/go-$(DESIRED_GO_VERSION)/.prepared go.mod
|
||||
$(BUILDDIR)/go-$(GO_VERSION)/.prepared: $(GRADLE_USER_HOME)/caches/golang/$(GO_TARBALL)
|
||||
mkdir -p "$(dir $@)"
|
||||
flock "$@.lock" -c ' \
|
||||
[ -f "$@" ] && exit 0; \
|
||||
tar -C "$(dir $@)" --strip-components=1 -xzf "$^" && \
|
||||
patch -p1 -f -N -r- -d "$(dir $@)" < goruntime-boottime-over-monotonic.diff && \
|
||||
touch "$@"'
|
||||
|
||||
$(DESTDIR)/libwg-go.so: export PATH := $(BUILDDIR)/go-$(GO_VERSION)/bin/:$(PATH)
|
||||
$(DESTDIR)/libwg-go.so: $(BUILDDIR)/go-$(GO_VERSION)/.prepared go.mod
|
||||
go build -tags linux -ldflags="-X golang.zx2c4.com/wireguard/ipc.socketDirectory=/data/data/$(ANDROID_PACKAGE_NAME)/cache/wireguard" -v -trimpath -o "$@" -buildmode c-shared
|
||||
|
||||
.DELETE_ON_ERROR:
|
||||
|
||||
@@ -10,30 +10,37 @@ package main
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"golang.org/x/sys/unix"
|
||||
"golang.zx2c4.com/wireguard/device"
|
||||
"golang.zx2c4.com/wireguard/ipc"
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
"bytes"
|
||||
"log"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"golang.zx2c4.com/wireguard/conn"
|
||||
"golang.zx2c4.com/wireguard/device"
|
||||
"golang.zx2c4.com/wireguard/ipc"
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
)
|
||||
|
||||
type AndroidLogger struct {
|
||||
level C.int
|
||||
interfaceName string
|
||||
level C.int
|
||||
tag *C.char
|
||||
}
|
||||
|
||||
func (l AndroidLogger) Write(p []byte) (int, error) {
|
||||
C.__android_log_write(l.level, C.CString("WireGuard/GoBackend/"+l.interfaceName), C.CString(string(p)))
|
||||
return len(p), nil
|
||||
func cstring(s string) *C.char {
|
||||
b, err := unix.BytePtrFromString(s)
|
||||
if err != nil {
|
||||
b := [1]C.char{}
|
||||
return &b[0]
|
||||
}
|
||||
return (*C.char)(unsafe.Pointer(b))
|
||||
}
|
||||
|
||||
func (l AndroidLogger) Printf(format string, args ...interface{}) {
|
||||
C.__android_log_write(l.level, l.tag, cstring(fmt.Sprintf(format, args...)))
|
||||
}
|
||||
|
||||
type TunnelHandle struct {
|
||||
@@ -44,7 +51,6 @@ type TunnelHandle struct {
|
||||
var tunnelHandles map[int32]TunnelHandle
|
||||
|
||||
func init() {
|
||||
device.RoamingDisabled = true
|
||||
tunnelHandles = make(map[int32]TunnelHandle)
|
||||
signals := make(chan os.Signal)
|
||||
signal.Notify(signals, unix.SIGUSR2)
|
||||
@@ -54,52 +60,52 @@ func init() {
|
||||
select {
|
||||
case <-signals:
|
||||
n := runtime.Stack(buf, true)
|
||||
if n == len(buf) {
|
||||
n--
|
||||
}
|
||||
buf[n] = 0
|
||||
C.__android_log_write(C.ANDROID_LOG_ERROR, C.CString("WireGuard/GoBackend/Stacktrace"), (*C.char)(unsafe.Pointer(&buf[0])))
|
||||
C.__android_log_write(C.ANDROID_LOG_ERROR, cstring("WireGuard/GoBackend/Stacktrace"), (*C.char)(unsafe.Pointer(&buf[0])))
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
//export wgTurnOn
|
||||
func wgTurnOn(ifnameRef string, tunFd int32, settings string) int32 {
|
||||
interfaceName := string([]byte(ifnameRef))
|
||||
|
||||
func wgTurnOn(interfaceName string, tunFd int32, settings string) int32 {
|
||||
tag := cstring("WireGuard/GoBackend/" + interfaceName)
|
||||
logger := &device.Logger{
|
||||
Debug: log.New(&AndroidLogger{level: C.ANDROID_LOG_DEBUG, interfaceName: interfaceName}, "", 0),
|
||||
Info: log.New(&AndroidLogger{level: C.ANDROID_LOG_INFO, interfaceName: interfaceName}, "", 0),
|
||||
Error: log.New(&AndroidLogger{level: C.ANDROID_LOG_ERROR, interfaceName: interfaceName}, "", 0),
|
||||
Verbosef: AndroidLogger{level: C.ANDROID_LOG_DEBUG, tag: tag}.Printf,
|
||||
Errorf: AndroidLogger{level: C.ANDROID_LOG_ERROR, tag: tag}.Printf,
|
||||
}
|
||||
|
||||
logger.Debug.Println("Debug log enabled")
|
||||
|
||||
tun, name, err := tun.CreateUnmonitoredTUNFromFD(int(tunFd))
|
||||
if err != nil {
|
||||
unix.Close(int(tunFd))
|
||||
logger.Error.Println(err)
|
||||
logger.Errorf("CreateUnmonitoredTUNFromFD: %v", err)
|
||||
return -1
|
||||
}
|
||||
|
||||
logger.Info.Println("Attaching to interface", name)
|
||||
logger.Verbosef("Attaching to interface %v", name)
|
||||
device := device.NewDevice(tun, logger)
|
||||
|
||||
setError := device.IpcSetOperation(bufio.NewReader(strings.NewReader(settings)))
|
||||
if setError != nil {
|
||||
err = device.IpcSet(settings)
|
||||
if err != nil {
|
||||
unix.Close(int(tunFd))
|
||||
logger.Error.Println(setError)
|
||||
logger.Errorf("IpcSet: %v", err)
|
||||
return -1
|
||||
}
|
||||
device.DisableSomeRoamingForBrokenMobileSemantics()
|
||||
|
||||
var uapi net.Listener
|
||||
|
||||
uapiFile, err := ipc.UAPIOpen(name)
|
||||
if err != nil {
|
||||
logger.Error.Println(err)
|
||||
logger.Errorf("UAPIOpen: %v", err)
|
||||
} else {
|
||||
uapi, err = ipc.UAPIListen(name, uapiFile)
|
||||
if err != nil {
|
||||
uapiFile.Close()
|
||||
logger.Error.Println(err)
|
||||
logger.Errorf("UAPIListen: %v", err)
|
||||
} else {
|
||||
go func() {
|
||||
for {
|
||||
@@ -113,8 +119,14 @@ func wgTurnOn(ifnameRef string, tunFd int32, settings string) int32 {
|
||||
}
|
||||
}
|
||||
|
||||
device.Up()
|
||||
logger.Info.Println("Device started")
|
||||
err = device.Up()
|
||||
if err != nil {
|
||||
logger.Errorf("Unable to bring up device: %v", err)
|
||||
uapiFile.Close()
|
||||
device.Close()
|
||||
return -1
|
||||
}
|
||||
logger.Verbosef("Device started")
|
||||
|
||||
var i int32
|
||||
for i = 0; i < math.MaxInt32; i++ {
|
||||
@@ -123,7 +135,9 @@ func wgTurnOn(ifnameRef string, tunFd int32, settings string) int32 {
|
||||
}
|
||||
}
|
||||
if i == math.MaxInt32 {
|
||||
unix.Close(int(tunFd))
|
||||
logger.Errorf("Unable to find empty handle")
|
||||
uapiFile.Close()
|
||||
device.Close()
|
||||
return -1
|
||||
}
|
||||
tunnelHandles[i] = TunnelHandle{device: device, uapi: uapi}
|
||||
@@ -149,7 +163,11 @@ func wgGetSocketV4(tunnelHandle int32) int32 {
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
fd, err := handle.device.PeekLookAtSocketFd4()
|
||||
bind, _ := handle.device.Bind().(conn.PeekLookAtSocketFd)
|
||||
if bind == nil {
|
||||
return -1
|
||||
}
|
||||
fd, err := bind.PeekLookAtSocketFd4()
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
@@ -162,7 +180,11 @@ func wgGetSocketV6(tunnelHandle int32) int32 {
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
fd, err := handle.device.PeekLookAtSocketFd6()
|
||||
bind, _ := handle.device.Bind().(conn.PeekLookAtSocketFd)
|
||||
if bind == nil {
|
||||
return -1
|
||||
}
|
||||
fd, err := bind.PeekLookAtSocketFd6()
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
@@ -175,14 +197,11 @@ func wgGetConfig(tunnelHandle int32) *C.char {
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
settings := new(bytes.Buffer)
|
||||
writer := bufio.NewWriter(settings)
|
||||
err := handle.device.IpcGetOperation(writer)
|
||||
settings, err := handle.device.IpcGet()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
writer.Flush()
|
||||
return C.CString(settings.String())
|
||||
return C.CString(settings)
|
||||
}
|
||||
|
||||
//export wgVersion
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
module golang.zx2c4.com/wireguard/android
|
||||
|
||||
go 1.13
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad // indirect
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa // indirect
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82
|
||||
golang.zx2c4.com/wireguard v0.0.20200121
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210211144856-ef8115f63b22
|
||||
)
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad h1:Jh8cai0fqIK+f6nG0UgPW5wFk8wmiMhM3AyciDBdtQg=
|
||||
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191003212358-c178f38b412c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210105210732-16f7687f5001 h1:/dSxr6gT0FNI1MO5WLJo8mTmItROeOKTkDn+7OwWBos=
|
||||
golang.org/x/sys v0.0.0-20210105210732-16f7687f5001/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.zx2c4.com/wireguard v0.0.20200121 h1:vcswa5Q6f+sylDfjqyrVNNrjsFUUbPsgAQTBCAg/Qf8=
|
||||
golang.zx2c4.com/wireguard v0.0.20200121/go.mod h1:P2HsVp8SKwZEufsnezXZA4GRX/T49/HlU7DGuelXsU4=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210211144856-ef8115f63b22 h1:7Ct1Jps4o4u2hBFWrg8Rt65VmXfMPTeR0OSXxdq8gMk=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210211144856-ef8115f63b22/go.mod h1:r0ExowOoGFfDoLDxx+M9SYbNVsoZ0xviLL+K4f2mt+A=
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
From e44f456f1d0e429e08afed64a161175ff493f3ac Mon Sep 17 00:00:00 2001
|
||||
From 1d1ba1da11afd73008c0e942db7621697055a6b6 Mon Sep 17 00:00:00 2001
|
||||
From: "Jason A. Donenfeld" <Jason@zx2c4.com>
|
||||
Date: Wed, 27 Feb 2019 05:05:44 +0100
|
||||
Date: Tue, 15 Sep 2020 13:39:22 +0200
|
||||
Subject: [PATCH] runtime: use CLOCK_BOOTTIME in nanotime on Linux
|
||||
|
||||
This makes timers account for having expired while a computer was
|
||||
@@ -28,10 +28,10 @@ Change-Id: I7b2a6ca0c5bc5fce57ec0eeafe7b68270b429321
|
||||
8 files changed, 11 insertions(+), 11 deletions(-)
|
||||
|
||||
diff --git a/src/runtime/sys_linux_386.s b/src/runtime/sys_linux_386.s
|
||||
index 1b28098ad9..46b7071ed8 100644
|
||||
index 5b9b638ad7..448ad8b2e6 100644
|
||||
--- a/src/runtime/sys_linux_386.s
|
||||
+++ b/src/runtime/sys_linux_386.s
|
||||
@@ -317,13 +317,13 @@ noswitch:
|
||||
@@ -339,13 +339,13 @@ noswitch:
|
||||
|
||||
LEAL 8(SP), BX // &ts (struct timespec)
|
||||
MOVL BX, 4(SP)
|
||||
@@ -48,10 +48,10 @@ index 1b28098ad9..46b7071ed8 100644
|
||||
INVOKE_SYSCALL
|
||||
|
||||
diff --git a/src/runtime/sys_linux_amd64.s b/src/runtime/sys_linux_amd64.s
|
||||
index 58d3bc54b4..4bb9bde3d0 100644
|
||||
index fe9c6bce85..4836a7c774 100644
|
||||
--- a/src/runtime/sys_linux_amd64.s
|
||||
+++ b/src/runtime/sys_linux_amd64.s
|
||||
@@ -293,7 +293,7 @@ noswitch:
|
||||
@@ -311,7 +311,7 @@ noswitch:
|
||||
MOVQ runtime·vdsoClockgettimeSym(SB), AX
|
||||
CMPQ AX, $0
|
||||
JEQ fallback
|
||||
@@ -61,7 +61,7 @@ index 58d3bc54b4..4bb9bde3d0 100644
|
||||
CALL AX
|
||||
MOVQ 0(SP), AX // sec
|
||||
diff --git a/src/runtime/sys_linux_arm.s b/src/runtime/sys_linux_arm.s
|
||||
index e103da56dc..0b872b90a6 100644
|
||||
index 475f52344c..bb567abcf4 100644
|
||||
--- a/src/runtime/sys_linux_arm.s
|
||||
+++ b/src/runtime/sys_linux_arm.s
|
||||
@@ -11,7 +11,7 @@
|
||||
@@ -73,7 +73,7 @@ index e103da56dc..0b872b90a6 100644
|
||||
|
||||
// for EABI, as we don't support OABI
|
||||
#define SYS_BASE 0x0
|
||||
@@ -345,7 +345,7 @@ noswitch:
|
||||
@@ -366,7 +366,7 @@ noswitch:
|
||||
SUB $24, R13 // Space for results
|
||||
BIC $0x7, R13 // Align for C code
|
||||
|
||||
@@ -83,7 +83,7 @@ index e103da56dc..0b872b90a6 100644
|
||||
MOVW runtime·vdsoClockgettimeSym(SB), R2
|
||||
CMP $0, R2
|
||||
diff --git a/src/runtime/sys_linux_arm64.s b/src/runtime/sys_linux_arm64.s
|
||||
index b9588cec30..e444d50df4 100644
|
||||
index 198a5bacef..9715387f36 100644
|
||||
--- a/src/runtime/sys_linux_arm64.s
|
||||
+++ b/src/runtime/sys_linux_arm64.s
|
||||
@@ -13,7 +13,7 @@
|
||||
@@ -95,7 +95,7 @@ index b9588cec30..e444d50df4 100644
|
||||
|
||||
#define SYS_exit 93
|
||||
#define SYS_read 63
|
||||
@@ -297,7 +297,7 @@ noswitch:
|
||||
@@ -319,7 +319,7 @@ noswitch:
|
||||
BIC $15, R1
|
||||
MOVD R1, RSP
|
||||
|
||||
@@ -105,10 +105,10 @@ index b9588cec30..e444d50df4 100644
|
||||
CBZ R2, fallback
|
||||
|
||||
diff --git a/src/runtime/sys_linux_mips64x.s b/src/runtime/sys_linux_mips64x.s
|
||||
index 723cfe43d9..edd7a195eb 100644
|
||||
index afad056d06..2c9162b903 100644
|
||||
--- a/src/runtime/sys_linux_mips64x.s
|
||||
+++ b/src/runtime/sys_linux_mips64x.s
|
||||
@@ -278,7 +278,7 @@ noswitch:
|
||||
@@ -304,7 +304,7 @@ noswitch:
|
||||
AND $~15, R1 // Align for C code
|
||||
MOVV R1, R29
|
||||
|
||||
@@ -118,10 +118,10 @@ index 723cfe43d9..edd7a195eb 100644
|
||||
|
||||
MOVV runtime·vdsoClockgettimeSym(SB), R25
|
||||
diff --git a/src/runtime/sys_linux_mipsx.s b/src/runtime/sys_linux_mipsx.s
|
||||
index 15893a7a28..f3edf9a83a 100644
|
||||
index fab2ab3892..f9af103594 100644
|
||||
--- a/src/runtime/sys_linux_mipsx.s
|
||||
+++ b/src/runtime/sys_linux_mipsx.s
|
||||
@@ -235,7 +235,7 @@ TEXT runtime·walltime1(SB),NOSPLIT,$8-12
|
||||
@@ -238,7 +238,7 @@ TEXT runtime·walltime1(SB),NOSPLIT,$8-12
|
||||
RET
|
||||
|
||||
TEXT runtime·nanotime1(SB),NOSPLIT,$8-8
|
||||
@@ -131,13 +131,13 @@ index 15893a7a28..f3edf9a83a 100644
|
||||
MOVW $SYS_clock_gettime, R2
|
||||
SYSCALL
|
||||
diff --git a/src/runtime/sys_linux_ppc64x.s b/src/runtime/sys_linux_ppc64x.s
|
||||
index 8629fe3233..2402e2623a 100644
|
||||
index fd69ee70a5..ff6bc8355b 100644
|
||||
--- a/src/runtime/sys_linux_ppc64x.s
|
||||
+++ b/src/runtime/sys_linux_ppc64x.s
|
||||
@@ -233,7 +233,7 @@ fallback:
|
||||
@@ -249,7 +249,7 @@ fallback:
|
||||
JMP finish
|
||||
|
||||
TEXT runtime·nanotime1(SB),NOSPLIT,$16
|
||||
TEXT runtime·nanotime1(SB),NOSPLIT,$16-8
|
||||
- MOVD $1, R3 // CLOCK_MONOTONIC
|
||||
+ MOVD $7, R3 // CLOCK_BOOTTIME
|
||||
|
||||
@@ -157,5 +157,5 @@ index c15a1d5364..f52c4d5098 100644
|
||||
MOVW $SYS_clock_gettime, R1
|
||||
SYSCALL
|
||||
--
|
||||
2.25.1
|
||||
2.28.0
|
||||
|
||||
|
||||
Submodule tunnel/tools/wireguard-tools updated: 828ffc88cd...e8fa0f662f
+28
-34
@@ -10,32 +10,24 @@ group groupName
|
||||
final def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
|
||||
android {
|
||||
buildToolsVersion '29.0.3'
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
}
|
||||
compileSdkVersion 29
|
||||
dataBinding.enabled true
|
||||
compileSdkVersion 30
|
||||
buildFeatures.dataBinding = true
|
||||
buildFeatures.viewBinding = true
|
||||
defaultConfig {
|
||||
applicationId 'com.wireguard.android'
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
targetSdkVersion 30
|
||||
versionCode wireguardVersionCode
|
||||
versionName wireguardVersionName
|
||||
buildConfigField 'int', 'MIN_SDK_VERSION', "$minSdkVersion.apiLevel"
|
||||
}
|
||||
// If the keystore file exists
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
coreLibraryDesugaringEnabled = true
|
||||
}
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
// Initialize a new Properties() object called keystoreProperties.
|
||||
final def keystoreProperties = new Properties()
|
||||
|
||||
// Load your keystore.properties file into the keystoreProperties object.
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
|
||||
signingConfigs {
|
||||
@@ -50,45 +42,47 @@ android {
|
||||
buildTypes {
|
||||
release {
|
||||
if (keystorePropertiesFile.exists()) signingConfig signingConfigs.release
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments "-DANDROID_PACKAGE_NAME=${android.defaultConfig.applicationId}"
|
||||
}
|
||||
}
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||
proguardFiles "proguard-android-optimize.txt", "proguard-rules.pro"
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
versionNameSuffix "-debug"
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments "-DANDROID_PACKAGE_NAME=${android.defaultConfig.applicationId}${applicationIdSuffix}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lintOptions {
|
||||
disable('LongLogTag')
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(":tunnel")
|
||||
implementation "androidx.activity:activity-ktx:$activityVersion"
|
||||
implementation "androidx.annotation:annotation:$annotationsVersion"
|
||||
implementation "androidx.appcompat:appcompat:$appcompatVersion"
|
||||
implementation "androidx.cardview:cardview:$cardviewVersion"
|
||||
implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion"
|
||||
implementation "androidx.coordinatorlayout:coordinatorlayout:$coordinatorLayoutVersion"
|
||||
implementation "androidx.biometric:biometric:$biometricVersion"
|
||||
implementation "androidx.core:core-ktx:$coreKtxVersion"
|
||||
implementation "androidx.databinding:databinding-runtime:$agpVersion"
|
||||
implementation "androidx.fragment:fragment:$fragmentVersion"
|
||||
implementation "androidx.preference:preference:$preferenceVersion"
|
||||
implementation "androidx.fragment:fragment-ktx:$fragmentVersion"
|
||||
implementation "androidx.preference:preference-ktx:$preferenceVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleRuntimeKtxVersion"
|
||||
implementation "androidx.datastore:datastore-preferences:$datastoreVersion"
|
||||
implementation "com.google.android.material:material:$materialComponentsVersion"
|
||||
implementation "com.journeyapps:zxing-android-embedded:$zxingEmbeddedVersion"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
|
||||
implementation "net.sourceforge.streamsupport:android-retrofuture:$streamsupportVersion"
|
||||
implementation "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion"
|
||||
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) {
|
||||
options.compilerArgs << '-Xlint:unchecked'
|
||||
options.deprecation = true
|
||||
}
|
||||
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
-allowaccessmodification
|
||||
-dontpreverify
|
||||
-dontusemixedcaseclassnames
|
||||
-dontskipnonpubliclibraryclasses
|
||||
-verbose
|
||||
|
||||
-keepattributes *Annotation*
|
||||
|
||||
-keepclasseswithmembernames class * {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
-keepclassmembers enum * {
|
||||
public static **[] values();
|
||||
public static ** valueOf(java.lang.String);
|
||||
}
|
||||
|
||||
-keepclassmembers class * implements android.os.Parcelable {
|
||||
public static final ** CREATOR;
|
||||
}
|
||||
|
||||
-keep class androidx.annotation.Keep
|
||||
|
||||
-keep @androidx.annotation.Keep class * {*;}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@androidx.annotation.Keep <methods>;
|
||||
}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@androidx.annotation.Keep <fields>;
|
||||
}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@androidx.annotation.Keep <init>(...);
|
||||
}
|
||||
Vendored
+11
@@ -1,3 +1,14 @@
|
||||
# Squelch all warnings, they're harmless but ProGuard
|
||||
# escalates them as errors.
|
||||
-dontwarn sun.misc.Unsafe
|
||||
|
||||
# Fragment 1.2.4 allows Fragment classes to be obfuscated but
|
||||
# databinding references in XML seem to not be rewritten to
|
||||
# match, so we preserve the names as 1.2.3 did.
|
||||
-if public class ** extends androidx.fragment.app.Fragment
|
||||
-keep public class <1> {
|
||||
public <init>();
|
||||
}
|
||||
|
||||
# Don't obfuscate
|
||||
-dontobfuscate
|
||||
|
||||
@@ -7,9 +7,20 @@
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
|
||||
<permission
|
||||
android:name="${applicationId}.permission.CONTROL_TUNNELS"
|
||||
@@ -21,19 +32,22 @@
|
||||
<application
|
||||
android:name=".Application"
|
||||
android:allowBackup="false"
|
||||
android:banner="@mipmap/banner"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
tools:ignore="UnusedAttribute">
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity
|
||||
android:name=".activity.TunnelToggleActivity"
|
||||
android:theme="@style/NoBackgroundTheme" />
|
||||
|
||||
<activity android:name=".activity.TunnelToggleActivity" android:theme="@style/NoBackgroundTheme"/>
|
||||
<activity android:name=".activity.MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
@@ -41,6 +55,15 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activity.TvMainActivity"
|
||||
android:theme="@style/TvTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activity.SettingsActivity"
|
||||
android:label="@string/settings"
|
||||
@@ -56,6 +79,20 @@
|
||||
android:screenOrientation="fullSensor"
|
||||
tools:replace="screenOrientation" />
|
||||
|
||||
<activity
|
||||
android:name=".activity.LogViewerActivity"
|
||||
android:label="@string/log_viewer_title">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name=".activity.LogViewerActivity$ExportedLogContentProvider"
|
||||
android:authorities="${applicationId}.exported-log"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true" />
|
||||
|
||||
<receiver android:name=".BootShutdownReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.ACTION_SHUTDOWN" />
|
||||
@@ -86,5 +123,16 @@
|
||||
android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||
android:value="false" />
|
||||
</service>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.APP_RESTRICTIONS"
|
||||
android:resource="@xml/app_restrictions" />
|
||||
</application>
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.StrictMode;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.backend.Backend;
|
||||
import com.wireguard.android.backend.GoBackend;
|
||||
import com.wireguard.android.backend.WgQuickBackend;
|
||||
import com.wireguard.android.configStore.FileConfigStore;
|
||||
import com.wireguard.android.model.TunnelManager;
|
||||
import com.wireguard.android.util.AsyncWorker;
|
||||
import com.wireguard.android.util.ExceptionLoggers;
|
||||
import com.wireguard.android.util.ModuleLoader;
|
||||
import com.wireguard.android.util.RootShell;
|
||||
import com.wireguard.android.util.ToolsInstaller;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Locale;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import java9.util.concurrent.CompletableFuture;
|
||||
|
||||
@NonNullForAll
|
||||
public class Application extends android.app.Application implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
public static final String USER_AGENT = String.format(Locale.ENGLISH, "WireGuard/%s (Android %d; %s; %s; %s %s; %s)", BuildConfig.VERSION_NAME, Build.VERSION.SDK_INT, Build.SUPPORTED_ABIS.length > 0 ? Build.SUPPORTED_ABIS[0] : "unknown ABI", Build.BOARD, Build.MANUFACTURER, Build.MODEL, Build.FINGERPRINT);
|
||||
private static final String TAG = "WireGuard/" + Application.class.getSimpleName();
|
||||
@SuppressWarnings("NullableProblems") private static WeakReference<Application> weakSelf;
|
||||
private final CompletableFuture<Backend> futureBackend = new CompletableFuture<>();
|
||||
@SuppressWarnings("NullableProblems") private AsyncWorker asyncWorker;
|
||||
@Nullable private Backend backend;
|
||||
@SuppressWarnings("NullableProblems") private ModuleLoader moduleLoader;
|
||||
@SuppressWarnings("NullableProblems") private RootShell rootShell;
|
||||
@SuppressWarnings("NullableProblems") private SharedPreferences sharedPreferences;
|
||||
@SuppressWarnings("NullableProblems") private ToolsInstaller toolsInstaller;
|
||||
@SuppressWarnings("NullableProblems") private TunnelManager tunnelManager;
|
||||
|
||||
public Application() {
|
||||
weakSelf = new WeakReference<>(this);
|
||||
}
|
||||
|
||||
public static Application get() {
|
||||
return weakSelf.get();
|
||||
}
|
||||
|
||||
public static AsyncWorker getAsyncWorker() {
|
||||
return get().asyncWorker;
|
||||
}
|
||||
|
||||
public static Backend getBackend() {
|
||||
final Application app = get();
|
||||
synchronized (app.futureBackend) {
|
||||
if (app.backend == null) {
|
||||
Backend backend = null;
|
||||
boolean didStartRootShell = false;
|
||||
if (!ModuleLoader.isModuleLoaded() && app.moduleLoader.moduleMightExist()) {
|
||||
try {
|
||||
app.rootShell.start();
|
||||
didStartRootShell = true;
|
||||
app.moduleLoader.loadModule();
|
||||
} catch (final Exception ignored) {
|
||||
}
|
||||
}
|
||||
if (!app.sharedPreferences.getBoolean("disable_kernel_module", false) && ModuleLoader.isModuleLoaded()) {
|
||||
try {
|
||||
if (!didStartRootShell)
|
||||
app.rootShell.start();
|
||||
final WgQuickBackend wgQuickBackend = new WgQuickBackend(app.getApplicationContext(), app.rootShell, app.toolsInstaller);
|
||||
wgQuickBackend.setMultipleTunnels(app.sharedPreferences.getBoolean("multiple_tunnels", false));
|
||||
backend = wgQuickBackend;
|
||||
} catch (final Exception ignored) {
|
||||
}
|
||||
}
|
||||
if (backend == null) {
|
||||
backend = new GoBackend(app.getApplicationContext());
|
||||
GoBackend.setAlwaysOnCallback(() -> {
|
||||
get().tunnelManager.restoreState(true).whenComplete(ExceptionLoggers.D);
|
||||
});
|
||||
}
|
||||
app.backend = backend;
|
||||
}
|
||||
return app.backend;
|
||||
}
|
||||
}
|
||||
|
||||
public static CompletableFuture<Backend> getBackendAsync() {
|
||||
return get().futureBackend;
|
||||
}
|
||||
|
||||
public static ModuleLoader getModuleLoader() {
|
||||
return get().moduleLoader;
|
||||
}
|
||||
|
||||
public static RootShell getRootShell() {
|
||||
return get().rootShell;
|
||||
}
|
||||
|
||||
public static SharedPreferences getSharedPreferences() {
|
||||
return get().sharedPreferences;
|
||||
}
|
||||
|
||||
public static ToolsInstaller getToolsInstaller() {
|
||||
return get().toolsInstaller;
|
||||
}
|
||||
|
||||
public static TunnelManager getTunnelManager() {
|
||||
return get().tunnelManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(final Context context) {
|
||||
super.attachBaseContext(context);
|
||||
|
||||
if (BuildConfig.MIN_SDK_VERSION > Build.VERSION.SDK_INT) {
|
||||
final Intent intent = new Intent(Intent.ACTION_MAIN);
|
||||
intent.addCategory(Intent.CATEGORY_HOME);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
Log.i(TAG, USER_AGENT);
|
||||
super.onCreate();
|
||||
|
||||
asyncWorker = new AsyncWorker(AsyncTask.SERIAL_EXECUTOR, new Handler(Looper.getMainLooper()));
|
||||
rootShell = new RootShell(getApplicationContext());
|
||||
toolsInstaller = new ToolsInstaller(getApplicationContext(), rootShell);
|
||||
moduleLoader = new ModuleLoader(getApplicationContext(), rootShell, USER_AGENT);
|
||||
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
sharedPreferences.getBoolean("dark_theme", false) ?
|
||||
AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);
|
||||
} else {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
|
||||
}
|
||||
|
||||
tunnelManager = new TunnelManager(new FileConfigStore(getApplicationContext()));
|
||||
tunnelManager.onCreate();
|
||||
|
||||
asyncWorker.supplyAsync(Application::getBackend).thenAccept(futureBackend::complete);
|
||||
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) {
|
||||
if ("multiple_tunnels".equals(key) && backend != null && backend instanceof WgQuickBackend)
|
||||
((WgQuickBackend) backend).setMultipleTunnels(sharedPreferences.getBoolean(key, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
|
||||
super.onTerminate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.StrictMode
|
||||
import android.os.StrictMode.ThreadPolicy
|
||||
import android.os.StrictMode.VmPolicy
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.datastore.DataStore
|
||||
import androidx.datastore.preferences.Preferences
|
||||
import androidx.datastore.preferences.createDataStore
|
||||
import com.wireguard.android.backend.Backend
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.wireguard.android.configStore.FileConfigStore
|
||||
import com.wireguard.android.model.TunnelManager
|
||||
import com.wireguard.android.util.ModuleLoader
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.wireguard.android.util.ToolsInstaller
|
||||
import com.wireguard.android.util.UserKnobs
|
||||
import com.wireguard.android.util.applicationScope
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.Locale
|
||||
|
||||
class Application : android.app.Application() {
|
||||
private val futureBackend = CompletableDeferred<Backend>()
|
||||
private val coroutineScope = CoroutineScope(Job() + Dispatchers.Main.immediate)
|
||||
private var backend: Backend? = null
|
||||
private lateinit var moduleLoader: ModuleLoader
|
||||
private lateinit var rootShell: RootShell
|
||||
private lateinit var preferencesDataStore: DataStore<Preferences>
|
||||
private lateinit var toolsInstaller: ToolsInstaller
|
||||
private lateinit var tunnelManager: TunnelManager
|
||||
|
||||
override fun attachBaseContext(context: Context) {
|
||||
super.attachBaseContext(context)
|
||||
if (BuildConfig.MIN_SDK_VERSION > Build.VERSION.SDK_INT) {
|
||||
val intent = Intent(Intent.ACTION_MAIN)
|
||||
intent.addCategory(Intent.CATEGORY_HOME)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(intent)
|
||||
System.exit(0)
|
||||
}
|
||||
if (BuildConfig.DEBUG) {
|
||||
StrictMode.setVmPolicy(VmPolicy.Builder().detectAll().penaltyLog().build())
|
||||
StrictMode.setThreadPolicy(ThreadPolicy.Builder().detectAll().penaltyLog().build())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun determineBackend(): Backend {
|
||||
var backend: Backend? = null
|
||||
var didStartRootShell = false
|
||||
if (!ModuleLoader.isModuleLoaded() && moduleLoader.moduleMightExist()) {
|
||||
try {
|
||||
rootShell.start()
|
||||
didStartRootShell = true
|
||||
moduleLoader.loadModule()
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
if (!UserKnobs.disableKernelModule.first() && ModuleLoader.isModuleLoaded()) {
|
||||
try {
|
||||
if (!didStartRootShell)
|
||||
rootShell.start()
|
||||
val wgQuickBackend = WgQuickBackend(applicationContext, rootShell, toolsInstaller)
|
||||
wgQuickBackend.setMultipleTunnels(UserKnobs.multipleTunnels.first())
|
||||
backend = wgQuickBackend
|
||||
UserKnobs.multipleTunnels.onEach {
|
||||
wgQuickBackend.setMultipleTunnels(it)
|
||||
}.launchIn(coroutineScope)
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
if (backend == null) {
|
||||
backend = GoBackend(applicationContext)
|
||||
GoBackend.setAlwaysOnCallback { get().applicationScope.launch { get().tunnelManager.restoreState(true) } }
|
||||
}
|
||||
return backend
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
Log.i(TAG, USER_AGENT)
|
||||
super.onCreate()
|
||||
rootShell = RootShell(applicationContext)
|
||||
toolsInstaller = ToolsInstaller(applicationContext, rootShell)
|
||||
moduleLoader = ModuleLoader(applicationContext, rootShell, USER_AGENT)
|
||||
preferencesDataStore = applicationContext.createDataStore(name = "settings")
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
coroutineScope.launch {
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
if (UserKnobs.darkTheme.first()) AppCompatDelegate.MODE_NIGHT_YES else AppCompatDelegate.MODE_NIGHT_NO)
|
||||
}
|
||||
} else {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
}
|
||||
tunnelManager = TunnelManager(FileConfigStore(applicationContext))
|
||||
tunnelManager.onCreate()
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
backend = determineBackend()
|
||||
futureBackend.complete(backend!!)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
coroutineScope.cancel()
|
||||
super.onTerminate()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val USER_AGENT = String.format(Locale.ENGLISH, "WireGuard/%s (Android %d; %s; %s; %s %s; %s)", BuildConfig.VERSION_NAME, Build.VERSION.SDK_INT, if (Build.SUPPORTED_ABIS.isNotEmpty()) Build.SUPPORTED_ABIS[0] else "unknown ABI", Build.BOARD, Build.MANUFACTURER, Build.MODEL, Build.FINGERPRINT)
|
||||
private const val TAG = "WireGuard/Application"
|
||||
private lateinit var weakSelf: WeakReference<Application>
|
||||
|
||||
@JvmStatic
|
||||
fun get(): Application {
|
||||
return weakSelf.get()!!
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
suspend fun getBackend() = get().futureBackend.await()
|
||||
|
||||
@JvmStatic
|
||||
fun getModuleLoader() = get().moduleLoader
|
||||
|
||||
@JvmStatic
|
||||
fun getRootShell() = get().rootShell
|
||||
|
||||
@JvmStatic
|
||||
fun getPreferencesDataStore() = get().preferencesDataStore
|
||||
|
||||
@JvmStatic
|
||||
fun getToolsInstaller() = get().toolsInstaller
|
||||
|
||||
@JvmStatic
|
||||
fun getTunnelManager() = get().tunnelManager
|
||||
|
||||
@JvmStatic
|
||||
fun getCoroutineScope() = get().coroutineScope
|
||||
}
|
||||
|
||||
init {
|
||||
weakSelf = WeakReference(this)
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.backend.WgQuickBackend;
|
||||
import com.wireguard.android.model.TunnelManager;
|
||||
import com.wireguard.android.util.ExceptionLoggers;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
@NonNullForAll
|
||||
public class BootShutdownReceiver extends BroadcastReceiver {
|
||||
private static final String TAG = "WireGuard/" + BootShutdownReceiver.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
Application.getBackendAsync().thenAccept(backend -> {
|
||||
if (!(backend instanceof WgQuickBackend))
|
||||
return;
|
||||
final String action = intent.getAction();
|
||||
if (action == null)
|
||||
return;
|
||||
final TunnelManager tunnelManager = Application.getTunnelManager();
|
||||
if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
|
||||
Log.i(TAG, "Broadcast receiver restoring state (boot)");
|
||||
tunnelManager.restoreState(false).whenComplete(ExceptionLoggers.D);
|
||||
} else if (Intent.ACTION_SHUTDOWN.equals(action)) {
|
||||
Log.i(TAG, "Broadcast receiver saving state (shutdown)");
|
||||
tunnelManager.saveState();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.wireguard.android.util.applicationScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class BootShutdownReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
applicationScope.launch {
|
||||
if (Application.getBackend() !is WgQuickBackend) return@launch
|
||||
val action = intent.action ?: return@launch
|
||||
val tunnelManager = Application.getTunnelManager()
|
||||
if (Intent.ACTION_BOOT_COMPLETED == action) {
|
||||
Log.i(TAG, "Broadcast receiver restoring state (boot)")
|
||||
tunnelManager.restoreState(false)
|
||||
} else if (Intent.ACTION_SHUTDOWN == action) {
|
||||
Log.i(TAG, "Broadcast receiver saving state (shutdown)")
|
||||
tunnelManager.saveState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "WireGuard/BootShutdownReceiver"
|
||||
}
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.service.quicksettings.Tile;
|
||||
import android.service.quicksettings.TileService;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.activity.MainActivity;
|
||||
import com.wireguard.android.activity.TunnelToggleActivity;
|
||||
import com.wireguard.android.backend.Tunnel.State;
|
||||
import com.wireguard.android.model.ObservableTunnel;
|
||||
import com.wireguard.android.widget.SlashDrawable;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.databinding.Observable;
|
||||
import androidx.databinding.Observable.OnPropertyChangedCallback;
|
||||
|
||||
/**
|
||||
* Service that maintains the application's custom Quick Settings tile. This service is bound by the
|
||||
* system framework as necessary to update the appearance of the tile in the system UI, and to
|
||||
* forward click events to the application.
|
||||
*/
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
@NonNullForAll
|
||||
public class QuickTileService extends TileService {
|
||||
private static final String TAG = "WireGuard/" + QuickTileService.class.getSimpleName();
|
||||
|
||||
private final OnStateChangedCallback onStateChangedCallback = new OnStateChangedCallback();
|
||||
private final OnTunnelChangedCallback onTunnelChangedCallback = new OnTunnelChangedCallback();
|
||||
@Nullable private Icon iconOff;
|
||||
@Nullable private Icon iconOn;
|
||||
@Nullable private ObservableTunnel tunnel;
|
||||
|
||||
/* This works around an annoying unsolved frameworks bug some people are hitting. */
|
||||
@Override
|
||||
@Nullable
|
||||
public IBinder onBind(final Intent intent) {
|
||||
IBinder ret = null;
|
||||
try {
|
||||
ret = super.onBind(intent);
|
||||
} catch (final Exception e) {
|
||||
Log.d(TAG, "Failed to bind to TileService", e);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
if (tunnel != null) {
|
||||
unlockAndRun(() -> {
|
||||
final Tile tile = getQsTile();
|
||||
if (tile != null) {
|
||||
tile.setIcon(tile.getIcon() == iconOn ? iconOff : iconOn);
|
||||
tile.updateTile();
|
||||
}
|
||||
tunnel.setState(State.TOGGLE).whenComplete((v, t) -> {
|
||||
if (t == null) {
|
||||
updateTile();
|
||||
} else {
|
||||
final Intent toggleIntent = new Intent(this, TunnelToggleActivity.class);
|
||||
toggleIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(toggleIntent);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
final Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivityAndCollapse(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
iconOff = iconOn = Icon.createWithResource(this, R.drawable.ic_tile);
|
||||
return;
|
||||
}
|
||||
final SlashDrawable icon = new SlashDrawable(getResources().getDrawable(R.drawable.ic_tile, Application.get().getTheme()));
|
||||
icon.setAnimationEnabled(false); /* Unfortunately we can't have animations, since Icons are marshaled. */
|
||||
icon.setSlashed(false);
|
||||
Bitmap b = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
|
||||
Canvas c = new Canvas(b);
|
||||
icon.setBounds(0, 0, c.getWidth(), c.getHeight());
|
||||
icon.draw(c);
|
||||
iconOn = Icon.createWithBitmap(b);
|
||||
icon.setSlashed(true);
|
||||
b = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
|
||||
c = new Canvas(b);
|
||||
icon.setBounds(0, 0, c.getWidth(), c.getHeight());
|
||||
icon.draw(c);
|
||||
iconOff = Icon.createWithBitmap(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartListening() {
|
||||
Application.getTunnelManager().addOnPropertyChangedCallback(onTunnelChangedCallback);
|
||||
if (tunnel != null)
|
||||
tunnel.addOnPropertyChangedCallback(onStateChangedCallback);
|
||||
updateTile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopListening() {
|
||||
if (tunnel != null)
|
||||
tunnel.removeOnPropertyChangedCallback(onStateChangedCallback);
|
||||
Application.getTunnelManager().removeOnPropertyChangedCallback(onTunnelChangedCallback);
|
||||
}
|
||||
|
||||
private void updateTile() {
|
||||
// Update the tunnel.
|
||||
final ObservableTunnel newTunnel = Application.getTunnelManager().getLastUsedTunnel();
|
||||
if (newTunnel != tunnel) {
|
||||
if (tunnel != null)
|
||||
tunnel.removeOnPropertyChangedCallback(onStateChangedCallback);
|
||||
tunnel = newTunnel;
|
||||
if (tunnel != null)
|
||||
tunnel.addOnPropertyChangedCallback(onStateChangedCallback);
|
||||
}
|
||||
// Update the tile contents.
|
||||
final String label;
|
||||
final int state;
|
||||
final Tile tile = getQsTile();
|
||||
if (tunnel != null) {
|
||||
label = tunnel.getName();
|
||||
state = tunnel.getState() == State.UP ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
|
||||
} else {
|
||||
label = getString(R.string.app_name);
|
||||
state = Tile.STATE_INACTIVE;
|
||||
}
|
||||
if (tile == null)
|
||||
return;
|
||||
tile.setLabel(label);
|
||||
if (tile.getState() != state) {
|
||||
tile.setIcon(state == Tile.STATE_ACTIVE ? iconOn : iconOff);
|
||||
tile.setState(state);
|
||||
}
|
||||
tile.updateTile();
|
||||
}
|
||||
|
||||
private final class OnStateChangedCallback extends OnPropertyChangedCallback {
|
||||
@Override
|
||||
public void onPropertyChanged(final Observable sender, final int propertyId) {
|
||||
if (!Objects.equals(sender, tunnel)) {
|
||||
sender.removeOnPropertyChangedCallback(this);
|
||||
return;
|
||||
}
|
||||
if (propertyId != 0 && propertyId != BR.state)
|
||||
return;
|
||||
updateTile();
|
||||
}
|
||||
}
|
||||
|
||||
private final class OnTunnelChangedCallback extends OnPropertyChangedCallback {
|
||||
@Override
|
||||
public void onPropertyChanged(final Observable sender, final int propertyId) {
|
||||
if (propertyId != 0 && propertyId != BR.lastUsedTunnel)
|
||||
return;
|
||||
updateTile();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.databinding.Observable
|
||||
import androidx.databinding.Observable.OnPropertyChangedCallback
|
||||
import com.wireguard.android.activity.MainActivity
|
||||
import com.wireguard.android.activity.TunnelToggleActivity
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.model.ObservableTunnel
|
||||
import com.wireguard.android.util.applicationScope
|
||||
import com.wireguard.android.widget.SlashDrawable
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Service that maintains the application's custom Quick Settings tile. This service is bound by the
|
||||
* system framework as necessary to update the appearance of the tile in the system UI, and to
|
||||
* forward click events to the application.
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
class QuickTileService : TileService() {
|
||||
private val onStateChangedCallback = OnStateChangedCallback()
|
||||
private val onTunnelChangedCallback = OnTunnelChangedCallback()
|
||||
private var iconOff: Icon? = null
|
||||
private var iconOn: Icon? = null
|
||||
private var tunnel: ObservableTunnel? = null
|
||||
|
||||
/* This works around an annoying unsolved frameworks bug some people are hitting. */
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
var ret: IBinder? = null
|
||||
try {
|
||||
ret = super.onBind(intent)
|
||||
} catch (e: Throwable) {
|
||||
Log.d(TAG, "Failed to bind to TileService", e)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
if (tunnel != null) {
|
||||
unlockAndRun {
|
||||
val tile = qsTile
|
||||
if (tile != null) {
|
||||
tile.icon = if (tile.icon == iconOn) iconOff else iconOn
|
||||
tile.updateTile()
|
||||
}
|
||||
applicationScope.launch {
|
||||
try {
|
||||
tunnel!!.setStateAsync(Tunnel.State.TOGGLE)
|
||||
updateTile()
|
||||
} catch (_: Throwable) {
|
||||
val toggleIntent = Intent(this@QuickTileService, TunnelToggleActivity::class.java)
|
||||
toggleIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(toggleIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivityAndCollapse(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
iconOn = Icon.createWithResource(this, R.drawable.ic_tile)
|
||||
iconOff = iconOn
|
||||
return
|
||||
}
|
||||
val icon = SlashDrawable(resources.getDrawable(R.drawable.ic_tile, Application.get().theme))
|
||||
icon.setAnimationEnabled(false) /* Unfortunately we can't have animations, since Icons are marshaled. */
|
||||
icon.setSlashed(false)
|
||||
var b = Bitmap.createBitmap(icon.intrinsicWidth, icon.intrinsicHeight, Bitmap.Config.ARGB_8888)
|
||||
?: return
|
||||
var c = Canvas(b)
|
||||
icon.setBounds(0, 0, c.width, c.height)
|
||||
icon.draw(c)
|
||||
iconOn = Icon.createWithBitmap(b)
|
||||
icon.setSlashed(true)
|
||||
b = Bitmap.createBitmap(icon.intrinsicWidth, icon.intrinsicHeight, Bitmap.Config.ARGB_8888)
|
||||
?: return
|
||||
c = Canvas(b)
|
||||
icon.setBounds(0, 0, c.width, c.height)
|
||||
icon.draw(c)
|
||||
iconOff = Icon.createWithBitmap(b)
|
||||
}
|
||||
|
||||
override fun onStartListening() {
|
||||
Application.getTunnelManager().addOnPropertyChangedCallback(onTunnelChangedCallback)
|
||||
if (tunnel != null) tunnel!!.addOnPropertyChangedCallback(onStateChangedCallback)
|
||||
updateTile()
|
||||
}
|
||||
|
||||
override fun onStopListening() {
|
||||
if (tunnel != null) tunnel!!.removeOnPropertyChangedCallback(onStateChangedCallback)
|
||||
Application.getTunnelManager().removeOnPropertyChangedCallback(onTunnelChangedCallback)
|
||||
}
|
||||
|
||||
private fun updateTile() {
|
||||
// Update the tunnel.
|
||||
val newTunnel = Application.getTunnelManager().lastUsedTunnel
|
||||
if (newTunnel != tunnel) {
|
||||
if (tunnel != null) tunnel!!.removeOnPropertyChangedCallback(onStateChangedCallback)
|
||||
tunnel = newTunnel
|
||||
if (tunnel != null) tunnel!!.addOnPropertyChangedCallback(onStateChangedCallback)
|
||||
}
|
||||
// Update the tile contents.
|
||||
val label: String
|
||||
val state: Int
|
||||
val tile = qsTile
|
||||
if (tunnel != null) {
|
||||
label = tunnel!!.name
|
||||
state = if (tunnel!!.state == Tunnel.State.UP) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
|
||||
} else {
|
||||
label = getString(R.string.app_name)
|
||||
state = Tile.STATE_INACTIVE
|
||||
}
|
||||
if (tile == null) return
|
||||
tile.label = label
|
||||
if (tile.state != state) {
|
||||
tile.icon = if (state == Tile.STATE_ACTIVE) iconOn else iconOff
|
||||
tile.state = state
|
||||
}
|
||||
tile.updateTile()
|
||||
}
|
||||
|
||||
private inner class OnStateChangedCallback : OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: Observable, propertyId: Int) {
|
||||
if (sender != tunnel) {
|
||||
sender.removeOnPropertyChangedCallback(this)
|
||||
return
|
||||
}
|
||||
if (propertyId != 0 && propertyId != BR.state) return
|
||||
updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class OnTunnelChangedCallback : OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: Observable, propertyId: Int) {
|
||||
if (propertyId != 0 && propertyId != BR.lastUsedTunnel) return
|
||||
updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "WireGuard/QuickTileService"
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ package com.wireguard.android.activity
|
||||
import android.os.Bundle
|
||||
import androidx.databinding.CallbackRegistry
|
||||
import androidx.databinding.CallbackRegistry.NotifierCallback
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.model.ObservableTunnel
|
||||
|
||||
@@ -23,6 +24,7 @@ abstract class BaseActivity : ThemeChangeAwareActivity() {
|
||||
onSelectedTunnelChanged(oldTunnel, value)
|
||||
selectionChangeRegistry.notifyCallbacks(oldTunnel, 0, value)
|
||||
}
|
||||
|
||||
fun addOnSelectedTunnelChangedListener(listener: OnSelectedTunnelChangedListener) {
|
||||
selectionChangeRegistry.add(listener)
|
||||
}
|
||||
@@ -34,11 +36,8 @@ abstract class BaseActivity : ThemeChangeAwareActivity() {
|
||||
intent != null -> intent.getStringExtra(KEY_SELECTED_TUNNEL)
|
||||
else -> null
|
||||
}
|
||||
if (savedTunnelName != null) {
|
||||
Application.getTunnelManager()
|
||||
.tunnels
|
||||
.thenAccept { selectedTunnel = it[savedTunnelName] }
|
||||
}
|
||||
if (savedTunnelName != null)
|
||||
lifecycleScope.launchWhenCreated { selectedTunnel = Application.getTunnelManager().getTunnels()[savedTunnelName] }
|
||||
|
||||
// The selected tunnel must be set before the superclass method recreates fragments.
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -50,6 +49,7 @@ abstract class BaseActivity : ThemeChangeAwareActivity() {
|
||||
}
|
||||
|
||||
protected abstract fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?)
|
||||
|
||||
fun removeOnSelectedTunnelChangedListener(
|
||||
listener: OnSelectedTunnelChangedListener) {
|
||||
selectionChangeRegistry.remove(listener)
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.activity
|
||||
|
||||
import android.content.ClipDescription.compareMimeTypes
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentValues
|
||||
import android.content.Intent
|
||||
import android.database.Cursor
|
||||
import android.database.MatrixCursor
|
||||
import android.graphics.Typeface.BOLD
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ShareCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.textview.MaterialTextView
|
||||
import com.wireguard.android.BuildConfig
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.databinding.LogViewerActivityBinding
|
||||
import com.wireguard.android.util.DownloadsFileSaver
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import com.wireguard.crypto.KeyPair
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.BufferedReader
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.text.DateFormat
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.regex.Matcher
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class LogViewerActivity : AppCompatActivity() {
|
||||
private lateinit var binding: LogViewerActivityBinding
|
||||
private lateinit var logAdapter: LogEntryAdapter
|
||||
private var logLines = arrayListOf<LogLine>()
|
||||
private var rawLogLines = StringBuffer()
|
||||
private var recyclerView: RecyclerView? = null
|
||||
private var saveButton: MenuItem? = null
|
||||
private val year by lazy {
|
||||
val yearFormatter: DateFormat = SimpleDateFormat("yyyy", Locale.US)
|
||||
yearFormatter.format(Date())
|
||||
}
|
||||
|
||||
private val defaultColor by lazy { ResourcesCompat.getColor(resources, R.color.primary_text_color, theme) }
|
||||
|
||||
private val debugColor by lazy { ResourcesCompat.getColor(resources, R.color.debug_tag_color, theme) }
|
||||
|
||||
private val errorColor by lazy { ResourcesCompat.getColor(resources, R.color.error_tag_color, theme) }
|
||||
|
||||
private val infoColor by lazy { ResourcesCompat.getColor(resources, R.color.info_tag_color, theme) }
|
||||
|
||||
private val warningColor by lazy { ResourcesCompat.getColor(resources, R.color.warning_tag_color, theme) }
|
||||
|
||||
private var lastUri: Uri? = null
|
||||
|
||||
private fun revokeLastUri() {
|
||||
lastUri?.let {
|
||||
LOGS.remove(it.pathSegments.lastOrNull())
|
||||
revokeUriPermission(it, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
lastUri = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = LogViewerActivityBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
logAdapter = LogEntryAdapter()
|
||||
binding.recyclerView.apply {
|
||||
recyclerView = this
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
adapter = logAdapter
|
||||
addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL))
|
||||
}
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) { streamingLog() }
|
||||
|
||||
val revokeLastActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
revokeLastUri()
|
||||
}
|
||||
|
||||
binding.shareFab.setOnClickListener {
|
||||
revokeLastUri()
|
||||
val key = KeyPair().privateKey.toHex()
|
||||
LOGS[key] = rawLogLines.toString().toByteArray(Charsets.UTF_8)
|
||||
lastUri = Uri.parse("content://${BuildConfig.APPLICATION_ID}.exported-log/$key")
|
||||
val shareIntent = ShareCompat.IntentBuilder.from(this)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.log_viewer, menu)
|
||||
saveButton = menu?.findItem(R.id.save_log)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
finish()
|
||||
true
|
||||
}
|
||||
R.id.save_log -> {
|
||||
saveButton?.isEnabled = false
|
||||
lifecycleScope.launch { saveLog() }
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private val downloadsFileSaver = DownloadsFileSaver(this)
|
||||
|
||||
private suspend fun saveLog() {
|
||||
var exception: Throwable? = null
|
||||
var outputFile: DownloadsFileSaver.DownloadsFile? = null
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
outputFile = downloadsFileSaver.save("wireguard-log.txt", "text/plain", true)
|
||||
outputFile?.outputStream?.write(rawLogLines.toString().toByteArray(Charsets.UTF_8))
|
||||
} catch (e: Throwable) {
|
||||
outputFile?.delete()
|
||||
exception = e
|
||||
}
|
||||
}
|
||||
saveButton?.isEnabled = true
|
||||
if (outputFile == null)
|
||||
return
|
||||
Snackbar.make(findViewById(android.R.id.content),
|
||||
if (exception == null) getString(R.string.log_export_success, outputFile?.fileName)
|
||||
else getString(R.string.log_export_error, ErrorMessages[exception]),
|
||||
if (exception == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG)
|
||||
.setAnchorView(binding.shareFab)
|
||||
.show()
|
||||
}
|
||||
|
||||
private suspend fun streamingLog() = withContext(Dispatchers.IO) {
|
||||
val builder = ProcessBuilder().command("logcat", "-b", "all", "-v", "threadtime", "*:V")
|
||||
builder.environment()["LC_ALL"] = "C"
|
||||
var process: Process? = null
|
||||
try {
|
||||
process = try {
|
||||
builder.start()
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
return@withContext
|
||||
}
|
||||
val stdout = BufferedReader(InputStreamReader(process!!.inputStream, StandardCharsets.UTF_8))
|
||||
var haveScrolled = false
|
||||
val start = System.nanoTime()
|
||||
var startPeriod = start
|
||||
while (true) {
|
||||
val line = stdout.readLine() ?: break
|
||||
rawLogLines.append(line)
|
||||
rawLogLines.append('\n')
|
||||
val logLine = parseLine(line)
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
if (logLine != null) {
|
||||
recyclerView?.let {
|
||||
val shouldScroll = haveScrolled && !it.canScrollVertically(1)
|
||||
logLines.add(logLine)
|
||||
if (haveScrolled) logAdapter.notifyDataSetChanged()
|
||||
if (shouldScroll)
|
||||
it.scrollToPosition(logLines.size - 1)
|
||||
}
|
||||
} else {
|
||||
/* TODO: I'd prefer for the next line to be:
|
||||
* logLines.lastOrNull()?.msg += "\n$line"
|
||||
* However, as of writing, that causes the kotlin compiler to freak out and crash, spewing bytecode.
|
||||
*/
|
||||
logLines.lastOrNull()?.apply { msg += "\n$line" }
|
||||
if (haveScrolled) logAdapter.notifyDataSetChanged()
|
||||
}
|
||||
if (!haveScrolled) {
|
||||
val end = System.nanoTime()
|
||||
val scroll = (end - start) > 1000000000L * 2.5 || !stdout.ready()
|
||||
if (logLines.isNotEmpty() && (scroll || (end - startPeriod) > 1000000000L / 4)) {
|
||||
logAdapter.notifyDataSetChanged()
|
||||
recyclerView?.scrollToPosition(logLines.size - 1)
|
||||
startPeriod = end
|
||||
}
|
||||
if (scroll) haveScrolled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
process?.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseTime(timeStr: String): Date? {
|
||||
val formatter: DateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US)
|
||||
return try {
|
||||
formatter.parse("$year-$timeStr")
|
||||
} catch (e: ParseException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseLine(line: String): LogLine? {
|
||||
val m: Matcher = THREADTIME_LINE.matcher(line)
|
||||
return if (m.matches()) {
|
||||
LogLine(m.group(2)!!.toInt(), m.group(3)!!.toInt(), parseTime(m.group(1)!!), m.group(4)!!, m.group(5)!!, m.group(6)!!)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private data class LogLine(val pid: Int, val tid: Int, val time: Date?, val level: String, val tag: String, var msg: String)
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Match a single line of `logcat -v threadtime`, such as:
|
||||
*
|
||||
* <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 LOGS: MutableMap<String, ByteArray> = ConcurrentHashMap()
|
||||
private const val TAG = "WireGuard/LogViewerActivity"
|
||||
}
|
||||
|
||||
private inner class LogEntryAdapter : RecyclerView.Adapter<LogEntryAdapter.ViewHolder>() {
|
||||
|
||||
private inner class ViewHolder(val layout: View, var isSingleLine: Boolean = true) : RecyclerView.ViewHolder(layout)
|
||||
|
||||
private fun levelToColor(level: String): Int {
|
||||
return when (level) {
|
||||
"D" -> debugColor
|
||||
"E" -> errorColor
|
||||
"I" -> infoColor
|
||||
"W" -> warningColor
|
||||
else -> defaultColor
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = logLines.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.log_viewer_entry, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val line = logLines[position]
|
||||
val spannable = if (position > 0 && logLines[position - 1].tag == line.tag)
|
||||
SpannableString(line.msg)
|
||||
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)
|
||||
}
|
||||
holder.layout.apply {
|
||||
findViewById<MaterialTextView>(R.id.log_date).text = line.time.toString()
|
||||
findViewById<MaterialTextView>(R.id.log_msg).apply {
|
||||
setSingleLine()
|
||||
text = spannable
|
||||
setOnClickListener {
|
||||
isSingleLine = !holder.isSingleLine
|
||||
holder.isSingleLine = !holder.isSingleLine
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ExportedLogContentProvider : ContentProvider() {
|
||||
private fun logForUri(uri: Uri): ByteArray? = LOGS[uri.pathSegments.lastOrNull()]
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
override fun onCreate(): Boolean = true
|
||||
|
||||
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int = 0
|
||||
|
||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0
|
||||
|
||||
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 openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
|
||||
if (mode != "r") return null
|
||||
val log = logForUri(uri) ?: return null
|
||||
return openPipeHelper(uri, "text/plain", null, log) { output, _, _, _, l ->
|
||||
try {
|
||||
FileOutputStream(output.fileDescriptor).write(l!!)
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,10 @@ import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import androidx.appcompat.app.ActionBar
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import androidx.fragment.app.commit
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.fragment.TunnelDetailFragment
|
||||
import com.wireguard.android.fragment.TunnelEditorFragment
|
||||
@@ -55,20 +55,9 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.main_activity)
|
||||
actionBar = supportActionBar
|
||||
isTwoPaneLayout = findViewById<View>(R.id.master_detail_wrapper) is LinearLayout
|
||||
isTwoPaneLayout = findViewById<View?>(R.id.master_detail_wrapper) != null
|
||||
supportFragmentManager.addOnBackStackChangedListener(this)
|
||||
onBackStackChanged()
|
||||
// Dispatch insets on back stack change
|
||||
// This is required to ensure replaced fragments are also able to consume insets
|
||||
findViewById<View>(R.id.master_detail_wrapper).setOnApplyWindowInsetsListener { _, insets ->
|
||||
val fragmentManager = supportFragmentManager
|
||||
fragmentManager.addOnBackStackChangedListener {
|
||||
fragmentManager.fragments.forEach {
|
||||
it.requireView().dispatchApplyWindowInsets(insets)
|
||||
}
|
||||
}
|
||||
insets
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
@@ -84,11 +73,11 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener
|
||||
true
|
||||
}
|
||||
R.id.menu_action_edit -> {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.detail_container, TunnelEditorFragment())
|
||||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.detail_container, TunnelEditorFragment())
|
||||
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||
addToBackStack(null)
|
||||
}
|
||||
true
|
||||
}
|
||||
// This menu item is handled by the editor fragment.
|
||||
@@ -116,11 +105,11 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener
|
||||
fragmentManager.popBackStackImmediate()
|
||||
} else if (backStackEntries == 0) {
|
||||
// Create and show a new detail fragment.
|
||||
fragmentManager.beginTransaction()
|
||||
.add(R.id.detail_container, TunnelDetailFragment())
|
||||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
fragmentManager.commit {
|
||||
add(R.id.detail_container, TunnelDetailFragment())
|
||||
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||
addToBackStack(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,54 +4,34 @@
|
||||
*/
|
||||
package com.wireguard.android.activity
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.SparseArray
|
||||
import android.view.MenuItem
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.wireguard.android.preference.PreferencesPreferenceDataStore
|
||||
import com.wireguard.android.util.AdminKnobs
|
||||
import com.wireguard.android.util.ModuleLoader
|
||||
import java.util.ArrayList
|
||||
import java.util.Arrays
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Interface for changing application-global persistent settings.
|
||||
*/
|
||||
class SettingsActivity : ThemeChangeAwareActivity() {
|
||||
private val permissionRequestCallbacks = SparseArray<(permissions: Array<String>, granted: IntArray) -> Unit>()
|
||||
private var permissionRequestCounter = 0
|
||||
|
||||
fun ensurePermissions(permissions: Array<String>, cb: (permissions: Array<String>, granted: IntArray) -> Unit) {
|
||||
val needPermissions: MutableList<String> = ArrayList(permissions.size)
|
||||
permissions.forEach {
|
||||
if (ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED) {
|
||||
needPermissions.add(it)
|
||||
}
|
||||
}
|
||||
if (needPermissions.isEmpty()) {
|
||||
val granted = IntArray(permissions.size)
|
||||
Arrays.fill(granted, PackageManager.PERMISSION_GRANTED)
|
||||
cb.invoke(permissions, granted)
|
||||
return
|
||||
}
|
||||
val idx = permissionRequestCounter++
|
||||
permissionRequestCallbacks.put(idx, cb)
|
||||
ActivityCompat.requestPermissions(this,
|
||||
needPermissions.toTypedArray(), idx)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (supportFragmentManager.findFragmentById(android.R.id.content) == null) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(android.R.id.content, SettingsFragment())
|
||||
.commit()
|
||||
supportFragmentManager.commit {
|
||||
add(android.R.id.content, SettingsFragment())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,18 +43,9 @@ class SettingsActivity : ThemeChangeAwareActivity() {
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray) {
|
||||
val f = permissionRequestCallbacks[requestCode]
|
||||
if (f != null) {
|
||||
permissionRequestCallbacks.remove(requestCode)
|
||||
f.invoke(permissions, grantResults)
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, key: String?) {
|
||||
preferenceManager.preferenceDataStore = PreferencesPreferenceDataStore(lifecycleScope, Application.getPreferencesDataStore())
|
||||
addPreferencesFromResource(R.xml.preferences)
|
||||
preferenceScreen.initialExpandedChildrenCount = 4
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
@@ -82,32 +53,55 @@ class SettingsActivity : ThemeChangeAwareActivity() {
|
||||
darkTheme?.parent?.removePreference(darkTheme)
|
||||
--preferenceScreen.initialExpandedChildrenCount
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
val remoteApps = preferenceManager.findPreference<Preference>("allow_remote_control_intents")
|
||||
remoteApps?.parent?.removePreference(remoteApps)
|
||||
}
|
||||
if (AdminKnobs.disableConfigExport) {
|
||||
val zipExporter = preferenceManager.findPreference<Preference>("zip_exporter")
|
||||
zipExporter?.parent?.removePreference(zipExporter)
|
||||
}
|
||||
val wgQuickOnlyPrefs = arrayOf(
|
||||
preferenceManager.findPreference("tools_installer"),
|
||||
preferenceManager.findPreference("restore_on_boot"),
|
||||
preferenceManager.findPreference<Preference>("multiple_tunnels")
|
||||
).filterNotNull()
|
||||
wgQuickOnlyPrefs.forEach { it.isVisible = false }
|
||||
Application.getBackendAsync().thenAccept { backend ->
|
||||
if (backend is WgQuickBackend) {
|
||||
lifecycleScope.launch {
|
||||
if (Application.getBackend() is WgQuickBackend) {
|
||||
++preferenceScreen.initialExpandedChildrenCount
|
||||
wgQuickOnlyPrefs.forEach { it.isVisible = true }
|
||||
} else {
|
||||
wgQuickOnlyPrefs.forEach { it.parent?.removePreference(it) }
|
||||
}
|
||||
}
|
||||
preferenceManager.findPreference<Preference>("log_viewer")?.setOnPreferenceClickListener {
|
||||
startActivity(Intent(requireContext(), LogViewerActivity::class.java))
|
||||
true
|
||||
}
|
||||
val moduleInstaller = preferenceManager.findPreference<Preference>("module_downloader")
|
||||
val kernelModuleDisabler = preferenceManager.findPreference<Preference>("kernel_module_disabler")
|
||||
moduleInstaller?.isVisible = false
|
||||
if (ModuleLoader.isModuleLoaded()) {
|
||||
moduleInstaller?.parent?.removePreference(moduleInstaller)
|
||||
lifecycleScope.launch {
|
||||
if (Application.getBackend() !is WgQuickBackend) {
|
||||
try {
|
||||
withContext(Dispatchers.IO) { Application.getRootShell().start() }
|
||||
} catch (_: Throwable) {
|
||||
kernelModuleDisabler?.parent?.removePreference(kernelModuleDisabler)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
kernelModuleDisabler?.parent?.removePreference(kernelModuleDisabler)
|
||||
Application.getAsyncWorker().runAsync(Application.getRootShell()::start).whenComplete { _, e ->
|
||||
if (e == null)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) { Application.getRootShell().start() }
|
||||
moduleInstaller?.isVisible = true
|
||||
else
|
||||
} catch (_: Throwable) {
|
||||
moduleInstaller?.parent?.removePreference(moduleInstaller)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,39 +4,30 @@
|
||||
*/
|
||||
package com.wireguard.android.activity
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import com.wireguard.android.Application
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.wireguard.android.util.UserKnobs
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
abstract class ThemeChangeAwareActivity : AppCompatActivity(), OnSharedPreferenceChangeListener {
|
||||
abstract class ThemeChangeAwareActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
Application.getSharedPreferences().registerOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
Application.getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
when (key) {
|
||||
"dark_theme" -> {
|
||||
AppCompatDelegate.setDefaultNightMode(if (sharedPreferences.getBoolean(key, false)) {
|
||||
UserKnobs.darkTheme.onEach {
|
||||
val newMode = if (it) {
|
||||
AppCompatDelegate.MODE_NIGHT_YES
|
||||
} else {
|
||||
AppCompatDelegate.MODE_NIGHT_NO
|
||||
})
|
||||
recreate()
|
||||
}
|
||||
}
|
||||
if (AppCompatDelegate.getDefaultNightMode() != newMode) {
|
||||
AppCompatDelegate.setDefaultNightMode(newMode)
|
||||
recreate()
|
||||
}
|
||||
}.launchIn(lifecycleScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package com.wireguard.android.activity
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.commit
|
||||
import com.wireguard.android.fragment.TunnelEditorFragment
|
||||
import com.wireguard.android.model.ObservableTunnel
|
||||
|
||||
@@ -15,9 +16,9 @@ class TunnelCreatorActivity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (supportFragmentManager.findFragmentById(android.R.id.content) == null) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(android.R.id.content, TunnelEditorFragment())
|
||||
.commit()
|
||||
supportFragmentManager.commit {
|
||||
add(android.R.id.content, TunnelEditorFragment())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,35 +10,56 @@ import android.os.Bundle
|
||||
import android.service.quicksettings.TileService
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.QuickTileService
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
class TunnelToggleActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { toggleTunnelWithPermissionsResult() }
|
||||
|
||||
private fun toggleTunnelWithPermissionsResult() {
|
||||
val tunnel = Application.getTunnelManager().lastUsedTunnel ?: return
|
||||
tunnel.setState(Tunnel.State.TOGGLE).whenComplete { _, t ->
|
||||
TileService.requestListeningState(this, ComponentName(this, QuickTileService::class.java))
|
||||
onToggleFinished(t)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
tunnel.setStateAsync(Tunnel.State.TOGGLE)
|
||||
} catch (e: Throwable) {
|
||||
TileService.requestListeningState(this@TunnelToggleActivity, ComponentName(this@TunnelToggleActivity, QuickTileService::class.java))
|
||||
val error = ErrorMessages[e]
|
||||
val message = getString(R.string.toggle_error, error)
|
||||
Log.e(TAG, message, e)
|
||||
Toast.makeText(this@TunnelToggleActivity, message, Toast.LENGTH_LONG).show()
|
||||
finishAffinity()
|
||||
return@launch
|
||||
}
|
||||
TileService.requestListeningState(this@TunnelToggleActivity, ComponentName(this@TunnelToggleActivity, QuickTileService::class.java))
|
||||
finishAffinity()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onToggleFinished(throwable: Throwable?) {
|
||||
if (throwable == null) return
|
||||
val error = ErrorMessages.get(throwable)
|
||||
val message = getString(R.string.toggle_error, error)
|
||||
Log.e(TAG, message, throwable)
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleScope.launch {
|
||||
if (Application.getBackend() is GoBackend) {
|
||||
val intent = GoBackend.VpnService.prepare(this@TunnelToggleActivity)
|
||||
if (intent != null) {
|
||||
permissionActivityResultLauncher.launch(intent)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
toggleTunnelWithPermissionsResult()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = "WireGuard/" + TunnelToggleActivity::class.java.simpleName
|
||||
private const val TAG = "WireGuard/TunnelToggleActivity"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,391 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.activity
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.os.storage.StorageManager
|
||||
import android.os.storage.StorageVolume
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.view.forEach
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ObservableBoolean
|
||||
import androidx.databinding.ObservableField
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.databinding.Keyed
|
||||
import com.wireguard.android.databinding.ObservableKeyedArrayList
|
||||
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter
|
||||
import com.wireguard.android.databinding.TvActivityBinding
|
||||
import com.wireguard.android.databinding.TvFileListItemBinding
|
||||
import com.wireguard.android.databinding.TvTunnelListItemBinding
|
||||
import com.wireguard.android.model.ObservableTunnel
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import com.wireguard.android.util.QuantityFormatter
|
||||
import com.wireguard.android.util.TunnelImporter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
class TvMainActivity : AppCompatActivity() {
|
||||
private val tunnelFileImportResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { data ->
|
||||
if (data == null) return@registerForActivityResult
|
||||
lifecycleScope.launch {
|
||||
TunnelImporter.importTunnel(contentResolver, data) {
|
||||
Toast.makeText(this@TvMainActivity, it, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
private var pendingTunnel: ObservableTunnel? = null
|
||||
private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
val tunnel = pendingTunnel
|
||||
if (tunnel != null)
|
||||
setTunnelStateWithPermissionsResult(tunnel)
|
||||
pendingTunnel = null
|
||||
}
|
||||
|
||||
private fun setTunnelStateWithPermissionsResult(tunnel: ObservableTunnel) {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
tunnel.setStateAsync(Tunnel.State.TOGGLE)
|
||||
} catch (e: Throwable) {
|
||||
val error = ErrorMessages[e]
|
||||
val message = getString(R.string.error_up, error)
|
||||
Toast.makeText(this@TvMainActivity, message, Toast.LENGTH_LONG).show()
|
||||
Log.e(TAG, message, e)
|
||||
}
|
||||
updateStats()
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var binding: TvActivityBinding
|
||||
private val isDeleting = ObservableBoolean()
|
||||
private val files = ObservableKeyedArrayList<String, KeyedFile>()
|
||||
private val filesRoot = ObservableField("")
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = TvActivityBinding.inflate(layoutInflater)
|
||||
lifecycleScope.launch {
|
||||
binding.tunnels = Application.getTunnelManager().getTunnels()
|
||||
if (binding.tunnels?.isEmpty() == true)
|
||||
binding.importButton.requestFocus()
|
||||
else
|
||||
binding.tunnelList.requestFocus()
|
||||
}
|
||||
binding.isDeleting = isDeleting
|
||||
binding.files = files
|
||||
binding.filesRoot = filesRoot
|
||||
val gridManager = binding.tunnelList.layoutManager as GridLayoutManager
|
||||
gridManager.spanSizeLookup = SlatedSpanSizeLookup(gridManager)
|
||||
binding.tunnelRowConfigurationHandler = object : ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TvTunnelListItemBinding, ObservableTunnel> {
|
||||
override fun onConfigureRow(binding: TvTunnelListItemBinding, item: ObservableTunnel, position: Int) {
|
||||
binding.isDeleting = isDeleting
|
||||
binding.isFocused = ObservableBoolean()
|
||||
binding.root.setOnFocusChangeListener { _, focused ->
|
||||
binding.isFocused?.set(focused)
|
||||
}
|
||||
binding.root.setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
if (isDeleting.get()) {
|
||||
try {
|
||||
item.deleteAsync()
|
||||
if (this@TvMainActivity.binding.tunnels?.isEmpty() != false)
|
||||
isDeleting.set(false)
|
||||
} catch (e: Throwable) {
|
||||
val error = ErrorMessages[e]
|
||||
val message = getString(R.string.config_delete_error, error)
|
||||
Toast.makeText(this@TvMainActivity, message, Toast.LENGTH_LONG).show()
|
||||
Log.e(TAG, message, e)
|
||||
}
|
||||
} else {
|
||||
if (Application.getBackend() is GoBackend) {
|
||||
val intent = GoBackend.VpnService.prepare(binding.root.context)
|
||||
if (intent != null) {
|
||||
pendingTunnel = item
|
||||
permissionActivityResultLauncher.launch(intent)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
setTunnelStateWithPermissionsResult(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.filesRowConfigurationHandler = object : ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TvFileListItemBinding, KeyedFile> {
|
||||
override fun onConfigureRow(binding: TvFileListItemBinding, item: KeyedFile, position: Int) {
|
||||
binding.root.setOnClickListener {
|
||||
if (item.file.isDirectory)
|
||||
navigateTo(item.file)
|
||||
else {
|
||||
val uri = Uri.fromFile(item.file)
|
||||
files.clear()
|
||||
filesRoot.set("")
|
||||
lifecycleScope.launch {
|
||||
TunnelImporter.importTunnel(contentResolver, uri) {
|
||||
Toast.makeText(this@TvMainActivity, it, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
runOnUiThread {
|
||||
this@TvMainActivity.binding.tunnelList.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.importButton.setOnClickListener {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
if (filesRoot.get()?.isEmpty() != false) {
|
||||
navigateTo(File("/"))
|
||||
runOnUiThread {
|
||||
binding.filesList.requestFocus()
|
||||
}
|
||||
} else {
|
||||
files.clear()
|
||||
filesRoot.set("")
|
||||
runOnUiThread {
|
||||
binding.tunnelList.requestFocus()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
tunnelFileImportResultLauncher.launch("*/*")
|
||||
} catch (_: Throwable) {
|
||||
Toast.makeText(this@TvMainActivity, getString(R.string.tv_no_file_picker), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.deleteButton.setOnClickListener {
|
||||
isDeleting.set(!isDeleting.get())
|
||||
runOnUiThread {
|
||||
binding.tunnelList.requestFocus()
|
||||
}
|
||||
}
|
||||
binding.executePendingBindings()
|
||||
setContentView(binding.root)
|
||||
|
||||
lifecycleScope.launch {
|
||||
while (true) {
|
||||
updateStats()
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var pendingNavigation: File? = null
|
||||
private val permissionRequestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||
val to = pendingNavigation
|
||||
if (it && to != null)
|
||||
navigateTo(to)
|
||||
pendingNavigation = null
|
||||
}
|
||||
|
||||
private var cachedRoots: Collection<KeyedFile>? = null
|
||||
|
||||
private suspend fun makeStorageRoots(): Collection<KeyedFile> = withContext(Dispatchers.IO) {
|
||||
cachedRoots?.let { return@withContext it }
|
||||
val list = HashSet<KeyedFile>()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
val storageManager: StorageManager = getSystemService() ?: return@withContext list
|
||||
list.addAll(storageManager.storageVolumes.mapNotNull { volume ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
volume.directory?.let { KeyedFile(it, volume.getDescription(this@TvMainActivity)) }
|
||||
} else {
|
||||
KeyedFile((StorageVolume::class.java.getMethod("getPathFile").invoke(volume) as File), volume.getDescription(this@TvMainActivity))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
list.add(KeyedFile(Environment.getExternalStorageDirectory()))
|
||||
try {
|
||||
File("/storage").listFiles()?.forEach {
|
||||
if (!it.isDirectory) return@forEach
|
||||
try {
|
||||
if (Environment.isExternalStorageRemovable(it)) {
|
||||
list.add(KeyedFile(it))
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
}
|
||||
cachedRoots = list
|
||||
list
|
||||
}
|
||||
|
||||
private fun isBelowCachedRoots(maybeChild: File): Boolean {
|
||||
val cachedRoots = cachedRoots ?: return true
|
||||
for (root in cachedRoots) {
|
||||
if (maybeChild.canonicalPath.startsWith(root.file.canonicalPath))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun navigateTo(directory: File) {
|
||||
require(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
|
||||
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
pendingNavigation = directory
|
||||
permissionRequestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
return
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
if (isBelowCachedRoots(directory)) {
|
||||
val roots = makeStorageRoots()
|
||||
if (roots.count() == 1) {
|
||||
navigateTo(roots.first().file)
|
||||
return@launch
|
||||
}
|
||||
files.clear()
|
||||
files.addAll(roots)
|
||||
filesRoot.set(getString(R.string.tv_select_a_storage_drive))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val newFiles = withContext(Dispatchers.IO) {
|
||||
val newFiles = ArrayList<KeyedFile>()
|
||||
try {
|
||||
directory.parentFile?.let {
|
||||
newFiles.add(KeyedFile(it, "../"))
|
||||
}
|
||||
val listing = directory.listFiles() ?: return@withContext null
|
||||
listing.forEach {
|
||||
if (it.extension == "conf" || it.extension == "zip" || it.isDirectory)
|
||||
newFiles.add(KeyedFile(it))
|
||||
}
|
||||
newFiles.sortWith { a, b ->
|
||||
if (a.file.isDirectory && !b.file.isDirectory) -1
|
||||
else if (!a.file.isDirectory && b.file.isDirectory) 1
|
||||
else a.file.compareTo(b.file)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
newFiles
|
||||
}
|
||||
if (newFiles?.isEmpty() != false)
|
||||
return@launch
|
||||
files.clear()
|
||||
files.addAll(newFiles)
|
||||
filesRoot.set(directory.canonicalPath)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
when {
|
||||
isDeleting.get() -> {
|
||||
isDeleting.set(false)
|
||||
runOnUiThread {
|
||||
binding.tunnelList.requestFocus()
|
||||
}
|
||||
}
|
||||
filesRoot.get()?.isNotEmpty() == true -> {
|
||||
files.clear()
|
||||
filesRoot.set("")
|
||||
runOnUiThread {
|
||||
binding.tunnelList.requestFocus()
|
||||
}
|
||||
}
|
||||
else -> super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateStats() {
|
||||
binding.tunnelList.forEach { viewItem ->
|
||||
val listItem = DataBindingUtil.findBinding<TvTunnelListItemBinding>(viewItem)
|
||||
?: return@forEach
|
||||
try {
|
||||
val tunnel = listItem.item!!
|
||||
if (tunnel.state != Tunnel.State.UP || isDeleting.get()) {
|
||||
throw Exception()
|
||||
}
|
||||
val statistics = tunnel.getStatisticsAsync()
|
||||
val rx = statistics.totalRx()
|
||||
val tx = statistics.totalTx()
|
||||
listItem.tunnelTransfer.text = getString(R.string.transfer_rx_tx, QuantityFormatter.formatBytes(rx), QuantityFormatter.formatBytes(tx))
|
||||
listItem.tunnelTransfer.visibility = View.VISIBLE
|
||||
} catch (_: Throwable) {
|
||||
listItem.tunnelTransfer.visibility = View.GONE
|
||||
listItem.tunnelTransfer.text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class KeyedFile(val file: File, private val forcedKey: String? = null) : Keyed<String> {
|
||||
override val key: String
|
||||
get() = forcedKey ?: if (file.isDirectory) "${file.name}/" else file.name
|
||||
}
|
||||
|
||||
private class SlatedSpanSizeLookup(private val gridManager: GridLayoutManager) : SpanSizeLookup() {
|
||||
private val originalHeight = gridManager.spanCount
|
||||
private var newWidth = 0
|
||||
private lateinit var sizeMap: Array<IntArray?>
|
||||
|
||||
private fun emptyUnderIndex(index: Int, size: Int): Int {
|
||||
sizeMap[size - 1]?.let { return it[index] }
|
||||
val sizes = IntArray(size)
|
||||
val oh = originalHeight
|
||||
val nw = newWidth
|
||||
var empties = 0
|
||||
for (i in 0 until size) {
|
||||
val ox = (i + empties) / oh
|
||||
val oy = (i + empties) % oh
|
||||
var empty = 0
|
||||
for (j in oy + 1 until oh) {
|
||||
val ni = nw * j + ox
|
||||
if (ni < size)
|
||||
break
|
||||
empty++
|
||||
}
|
||||
empties += empty
|
||||
sizes[i] = empty
|
||||
}
|
||||
sizeMap[size - 1] = sizes
|
||||
return sizes[index]
|
||||
}
|
||||
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
if (newWidth == 0) {
|
||||
val child = gridManager.getChildAt(0) ?: return 1
|
||||
if (child.width == 0) return 1
|
||||
newWidth = gridManager.width / child.width
|
||||
sizeMap = Array(originalHeight * newWidth - 1) { null }
|
||||
}
|
||||
val total = gridManager.itemCount
|
||||
if (total >= originalHeight * newWidth || total == 0)
|
||||
return 1
|
||||
return emptyUnderIndex(position, total) + 1
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "WireGuard/TvMainActivity"
|
||||
}
|
||||
}
|
||||
@@ -24,8 +24,9 @@ class FileConfigStore(private val context: Context) : ConfigStore {
|
||||
override fun create(name: String, config: Config): Config {
|
||||
Log.d(TAG, "Creating configuration for tunnel $name")
|
||||
val file = fileFor(name)
|
||||
if (!file.createNewFile()) throw IOException(context.getString(R.string.config_file_exists_error, file.name))
|
||||
FileOutputStream(file, false).use { stream -> stream.write(config.toWgQuickString().toByteArray(StandardCharsets.UTF_8)) }
|
||||
if (!file.createNewFile())
|
||||
throw IOException(context.getString(R.string.config_file_exists_error, file.name))
|
||||
FileOutputStream(file, false).use { it.write(config.toWgQuickString().toByteArray(StandardCharsets.UTF_8)) }
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -33,7 +34,8 @@ class FileConfigStore(private val context: Context) : ConfigStore {
|
||||
override fun delete(name: String) {
|
||||
Log.d(TAG, "Deleting configuration for tunnel $name")
|
||||
val file = fileFor(name)
|
||||
if (!file.delete()) throw IOException(context.getString(R.string.config_delete_error, file.name))
|
||||
if (!file.delete())
|
||||
throw IOException(context.getString(R.string.config_delete_error, file.name))
|
||||
}
|
||||
|
||||
override fun enumerate(): Set<String> {
|
||||
@@ -68,12 +70,13 @@ class FileConfigStore(private val context: Context) : ConfigStore {
|
||||
override fun save(name: String, config: Config): Config {
|
||||
Log.d(TAG, "Saving configuration for tunnel $name")
|
||||
val file = fileFor(name)
|
||||
if (!file.isFile) throw FileNotFoundException(context.getString(R.string.config_not_found_error, file.name))
|
||||
if (!file.isFile)
|
||||
throw FileNotFoundException(context.getString(R.string.config_not_found_error, file.name))
|
||||
FileOutputStream(file, false).use { stream -> stream.write(config.toWgQuickString().toByteArray(StandardCharsets.UTF_8)) }
|
||||
return config
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = "WireGuard/" + FileConfigStore::class.java.simpleName
|
||||
private const val TAG = "WireGuard/FileConfigStore"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.databinding;
|
||||
|
||||
import android.text.InputFilter;
|
||||
import android.view.LayoutInflater;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.wireguard.android.BR;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler;
|
||||
import com.wireguard.android.util.ObservableKeyedList;
|
||||
import com.wireguard.android.widget.ToggleSwitch;
|
||||
import com.wireguard.android.widget.ToggleSwitch.OnBeforeCheckedChangeListener;
|
||||
import com.wireguard.config.Attribute;
|
||||
import com.wireguard.config.InetNetwork;
|
||||
import com.wireguard.util.Keyed;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.databinding.BindingAdapter;
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
import androidx.databinding.ObservableList;
|
||||
import androidx.databinding.ViewDataBinding;
|
||||
import androidx.databinding.adapters.ListenerUtil;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import java9.util.Optional;
|
||||
|
||||
/**
|
||||
* Static methods for use by generated code in the Android data binding library.
|
||||
*/
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@NonNullForAll
|
||||
public final class BindingAdapters {
|
||||
private BindingAdapters() {
|
||||
// Prevent instantiation.
|
||||
}
|
||||
|
||||
@BindingAdapter("checked")
|
||||
public static void setChecked(final ToggleSwitch view, final boolean checked) {
|
||||
view.setCheckedInternal(checked);
|
||||
}
|
||||
|
||||
@BindingAdapter("filter")
|
||||
public static void setFilter(final TextView view, final InputFilter filter) {
|
||||
view.setFilters(new InputFilter[]{filter});
|
||||
}
|
||||
|
||||
@BindingAdapter({"items", "layout"})
|
||||
public static <E>
|
||||
void setItems(final LinearLayout view,
|
||||
@Nullable final ObservableList<E> oldList, final int oldLayoutId,
|
||||
@Nullable final ObservableList<E> newList, final int newLayoutId) {
|
||||
if (oldList == newList && oldLayoutId == newLayoutId)
|
||||
return;
|
||||
ItemChangeListener<E> listener = ListenerUtil.getListener(view, R.id.item_change_listener);
|
||||
// If the layout changes, any existing listener must be replaced.
|
||||
if (listener != null && oldList != null && oldLayoutId != newLayoutId) {
|
||||
listener.setList(null);
|
||||
listener = null;
|
||||
// Stop tracking the old listener.
|
||||
ListenerUtil.trackListener(view, null, R.id.item_change_listener);
|
||||
}
|
||||
// Avoid adding a listener when there is no new list or layout.
|
||||
if (newList == null || newLayoutId == 0)
|
||||
return;
|
||||
if (listener == null) {
|
||||
listener = new ItemChangeListener<>(view, newLayoutId);
|
||||
ListenerUtil.trackListener(view, listener, R.id.item_change_listener);
|
||||
}
|
||||
// Either the list changed, or this is an entirely new listener because the layout changed.
|
||||
listener.setList(newList);
|
||||
}
|
||||
|
||||
@BindingAdapter({"items", "layout"})
|
||||
public static <E>
|
||||
void setItems(final LinearLayout view,
|
||||
@Nullable final Iterable<E> oldList, final int oldLayoutId,
|
||||
@Nullable final Iterable<E> newList, final int newLayoutId) {
|
||||
if (oldList == newList && oldLayoutId == newLayoutId)
|
||||
return;
|
||||
view.removeAllViews();
|
||||
if (newList == null)
|
||||
return;
|
||||
final LayoutInflater layoutInflater = LayoutInflater.from(view.getContext());
|
||||
for (final E item : newList) {
|
||||
final ViewDataBinding binding =
|
||||
DataBindingUtil.inflate(layoutInflater, newLayoutId, view, false);
|
||||
binding.setVariable(BR.collection, newList);
|
||||
binding.setVariable(BR.item, item);
|
||||
binding.executePendingBindings();
|
||||
view.addView(binding.getRoot());
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter(requireAll = false, value = {"items", "layout", "configurationHandler"})
|
||||
public static <K, E extends Keyed<? extends K>>
|
||||
void setItems(final RecyclerView view,
|
||||
@Nullable final ObservableKeyedList<K, E> oldList, final int oldLayoutId,
|
||||
final RowConfigurationHandler oldRowConfigurationHandler,
|
||||
@Nullable final ObservableKeyedList<K, E> newList, final int newLayoutId,
|
||||
final RowConfigurationHandler newRowConfigurationHandler) {
|
||||
if (view.getLayoutManager() == null)
|
||||
view.setLayoutManager(new LinearLayoutManager(view.getContext(), RecyclerView.VERTICAL, false));
|
||||
|
||||
if (oldList == newList && oldLayoutId == newLayoutId)
|
||||
return;
|
||||
// The ListAdapter interface is not generic, so this cannot be checked.
|
||||
@SuppressWarnings("unchecked") ObservableKeyedRecyclerViewAdapter<K, E> adapter =
|
||||
(ObservableKeyedRecyclerViewAdapter<K, E>) view.getAdapter();
|
||||
// If the layout changes, any existing adapter must be replaced.
|
||||
if (adapter != null && oldList != null && oldLayoutId != newLayoutId) {
|
||||
adapter.setList(null);
|
||||
adapter = null;
|
||||
}
|
||||
// Avoid setting an adapter when there is no new list or layout.
|
||||
if (newList == null || newLayoutId == 0)
|
||||
return;
|
||||
if (adapter == null) {
|
||||
adapter = new ObservableKeyedRecyclerViewAdapter<>(view.getContext(), newLayoutId, newList);
|
||||
view.setAdapter(adapter);
|
||||
}
|
||||
|
||||
adapter.setRowConfigurationHandler(newRowConfigurationHandler);
|
||||
// Either the list changed, or this is an entirely new listener because the layout changed.
|
||||
adapter.setList(newList);
|
||||
}
|
||||
|
||||
@BindingAdapter("onBeforeCheckedChanged")
|
||||
public static void setOnBeforeCheckedChanged(final ToggleSwitch view,
|
||||
final OnBeforeCheckedChangeListener listener) {
|
||||
view.setOnBeforeCheckedChangeListener(listener);
|
||||
}
|
||||
|
||||
@BindingAdapter("android:text")
|
||||
public static void setText(final TextView view, final Optional<?> text) {
|
||||
view.setText(text.map(Object::toString).orElse(""));
|
||||
}
|
||||
|
||||
@BindingAdapter("android:text")
|
||||
public static void setText(final TextView view, @Nullable final Iterable<InetNetwork> networks) {
|
||||
view.setText(networks != null ? Attribute.join(networks) : "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.databinding
|
||||
|
||||
import android.text.InputFilter
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.databinding.BindingAdapter
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ObservableList
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.databinding.adapters.ListenerUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.wireguard.android.BR
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler
|
||||
import com.wireguard.android.widget.ToggleSwitch
|
||||
import com.wireguard.android.widget.ToggleSwitch.OnBeforeCheckedChangeListener
|
||||
import com.wireguard.config.Attribute
|
||||
import com.wireguard.config.InetNetwork
|
||||
import java.net.InetAddress
|
||||
import java.util.Optional
|
||||
|
||||
/**
|
||||
* Static methods for use by generated code in the Android data binding library.
|
||||
*/
|
||||
object BindingAdapters {
|
||||
@JvmStatic
|
||||
@BindingAdapter("checked")
|
||||
fun setChecked(view: ToggleSwitch, checked: Boolean) {
|
||||
view.setCheckedInternal(checked)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@BindingAdapter("filter")
|
||||
fun setFilter(view: TextView, filter: InputFilter) {
|
||||
view.filters = arrayOf(filter)
|
||||
}
|
||||
|
||||
@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?) {
|
||||
if (oldList === newList && oldLayoutId == newLayoutId)
|
||||
return
|
||||
var listener: ItemChangeListener<E>? = ListenerUtil.getListener(view, R.id.item_change_listener)
|
||||
// If the layout changes, any existing listener must be replaced.
|
||||
if (listener != null && oldList != null && oldLayoutId != newLayoutId) {
|
||||
listener.setList(null)
|
||||
listener = null
|
||||
// Stop tracking the old listener.
|
||||
ListenerUtil.trackListener<Any?>(view, null, R.id.item_change_listener)
|
||||
}
|
||||
// Avoid adding a listener when there is no new list or layout.
|
||||
if (newList == null || newLayoutId == 0)
|
||||
return
|
||||
if (listener == null) {
|
||||
listener = ItemChangeListener(view, newLayoutId, newFragment)
|
||||
ListenerUtil.trackListener(view, listener, R.id.item_change_listener)
|
||||
}
|
||||
// Either the list changed, or this is an entirely new listener because the layout changed.
|
||||
listener.setList(newList)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@BindingAdapter("items", "layout")
|
||||
fun <E> setItems(view: LinearLayout,
|
||||
oldList: Iterable<E>?, oldLayoutId: Int,
|
||||
newList: Iterable<E>?, newLayoutId: Int) {
|
||||
if (oldList === newList && oldLayoutId == newLayoutId)
|
||||
return
|
||||
view.removeAllViews()
|
||||
if (newList == null)
|
||||
return
|
||||
val layoutInflater = LayoutInflater.from(view.context)
|
||||
for (item in newList) {
|
||||
val binding = DataBindingUtil.inflate<ViewDataBinding>(layoutInflater, newLayoutId, view, false)
|
||||
binding.setVariable(BR.collection, newList)
|
||||
binding.setVariable(BR.item, item)
|
||||
binding.executePendingBindings()
|
||||
view.addView(binding.root)
|
||||
}
|
||||
}
|
||||
|
||||
@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<*, *>?) {
|
||||
if (view.layoutManager == null)
|
||||
view.layoutManager = LinearLayoutManager(view.context, RecyclerView.VERTICAL, false)
|
||||
if (oldList === newList && oldLayoutId == newLayoutId)
|
||||
return
|
||||
// The ListAdapter interface is not generic, so this cannot be checked.
|
||||
@Suppress("UNCHECKED_CAST") var adapter = view.adapter as? ObservableKeyedRecyclerViewAdapter<K, E>?
|
||||
// If the layout changes, any existing adapter must be replaced.
|
||||
if (adapter != null && oldList != null && oldLayoutId != newLayoutId) {
|
||||
adapter.setList(null)
|
||||
adapter = null
|
||||
}
|
||||
// Avoid setting an adapter when there is no new list or layout.
|
||||
if (newList == null || newLayoutId == 0)
|
||||
return
|
||||
if (adapter == null) {
|
||||
adapter = ObservableKeyedRecyclerViewAdapter(view.context, newLayoutId, newList)
|
||||
view.adapter = adapter
|
||||
}
|
||||
adapter.setRowConfigurationHandler(newRowConfigurationHandler)
|
||||
// Either the list changed, or this is an entirely new listener because the layout changed.
|
||||
adapter.setList(newList)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@BindingAdapter("onBeforeCheckedChanged")
|
||||
fun setOnBeforeCheckedChanged(view: ToggleSwitch,
|
||||
listener: OnBeforeCheckedChangeListener?) {
|
||||
view.setOnBeforeCheckedChangeListener(listener)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@BindingAdapter("onFocusChange")
|
||||
fun setOnFocusChange(view: EditText,
|
||||
listener: View.OnFocusChangeListener?) {
|
||||
view.setOnFocusChangeListener(listener)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@BindingAdapter("android:text")
|
||||
fun setOptionalText(view: TextView, text: Optional<*>?) {
|
||||
view.text = text?.map { it.toString() }?.orElse("") ?: ""
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@BindingAdapter("android:text")
|
||||
fun setInetNetworkSetText(view: TextView, networks: Iterable<InetNetwork?>?) {
|
||||
view.text = if (networks != null) Attribute.join(networks) else ""
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@BindingAdapter("android:text")
|
||||
fun setInetAddressSetText(view: TextView, addresses: Iterable<InetAddress?>?) {
|
||||
view.text = if (addresses != null) Attribute.join(addresses.map { it?.hostAddress }) else ""
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun tryParseInt(s: String?): Int {
|
||||
if (s == null)
|
||||
return 0
|
||||
return try {
|
||||
Integer.parseInt(s)
|
||||
} catch (_: Throwable) {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.databinding;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.wireguard.android.BR;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
import androidx.databinding.ObservableList;
|
||||
import androidx.databinding.ViewDataBinding;
|
||||
|
||||
/**
|
||||
* Helper class for binding an ObservableList to the children of a ViewGroup.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
class ItemChangeListener<T> {
|
||||
private final OnListChangedCallback<T> callback = new OnListChangedCallback<>(this);
|
||||
private final ViewGroup container;
|
||||
private final int layoutId;
|
||||
private final LayoutInflater layoutInflater;
|
||||
@Nullable private ObservableList<T> list;
|
||||
|
||||
ItemChangeListener(final ViewGroup container, final int layoutId) {
|
||||
this.container = container;
|
||||
this.layoutId = layoutId;
|
||||
layoutInflater = LayoutInflater.from(container.getContext());
|
||||
}
|
||||
|
||||
private View getView(final int position, @Nullable final View convertView) {
|
||||
ViewDataBinding binding = convertView != null ? DataBindingUtil.getBinding(convertView) : null;
|
||||
if (binding == null) {
|
||||
binding = DataBindingUtil.inflate(layoutInflater, layoutId, container, false);
|
||||
}
|
||||
|
||||
Objects.requireNonNull(list, "Trying to get a view while list is still null");
|
||||
|
||||
binding.setVariable(BR.collection, list);
|
||||
binding.setVariable(BR.item, list.get(position));
|
||||
binding.executePendingBindings();
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
void setList(@Nullable final ObservableList<T> newList) {
|
||||
if (list != null)
|
||||
list.removeOnListChangedCallback(callback);
|
||||
list = newList;
|
||||
if (list != null) {
|
||||
list.addOnListChangedCallback(callback);
|
||||
callback.onChanged(list);
|
||||
} else {
|
||||
container.removeAllViews();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class OnListChangedCallback<T>
|
||||
extends ObservableList.OnListChangedCallback<ObservableList<T>> {
|
||||
|
||||
private final WeakReference<ItemChangeListener<T>> weakListener;
|
||||
|
||||
private OnListChangedCallback(final ItemChangeListener<T> listener) {
|
||||
weakListener = new WeakReference<>(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(final ObservableList<T> sender) {
|
||||
final ItemChangeListener<T> listener = weakListener.get();
|
||||
if (listener != null) {
|
||||
// TODO: recycle views
|
||||
listener.container.removeAllViews();
|
||||
for (int i = 0; i < sender.size(); ++i)
|
||||
listener.container.addView(listener.getView(i, null));
|
||||
} else {
|
||||
sender.removeOnListChangedCallback(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeChanged(final ObservableList<T> sender, final int positionStart,
|
||||
final int itemCount) {
|
||||
final ItemChangeListener<T> listener = weakListener.get();
|
||||
if (listener != null) {
|
||||
for (int i = positionStart; i < positionStart + itemCount; ++i) {
|
||||
final View child = listener.container.getChildAt(i);
|
||||
listener.container.removeViewAt(i);
|
||||
listener.container.addView(listener.getView(i, child));
|
||||
}
|
||||
} else {
|
||||
sender.removeOnListChangedCallback(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeInserted(final ObservableList<T> sender, final int positionStart,
|
||||
final int itemCount) {
|
||||
final ItemChangeListener<T> listener = weakListener.get();
|
||||
if (listener != null) {
|
||||
for (int i = positionStart; i < positionStart + itemCount; ++i)
|
||||
listener.container.addView(listener.getView(i, null));
|
||||
} else {
|
||||
sender.removeOnListChangedCallback(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeMoved(final ObservableList<T> sender, final int fromPosition,
|
||||
final int toPosition, final int itemCount) {
|
||||
final ItemChangeListener<T> listener = weakListener.get();
|
||||
if (listener != null) {
|
||||
final View[] views = new View[itemCount];
|
||||
for (int i = 0; i < itemCount; ++i)
|
||||
views[i] = listener.container.getChildAt(fromPosition + i);
|
||||
listener.container.removeViews(fromPosition, itemCount);
|
||||
for (int i = 0; i < itemCount; ++i)
|
||||
listener.container.addView(views[i], toPosition + i);
|
||||
} else {
|
||||
sender.removeOnListChangedCallback(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeRemoved(final ObservableList<T> sender, final int positionStart,
|
||||
final int itemCount) {
|
||||
final ItemChangeListener<T> listener = weakListener.get();
|
||||
if (listener != null) {
|
||||
listener.container.removeViews(positionStart, itemCount);
|
||||
} else {
|
||||
sender.removeOnListChangedCallback(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.databinding
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ObservableList
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.wireguard.android.BR
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
* Helper class for binding an ObservableList to the children of a ViewGroup.
|
||||
*/
|
||||
internal class ItemChangeListener<T>(private val container: ViewGroup, private val layoutId: Int, private val fragment: Fragment?) {
|
||||
private val callback = OnListChangedCallback(this)
|
||||
private val layoutInflater: LayoutInflater = LayoutInflater.from(container.context)
|
||||
private var list: ObservableList<T>? = null
|
||||
|
||||
private fun getView(position: Int, convertView: View?): View {
|
||||
var binding = if (convertView != null) DataBindingUtil.getBinding<ViewDataBinding>(convertView) else null
|
||||
if (binding == null) {
|
||||
binding = DataBindingUtil.inflate(layoutInflater, layoutId, container, false)
|
||||
}
|
||||
require(list != null) { "Trying to get a view while list is still null" }
|
||||
binding!!.setVariable(BR.collection, list)
|
||||
binding.setVariable(BR.item, list!![position])
|
||||
binding.setVariable(BR.fragment, fragment)
|
||||
binding.executePendingBindings()
|
||||
return binding.root
|
||||
}
|
||||
|
||||
fun setList(newList: ObservableList<T>?) {
|
||||
list?.removeOnListChangedCallback(callback)
|
||||
list = newList
|
||||
if (list != null) {
|
||||
list!!.addOnListChangedCallback(callback)
|
||||
callback.onChanged(list!!)
|
||||
} else {
|
||||
container.removeAllViews()
|
||||
}
|
||||
}
|
||||
|
||||
private class OnListChangedCallback<T> constructor(listener: ItemChangeListener<T>) : ObservableList.OnListChangedCallback<ObservableList<T>>() {
|
||||
private val weakListener: WeakReference<ItemChangeListener<T>> = WeakReference(listener)
|
||||
|
||||
override fun onChanged(sender: ObservableList<T>) {
|
||||
val listener = weakListener.get()
|
||||
if (listener != null) {
|
||||
// TODO: recycle views
|
||||
listener.container.removeAllViews()
|
||||
for (i in sender.indices)
|
||||
listener.container.addView(listener.getView(i, null))
|
||||
} else {
|
||||
sender.removeOnListChangedCallback(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemRangeChanged(sender: ObservableList<T>, positionStart: Int,
|
||||
itemCount: Int) {
|
||||
val listener = weakListener.get()
|
||||
if (listener != null) {
|
||||
for (i in positionStart until positionStart + itemCount) {
|
||||
val child = listener.container.getChildAt(i)
|
||||
listener.container.removeViewAt(i)
|
||||
listener.container.addView(listener.getView(i, child))
|
||||
}
|
||||
} else {
|
||||
sender.removeOnListChangedCallback(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(sender: ObservableList<T>, positionStart: Int,
|
||||
itemCount: Int) {
|
||||
val listener = weakListener.get()
|
||||
if (listener != null) {
|
||||
for (i in positionStart until positionStart + itemCount)
|
||||
listener.container.addView(listener.getView(i, null))
|
||||
} else {
|
||||
sender.removeOnListChangedCallback(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemRangeMoved(sender: ObservableList<T>, fromPosition: Int,
|
||||
toPosition: Int, itemCount: Int) {
|
||||
val listener = weakListener.get()
|
||||
if (listener != null) {
|
||||
val views = arrayOfNulls<View>(itemCount)
|
||||
for (i in 0 until itemCount) views[i] = listener.container.getChildAt(fromPosition + i)
|
||||
listener.container.removeViews(fromPosition, itemCount)
|
||||
for (i in 0 until itemCount) listener.container.addView(views[i], toPosition + i)
|
||||
} else {
|
||||
sender.removeOnListChangedCallback(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemRangeRemoved(sender: ObservableList<T>, positionStart: Int,
|
||||
itemCount: Int) {
|
||||
val listener = weakListener.get()
|
||||
if (listener != null) {
|
||||
listener.container.removeViews(positionStart, itemCount)
|
||||
} else {
|
||||
sender.removeOnListChangedCallback(this)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.databinding
|
||||
|
||||
/**
|
||||
* Interface for objects that have a identifying key of the given type.
|
||||
*/
|
||||
interface Keyed<K> {
|
||||
val key: K
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.databinding
|
||||
|
||||
import androidx.databinding.ObservableArrayList
|
||||
|
||||
/**
|
||||
* ArrayList that allows looking up elements by some key property. As the key property must always
|
||||
* be retrievable, this list cannot hold `null` elements. Because this class places no
|
||||
* restrictions on the order or duplication of keys, lookup by key, as well as all list modification
|
||||
* operations, require O(n) time.
|
||||
*/
|
||||
open class ObservableKeyedArrayList<K, E : Keyed<out K>> : ObservableArrayList<E>() {
|
||||
fun containsKey(key: K) = indexOfKey(key) >= 0
|
||||
|
||||
operator fun get(key: K): E? {
|
||||
val index = indexOfKey(key)
|
||||
return if (index >= 0) get(index) else null
|
||||
}
|
||||
|
||||
open fun indexOfKey(key: K): Int {
|
||||
val iterator = listIterator()
|
||||
while (iterator.hasNext()) {
|
||||
val index = iterator.nextIndex()
|
||||
if (iterator.next()!!.key == key)
|
||||
return index
|
||||
}
|
||||
return -1
|
||||
}
|
||||
}
|
||||
-162
@@ -1,162 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.databinding;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.wireguard.android.BR;
|
||||
import com.wireguard.android.util.ObservableKeyedList;
|
||||
import com.wireguard.util.Keyed;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
import androidx.databinding.ObservableList;
|
||||
import androidx.databinding.ViewDataBinding;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.Adapter;
|
||||
|
||||
/**
|
||||
* A generic {@code RecyclerView.Adapter} backed by a {@code ObservableKeyedList}.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public class ObservableKeyedRecyclerViewAdapter<K, E extends Keyed<? extends K>> extends Adapter<ObservableKeyedRecyclerViewAdapter.ViewHolder> {
|
||||
|
||||
private final OnListChangedCallback<E> callback = new OnListChangedCallback<>(this);
|
||||
private final int layoutId;
|
||||
private final LayoutInflater layoutInflater;
|
||||
@Nullable private ObservableKeyedList<K, E> list;
|
||||
@Nullable private RowConfigurationHandler rowConfigurationHandler;
|
||||
|
||||
ObservableKeyedRecyclerViewAdapter(final Context context, final int layoutId,
|
||||
final ObservableKeyedList<K, E> list) {
|
||||
this.layoutId = layoutId;
|
||||
layoutInflater = LayoutInflater.from(context);
|
||||
setList(list);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private E getItem(final int position) {
|
||||
if (list == null || position < 0 || position >= list.size())
|
||||
return null;
|
||||
return list.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return list != null ? list.size() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(final int position) {
|
||||
final K key = getKey(position);
|
||||
return key != null ? key.hashCode() : -1;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private K getKey(final int position) {
|
||||
final E item = getItem(position);
|
||||
return item != null ? item.getKey() : null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public void onBindViewHolder(final ViewHolder holder, final int position) {
|
||||
holder.binding.setVariable(BR.collection, list);
|
||||
holder.binding.setVariable(BR.key, getKey(position));
|
||||
holder.binding.setVariable(BR.item, getItem(position));
|
||||
holder.binding.executePendingBindings();
|
||||
|
||||
if (rowConfigurationHandler != null) {
|
||||
final E item = getItem(position);
|
||||
if (item != null) {
|
||||
rowConfigurationHandler.onConfigureRow(holder.binding, item, position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
|
||||
return new ViewHolder(DataBindingUtil.inflate(layoutInflater, layoutId, parent, false));
|
||||
}
|
||||
|
||||
void setList(@Nullable final ObservableKeyedList<K, E> newList) {
|
||||
if (list != null)
|
||||
list.removeOnListChangedCallback(callback);
|
||||
list = newList;
|
||||
if (list != null) {
|
||||
list.addOnListChangedCallback(callback);
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
void setRowConfigurationHandler(final RowConfigurationHandler rowConfigurationHandler) {
|
||||
this.rowConfigurationHandler = rowConfigurationHandler;
|
||||
}
|
||||
|
||||
public interface RowConfigurationHandler<B extends ViewDataBinding, T> {
|
||||
void onConfigureRow(B binding, T item, int position);
|
||||
}
|
||||
|
||||
private static final class OnListChangedCallback<E extends Keyed<?>>
|
||||
extends ObservableList.OnListChangedCallback<ObservableList<E>> {
|
||||
|
||||
private final WeakReference<ObservableKeyedRecyclerViewAdapter<?, E>> weakAdapter;
|
||||
|
||||
private OnListChangedCallback(final ObservableKeyedRecyclerViewAdapter<?, E> adapter) {
|
||||
weakAdapter = new WeakReference<>(adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(final ObservableList<E> sender) {
|
||||
final ObservableKeyedRecyclerViewAdapter adapter = weakAdapter.get();
|
||||
if (adapter != null)
|
||||
adapter.notifyDataSetChanged();
|
||||
else
|
||||
sender.removeOnListChangedCallback(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeChanged(final ObservableList<E> sender, final int positionStart,
|
||||
final int itemCount) {
|
||||
onChanged(sender);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeInserted(final ObservableList<E> sender, final int positionStart,
|
||||
final int itemCount) {
|
||||
onChanged(sender);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeMoved(final ObservableList<E> sender, final int fromPosition,
|
||||
final int toPosition, final int itemCount) {
|
||||
onChanged(sender);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeRemoved(final ObservableList<E> sender, final int positionStart,
|
||||
final int itemCount) {
|
||||
onChanged(sender);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
final ViewDataBinding binding;
|
||||
|
||||
public ViewHolder(final ViewDataBinding binding) {
|
||||
super(binding.getRoot());
|
||||
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.databinding
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ObservableList
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.wireguard.android.BR
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
* A generic `RecyclerView.Adapter` backed by a `ObservableKeyedArrayList`.
|
||||
*/
|
||||
class ObservableKeyedRecyclerViewAdapter<K, E : Keyed<out K>> internal constructor(
|
||||
context: Context, private val layoutId: Int,
|
||||
list: ObservableKeyedArrayList<K, E>?
|
||||
) : RecyclerView.Adapter<ObservableKeyedRecyclerViewAdapter.ViewHolder>() {
|
||||
private val callback = OnListChangedCallback(this)
|
||||
private val layoutInflater: LayoutInflater = LayoutInflater.from(context)
|
||||
private var list: ObservableKeyedArrayList<K, E>? = null
|
||||
private var rowConfigurationHandler: RowConfigurationHandler<ViewDataBinding, Any>? = null
|
||||
|
||||
private fun getItem(position: Int): E? = if (list == null || position < 0 || position >= list!!.size) null else list?.get(position)
|
||||
|
||||
override fun getItemCount() = list?.size ?: 0
|
||||
|
||||
override fun getItemId(position: Int) = (getKey(position)?.hashCode() ?: -1).toLong()
|
||||
|
||||
private fun getKey(position: Int): K? = getItem(position)?.key
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.binding.setVariable(BR.collection, list)
|
||||
holder.binding.setVariable(BR.key, getKey(position))
|
||||
holder.binding.setVariable(BR.item, getItem(position))
|
||||
holder.binding.executePendingBindings()
|
||||
if (rowConfigurationHandler != null) {
|
||||
val item = getItem(position)
|
||||
if (item != null) {
|
||||
rowConfigurationHandler?.onConfigureRow(holder.binding, item, position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(DataBindingUtil.inflate(layoutInflater, layoutId, parent, false))
|
||||
|
||||
fun setList(newList: ObservableKeyedArrayList<K, E>?) {
|
||||
list?.removeOnListChangedCallback(callback)
|
||||
list = newList
|
||||
list?.addOnListChangedCallback(callback)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun setRowConfigurationHandler(rowConfigurationHandler: RowConfigurationHandler<*, *>?) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
this.rowConfigurationHandler = rowConfigurationHandler as? RowConfigurationHandler<ViewDataBinding, Any>
|
||||
}
|
||||
|
||||
interface RowConfigurationHandler<B : ViewDataBinding, T> {
|
||||
fun onConfigureRow(binding: B, item: T, position: Int)
|
||||
}
|
||||
|
||||
private class OnListChangedCallback<E : Keyed<*>> constructor(adapter: ObservableKeyedRecyclerViewAdapter<*, E>) : ObservableList.OnListChangedCallback<ObservableList<E>>() {
|
||||
private val weakAdapter: WeakReference<ObservableKeyedRecyclerViewAdapter<*, E>> = WeakReference(adapter)
|
||||
|
||||
override fun onChanged(sender: ObservableList<E>) {
|
||||
val adapter = weakAdapter.get()
|
||||
if (adapter != null)
|
||||
adapter.notifyDataSetChanged()
|
||||
else
|
||||
sender.removeOnListChangedCallback(this)
|
||||
}
|
||||
|
||||
override fun onItemRangeChanged(sender: ObservableList<E>, positionStart: Int,
|
||||
itemCount: Int) {
|
||||
onChanged(sender)
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(sender: ObservableList<E>, positionStart: Int,
|
||||
itemCount: Int) {
|
||||
onChanged(sender)
|
||||
}
|
||||
|
||||
override fun onItemRangeMoved(sender: ObservableList<E>, fromPosition: Int,
|
||||
toPosition: Int, itemCount: Int) {
|
||||
onChanged(sender)
|
||||
}
|
||||
|
||||
override fun onItemRangeRemoved(sender: ObservableList<E>, positionStart: Int,
|
||||
itemCount: Int) {
|
||||
onChanged(sender)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
init {
|
||||
setList(list)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.databinding
|
||||
|
||||
import java.util.AbstractList
|
||||
import java.util.Collections
|
||||
import java.util.Comparator
|
||||
import java.util.Spliterator
|
||||
|
||||
/**
|
||||
* KeyedArrayList that enforces uniqueness and sorted order across the set of keys. This class uses
|
||||
* binary search to improve lookup and replacement times to O(log(n)). However, due to the
|
||||
* array-based nature of this class, insertion and removal of elements with anything but the largest
|
||||
* key still require O(n) time.
|
||||
*/
|
||||
class ObservableSortedKeyedArrayList<K, E : Keyed<out K>>(private val comparator: Comparator<in K>) : ObservableKeyedArrayList<K, E>() {
|
||||
@Transient
|
||||
private val keyList = KeyList(this)
|
||||
|
||||
override fun add(element: E): Boolean {
|
||||
val insertionPoint = getInsertionPoint(element)
|
||||
if (insertionPoint < 0) {
|
||||
// Skipping insertion is non-destructive if the new and existing objects are the same.
|
||||
if (element === get(-insertionPoint - 1)) return false
|
||||
throw IllegalArgumentException("Element with same key already exists in list")
|
||||
}
|
||||
super.add(insertionPoint, element)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun add(index: Int, element: E) {
|
||||
val insertionPoint = getInsertionPoint(element)
|
||||
require(insertionPoint >= 0) { "Element with same key already exists in list" }
|
||||
if (insertionPoint != index) throw IndexOutOfBoundsException("Wrong index given for element")
|
||||
super.add(index, element)
|
||||
}
|
||||
|
||||
override fun addAll(elements: Collection<E>): Boolean {
|
||||
var didChange = false
|
||||
for (e in elements) {
|
||||
if (add(e))
|
||||
didChange = true
|
||||
}
|
||||
return didChange
|
||||
}
|
||||
|
||||
override fun addAll(index: Int, elements: Collection<E>): Boolean {
|
||||
var i = index
|
||||
for (e in elements)
|
||||
add(i++, e)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getInsertionPoint(e: E) = -Collections.binarySearch(keyList, e.key, comparator) - 1
|
||||
|
||||
override fun indexOfKey(key: K): Int {
|
||||
val index = Collections.binarySearch(keyList, key, comparator)
|
||||
return if (index >= 0) index else -1
|
||||
}
|
||||
|
||||
override fun set(index: Int, element: E): E {
|
||||
val order = comparator.compare(element.key, get(index).key)
|
||||
if (order != 0) {
|
||||
// Allow replacement if the new key would be inserted adjacent to the replaced element.
|
||||
val insertionPoint = getInsertionPoint(element)
|
||||
if (insertionPoint < index || insertionPoint > index + 1)
|
||||
throw IndexOutOfBoundsException("Wrong index given for element")
|
||||
}
|
||||
return super.set(index, element)
|
||||
}
|
||||
|
||||
private class KeyList<K, E : Keyed<out K>>(private val list: ObservableSortedKeyedArrayList<K, E>) : AbstractList<K>(), Set<K> {
|
||||
override fun get(index: Int): K = list[index].key
|
||||
|
||||
override val size
|
||||
get() = list.size
|
||||
|
||||
override fun spliterator(): Spliterator<K> = super<AbstractList>.spliterator()
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
package com.wireguard.android.fragment
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
@@ -12,18 +12,17 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewTreeObserver
|
||||
import android.widget.FrameLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.activity.TunnelCreatorActivity
|
||||
import com.wireguard.android.util.resolveAttribute
|
||||
|
||||
class AddTunnelsSheet : BottomSheetDialogFragment() {
|
||||
|
||||
private lateinit var behavior: BottomSheetBehavior<FrameLayout>
|
||||
private var behavior: BottomSheetBehavior<FrameLayout>? = null
|
||||
private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||
}
|
||||
@@ -41,7 +40,13 @@ class AddTunnelsSheet : BottomSheetDialogFragment() {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
if (savedInstanceState != null) dismiss()
|
||||
return inflater.inflate(R.layout.add_tunnels_bottom_sheet, container, false)
|
||||
val view = inflater.inflate(R.layout.add_tunnels_bottom_sheet, container, false)
|
||||
if (activity?.packageManager?.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) != true) {
|
||||
val qrcode = view.findViewById<View>(R.id.create_from_qrcode)
|
||||
qrcode.isEnabled = false
|
||||
qrcode.visibility = View.GONE
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@@ -51,7 +56,7 @@ class AddTunnelsSheet : BottomSheetDialogFragment() {
|
||||
view.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
val dialog = dialog as BottomSheetDialog? ?: return
|
||||
behavior = dialog.behavior
|
||||
behavior.apply {
|
||||
behavior?.apply {
|
||||
state = BottomSheetBehavior.STATE_EXPANDED
|
||||
peekHeight = 0
|
||||
addBottomSheetCallback(bottomSheetCallback)
|
||||
@@ -78,31 +83,26 @@ class AddTunnelsSheet : BottomSheetDialogFragment() {
|
||||
|
||||
override fun dismiss() {
|
||||
super.dismiss()
|
||||
behavior.removeBottomSheetCallback(bottomSheetCallback)
|
||||
}
|
||||
|
||||
private fun requireTargetFragment(): Fragment {
|
||||
return requireNotNull(targetFragment) { "A target fragment should always be set" }
|
||||
behavior?.removeBottomSheetCallback(bottomSheetCallback)
|
||||
}
|
||||
|
||||
private fun onRequestCreateConfig() {
|
||||
startActivity(Intent(activity, TunnelCreatorActivity::class.java))
|
||||
setFragmentResult(REQUEST_KEY_NEW_TUNNEL, bundleOf(REQUEST_METHOD to REQUEST_CREATE))
|
||||
}
|
||||
|
||||
private fun onRequestImportConfig() {
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
}
|
||||
requireTargetFragment().startActivityForResult(intent, TunnelListFragment.REQUEST_IMPORT)
|
||||
setFragmentResult(REQUEST_KEY_NEW_TUNNEL, bundleOf(REQUEST_METHOD to REQUEST_IMPORT))
|
||||
}
|
||||
|
||||
private fun onRequestScanQRCode() {
|
||||
val integrator = IntentIntegrator.forSupportFragment(requireTargetFragment()).apply {
|
||||
setOrientationLocked(false)
|
||||
setBeepEnabled(false)
|
||||
setPrompt(getString(R.string.qr_code_hint))
|
||||
}
|
||||
integrator.initiateScan(listOf(IntentIntegrator.QR_CODE))
|
||||
setFragmentResult(REQUEST_KEY_NEW_TUNNEL, bundleOf(REQUEST_METHOD to REQUEST_SCAN))
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val REQUEST_KEY_NEW_TUNNEL = "request_new_tunnel"
|
||||
const val REQUEST_METHOD = "request_method"
|
||||
const val REQUEST_CREATE = "request_create"
|
||||
const val REQUEST_IMPORT = "request_import"
|
||||
const val REQUEST_SCAN = "request_scan"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,66 +4,104 @@
|
||||
*/
|
||||
package com.wireguard.android.fragment
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Button
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.databinding.Observable
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.wireguard.android.Application
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.wireguard.android.BR
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.databinding.AppListDialogFragmentBinding
|
||||
import com.wireguard.android.databinding.ObservableKeyedArrayList
|
||||
import com.wireguard.android.model.ApplicationData
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import com.wireguard.android.util.ObservableKeyedArrayList
|
||||
import com.wireguard.android.util.ObservableKeyedList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class AppListDialogFragment : DialogFragment() {
|
||||
private val appData: ObservableKeyedList<String, ApplicationData> = ObservableKeyedArrayList()
|
||||
private var currentlyExcludedApps = emptyList<String>()
|
||||
private val appData = ObservableKeyedArrayList<String, ApplicationData>()
|
||||
private var currentlySelectedApps = emptyList<String>()
|
||||
private var initiallyExcluded = false
|
||||
private var button: Button? = null
|
||||
private var tabs: TabLayout? = null
|
||||
|
||||
private fun loadData() {
|
||||
val activity = activity ?: return
|
||||
val pm = activity.packageManager
|
||||
Application.getAsyncWorker().supplyAsync<List<ApplicationData>> {
|
||||
val launcherIntent = Intent(Intent.ACTION_MAIN, null)
|
||||
launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
val resolveInfos = pm.queryIntentActivities(launcherIntent, 0)
|
||||
val applicationData: MutableList<ApplicationData> = ArrayList()
|
||||
resolveInfos.forEach {
|
||||
val packageName = it.activityInfo.packageName
|
||||
applicationData.add(ApplicationData(it.loadIcon(pm), it.loadLabel(pm).toString(), packageName, currentlyExcludedApps.contains(packageName)))
|
||||
}
|
||||
applicationData.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
applicationData
|
||||
}.whenComplete { data, throwable ->
|
||||
if (data != null) {
|
||||
appData.clear()
|
||||
appData.addAll(data)
|
||||
} else {
|
||||
val error = ErrorMessages.get(throwable)
|
||||
val message = activity.getString(R.string.error_fetching_apps, error)
|
||||
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
|
||||
dismissAllowingStateLoss()
|
||||
lifecycleScope.launch(Dispatchers.Default) {
|
||||
try {
|
||||
val applicationData: MutableList<ApplicationData> = ArrayList()
|
||||
withContext(Dispatchers.IO) {
|
||||
val packageInfos = pm.getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET), 0)
|
||||
packageInfos.forEach {
|
||||
val packageName = it.packageName
|
||||
val appInfo = it.applicationInfo
|
||||
val appData = ApplicationData(appInfo.loadIcon(pm), appInfo.loadLabel(pm).toString(), packageName, currentlySelectedApps.contains(packageName))
|
||||
applicationData.add(appData)
|
||||
appData.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
||||
if (propertyId == BR.selected)
|
||||
setButtonText()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
applicationData.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
appData.clear()
|
||||
appData.addAll(applicationData)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
val error = ErrorMessages[e]
|
||||
val message = activity.getString(R.string.error_fetching_apps, error)
|
||||
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val excludedApps = requireArguments().getStringArrayList(KEY_EXCLUDED_APPS)
|
||||
currentlyExcludedApps = (excludedApps ?: emptyList())
|
||||
currentlySelectedApps = (arguments?.getStringArrayList(KEY_SELECTED_APPS) ?: emptyList())
|
||||
initiallyExcluded = arguments?.getBoolean(KEY_IS_EXCLUDED) ?: true
|
||||
}
|
||||
|
||||
private fun setButtonText() {
|
||||
val numSelected = appData.count { it.isSelected }
|
||||
button?.text = if (numSelected == 0)
|
||||
getString(R.string.use_all_applications)
|
||||
else when (tabs?.selectedTabPosition) {
|
||||
0 -> resources.getQuantityString(R.plurals.exclude_n_applications, numSelected, numSelected)
|
||||
1 -> resources.getQuantityString(R.plurals.include_n_applications, numSelected, numSelected)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val alertDialogBuilder = AlertDialog.Builder(requireActivity())
|
||||
alertDialogBuilder.setTitle(R.string.excluded_applications)
|
||||
val binding = AppListDialogFragmentBinding.inflate(requireActivity().layoutInflater, null, false)
|
||||
binding.executePendingBindings()
|
||||
alertDialogBuilder.setView(binding.root)
|
||||
alertDialogBuilder.setPositiveButton(R.string.set_exclusions) { _, _ -> setExclusionsAndDismiss() }
|
||||
tabs = binding.tabs
|
||||
tabs?.apply {
|
||||
selectTab(binding.tabs.getTabAt(if (initiallyExcluded) 0 else 1))
|
||||
addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) = Unit
|
||||
override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) = setButtonText()
|
||||
})
|
||||
}
|
||||
alertDialogBuilder.setPositiveButton(" ") { _, _ -> setSelectionAndDismiss() }
|
||||
alertDialogBuilder.setNegativeButton(R.string.cancel) { dialog, _ -> dialog.dismiss() }
|
||||
alertDialogBuilder.setNeutralButton(R.string.toggle_all) { _, _ -> }
|
||||
binding.fragment = this
|
||||
@@ -71,41 +109,41 @@ class AppListDialogFragment : DialogFragment() {
|
||||
loadData()
|
||||
val dialog = alertDialogBuilder.create()
|
||||
dialog.setOnShowListener {
|
||||
dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener {
|
||||
val selectedItems = appData
|
||||
.filter { it.isExcludedFromTunnel }
|
||||
|
||||
val excludeAll = selectedItems.isEmpty()
|
||||
button = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||
setButtonText()
|
||||
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener { _ ->
|
||||
val selectAll = appData.none { it.isSelected }
|
||||
appData.forEach {
|
||||
it.isExcludedFromTunnel = excludeAll
|
||||
it.isSelected = selectAll
|
||||
}
|
||||
}
|
||||
}
|
||||
return dialog
|
||||
}
|
||||
|
||||
private fun setExclusionsAndDismiss() {
|
||||
val excludedApps: MutableList<String> = ArrayList()
|
||||
private fun setSelectionAndDismiss() {
|
||||
val selectedApps: MutableList<String> = ArrayList()
|
||||
for (data in appData) {
|
||||
if (data.isExcludedFromTunnel) {
|
||||
excludedApps.add(data.packageName)
|
||||
if (data.isSelected) {
|
||||
selectedApps.add(data.packageName)
|
||||
}
|
||||
}
|
||||
(targetFragment as AppExclusionListener?)!!.onExcludedAppsSelected(excludedApps)
|
||||
setFragmentResult(REQUEST_SELECTION, bundleOf(
|
||||
KEY_SELECTED_APPS to selectedApps.toTypedArray(),
|
||||
KEY_IS_EXCLUDED to (tabs?.selectedTabPosition == 0)
|
||||
))
|
||||
dismiss()
|
||||
}
|
||||
|
||||
interface AppExclusionListener {
|
||||
fun onExcludedAppsSelected(excludedApps: List<String>)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_EXCLUDED_APPS = "excludedApps"
|
||||
fun <T> newInstance(excludedApps: ArrayList<String?>?, target: T): AppListDialogFragment where T : Fragment?, T : AppExclusionListener? {
|
||||
const val KEY_SELECTED_APPS = "selected_apps"
|
||||
const val KEY_IS_EXCLUDED = "is_excluded"
|
||||
const val REQUEST_SELECTION = "request_selection"
|
||||
fun newInstance(selectedApps: ArrayList<String?>?, isExcluded: Boolean): AppListDialogFragment {
|
||||
val extras = Bundle()
|
||||
extras.putStringArrayList(KEY_EXCLUDED_APPS, excludedApps)
|
||||
extras.putStringArrayList(KEY_SELECTED_APPS, selectedApps)
|
||||
extras.putBoolean(KEY_IS_EXCLUDED, isExcluded)
|
||||
val fragment = AppListDialogFragment()
|
||||
fragment.setTargetFragment(target, 0)
|
||||
fragment.arguments = extras
|
||||
return fragment
|
||||
}
|
||||
|
||||
@@ -5,62 +5,56 @@
|
||||
package com.wireguard.android.fragment
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.activity.BaseActivity
|
||||
import com.wireguard.android.activity.BaseActivity.OnSelectedTunnelChangedListener
|
||||
import com.wireguard.android.backend.Backend
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.databinding.TunnelDetailFragmentBinding
|
||||
import com.wireguard.android.databinding.TunnelListItemBinding
|
||||
import com.wireguard.android.model.ObservableTunnel
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Base class for fragments that need to know the currently-selected tunnel. Only does anything when
|
||||
* attached to a `BaseActivity`.
|
||||
*/
|
||||
abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
|
||||
private var baseActivity: BaseActivity? = null
|
||||
private var pendingTunnel: ObservableTunnel? = null
|
||||
private var pendingTunnelUp: Boolean? = null
|
||||
protected var selectedTunnel: ObservableTunnel?
|
||||
get() = baseActivity?.selectedTunnel
|
||||
protected set(tunnel) {
|
||||
baseActivity?.selectedTunnel = tunnel
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == REQUEST_CODE_VPN_PERMISSION) {
|
||||
if (pendingTunnel != null && pendingTunnelUp != null) setTunnelStateWithPermissionsResult(pendingTunnel!!, pendingTunnelUp!!)
|
||||
pendingTunnel = null
|
||||
pendingTunnelUp = null
|
||||
}
|
||||
private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
val tunnel = pendingTunnel
|
||||
val checked = pendingTunnelUp
|
||||
if (tunnel != null && checked != null)
|
||||
setTunnelStateWithPermissionsResult(tunnel, checked)
|
||||
pendingTunnel = null
|
||||
pendingTunnelUp = null
|
||||
}
|
||||
|
||||
protected var selectedTunnel: ObservableTunnel?
|
||||
get() = (activity as? BaseActivity)?.selectedTunnel
|
||||
protected set(tunnel) {
|
||||
(activity as? BaseActivity)?.selectedTunnel = tunnel
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
if (context is BaseActivity) {
|
||||
baseActivity = context
|
||||
baseActivity?.addOnSelectedTunnelChangedListener(this)
|
||||
} else {
|
||||
baseActivity = null
|
||||
}
|
||||
(activity as? BaseActivity)?.addOnSelectedTunnelChangedListener(this)
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
baseActivity?.removeOnSelectedTunnelChangedListener(this)
|
||||
baseActivity = null
|
||||
(activity as? BaseActivity)?.removeOnSelectedTunnelChangedListener(this)
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
@@ -70,14 +64,15 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
|
||||
is TunnelListItemBinding -> binding.item
|
||||
else -> return
|
||||
} ?: return
|
||||
Application.getBackendAsync().thenAccept { backend: Backend? ->
|
||||
if (backend is GoBackend) {
|
||||
val intent = GoBackend.VpnService.prepare(view.context)
|
||||
val activity = activity ?: return
|
||||
activity.lifecycleScope.launch {
|
||||
if (Application.getBackend() is GoBackend) {
|
||||
val intent = GoBackend.VpnService.prepare(activity)
|
||||
if (intent != null) {
|
||||
pendingTunnel = tunnel
|
||||
pendingTunnelUp = checked
|
||||
startActivityForResult(intent, REQUEST_CODE_VPN_PERMISSION)
|
||||
return@thenAccept
|
||||
permissionActivityResultLauncher.launch(intent)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
setTunnelStateWithPermissionsResult(tunnel, checked)
|
||||
@@ -85,24 +80,27 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
|
||||
}
|
||||
|
||||
private fun setTunnelStateWithPermissionsResult(tunnel: ObservableTunnel, checked: Boolean) {
|
||||
tunnel.setState(Tunnel.State.of(checked)).whenComplete { _, throwable ->
|
||||
if (throwable == null) return@whenComplete
|
||||
val error = ErrorMessages.get(throwable)
|
||||
val messageResId = if (checked) R.string.error_up else R.string.error_down
|
||||
val message = requireContext().getString(messageResId, error)
|
||||
val view = view
|
||||
if (view != null)
|
||||
Snackbar.make(view, message, Snackbar.LENGTH_LONG)
|
||||
.setAnchorView(view.findViewById<View>(R.id.create_fab))
|
||||
.show()
|
||||
else
|
||||
Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show()
|
||||
Log.e(TAG, message, throwable)
|
||||
val activity = activity ?: return
|
||||
activity.lifecycleScope.launch {
|
||||
try {
|
||||
tunnel.setStateAsync(Tunnel.State.of(checked))
|
||||
} catch (e: Throwable) {
|
||||
val error = ErrorMessages[e]
|
||||
val messageResId = if (checked) R.string.error_up else R.string.error_down
|
||||
val message = activity.getString(messageResId, error)
|
||||
val view = view
|
||||
if (view != null)
|
||||
Snackbar.make(view, message, Snackbar.LENGTH_LONG)
|
||||
.setAnchorView(view.findViewById(R.id.create_fab))
|
||||
.show()
|
||||
else
|
||||
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
|
||||
Log.e(TAG, message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_CODE_VPN_PERMISSION = 23491
|
||||
private val TAG = "WireGuard/" + BaseFragment::class.java.simpleName
|
||||
private const val TAG = "WireGuard/BaseFragment"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,17 +5,20 @@
|
||||
package com.wireguard.android.fragment
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.databinding.ConfigNamingDialogFragmentBinding
|
||||
import com.wireguard.config.BadConfigException
|
||||
import com.wireguard.config.Config
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.nio.charset.StandardCharsets
|
||||
@@ -26,14 +29,15 @@ class ConfigNamingDialogFragment : DialogFragment() {
|
||||
private var imm: InputMethodManager? = null
|
||||
|
||||
private fun createTunnelAndDismiss() {
|
||||
binding?.let {
|
||||
val name = it.tunnelNameText.text.toString()
|
||||
Application.getTunnelManager().create(name, config).whenComplete { tunnel, throwable ->
|
||||
if (tunnel != null) {
|
||||
dismiss()
|
||||
} else {
|
||||
it.tunnelNameTextLayout.error = throwable.message
|
||||
}
|
||||
val binding = binding ?: return
|
||||
val activity = activity ?: return
|
||||
val name = binding.tunnelNameText.text.toString()
|
||||
activity.lifecycleScope.launch {
|
||||
try {
|
||||
Application.getTunnelManager().create(name, config)
|
||||
dismiss()
|
||||
} catch (e: Throwable) {
|
||||
binding.tunnelNameTextLayout.error = e.message
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,8 +53,8 @@ class ConfigNamingDialogFragment : DialogFragment() {
|
||||
val configBytes = configText!!.toByteArray(StandardCharsets.UTF_8)
|
||||
config = try {
|
||||
Config.parse(ByteArrayInputStream(configBytes))
|
||||
} catch(e: Exception) {
|
||||
when(e) {
|
||||
} catch (e: Throwable) {
|
||||
when (e) {
|
||||
is BadConfigException, is IOException -> throw IllegalArgumentException("Invalid config passed to ${javaClass.simpleName}", e)
|
||||
else -> throw e
|
||||
}
|
||||
@@ -59,7 +63,7 @@ class ConfigNamingDialogFragment : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val activity = requireActivity()
|
||||
imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm = activity.getSystemService()
|
||||
val alertDialogBuilder = AlertDialog.Builder(activity)
|
||||
alertDialogBuilder.setTitle(R.string.import_from_qr_code)
|
||||
binding = ConfigNamingDialogFragmentBinding.inflate(activity.layoutInflater, null, false)
|
||||
@@ -69,7 +73,18 @@ class ConfigNamingDialogFragment : DialogFragment() {
|
||||
}
|
||||
alertDialogBuilder.setPositiveButton(R.string.create_tunnel, null)
|
||||
alertDialogBuilder.setNegativeButton(R.string.cancel) { _, _ -> dismiss() }
|
||||
return alertDialogBuilder.create()
|
||||
return alertDialogBuilder.create().apply {
|
||||
setOnShowListener {
|
||||
findViewById<TextInputEditText>(R.id.tunnel_name_text)?.apply {
|
||||
setOnFocusChangeListener { v, _ ->
|
||||
v.post {
|
||||
imm?.showSoftInput(v, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
}
|
||||
requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
||||
@@ -11,15 +11,15 @@ import android.view.MenuInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.databinding.TunnelDetailFragmentBinding
|
||||
import com.wireguard.android.databinding.TunnelDetailPeerBinding
|
||||
import com.wireguard.android.model.ObservableTunnel
|
||||
import com.wireguard.android.widget.EdgeToEdge.setUpRoot
|
||||
import com.wireguard.android.widget.EdgeToEdge.setUpScrollingContent
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
import com.wireguard.android.util.QuantityFormatter
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Fragment that shows details about a specific tunnel.
|
||||
@@ -27,18 +27,7 @@ import java.util.TimerTask
|
||||
class TunnelDetailFragment : BaseFragment() {
|
||||
private var binding: TunnelDetailFragmentBinding? = null
|
||||
private var lastState = Tunnel.State.TOGGLE
|
||||
private var timer: Timer? = null
|
||||
|
||||
private fun formatBytes(bytes: Long): String {
|
||||
val context = requireContext()
|
||||
return when {
|
||||
bytes < 1024 -> context.getString(R.string.transfer_bytes, bytes)
|
||||
bytes < 1024 * 1024 -> context.getString(R.string.transfer_kibibytes, bytes / 1024.0)
|
||||
bytes < 1024 * 1024 * 1024 -> context.getString(R.string.transfer_mibibytes, bytes / (1024.0 * 1024.0))
|
||||
bytes < 1024 * 1024 * 1024 * 1024L -> context.getString(R.string.transfer_gibibytes, bytes / (1024.0 * 1024.0 * 1024.0))
|
||||
else -> context.getString(R.string.transfer_tibibytes, bytes / (1024.0 * 1024.0 * 1024.0) / 1024.0)
|
||||
}
|
||||
}
|
||||
private var timerActive = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -53,11 +42,7 @@ class TunnelDetailFragment : BaseFragment() {
|
||||
savedInstanceState: Bundle?): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
binding = TunnelDetailFragmentBinding.inflate(inflater, container, false)
|
||||
binding?.apply {
|
||||
executePendingBindings()
|
||||
setUpRoot(root as ViewGroup)
|
||||
setUpScrollingContent(root as ViewGroup, null)
|
||||
}
|
||||
binding?.executePendingBindings()
|
||||
return binding!!.root
|
||||
}
|
||||
|
||||
@@ -68,28 +53,36 @@ class TunnelDetailFragment : BaseFragment() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
timer = Timer()
|
||||
timer!!.scheduleAtFixedRate(object : TimerTask() {
|
||||
override fun run() {
|
||||
timerActive = true
|
||||
lifecycleScope.launch {
|
||||
while (timerActive) {
|
||||
updateStats()
|
||||
delay(1000)
|
||||
}
|
||||
}, 0, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?) {
|
||||
binding ?: return
|
||||
binding!!.tunnel = newTunnel
|
||||
if (newTunnel == null) binding!!.config = null else newTunnel.configAsync.thenAccept { config -> binding!!.config = config }
|
||||
val binding = binding ?: return
|
||||
binding.tunnel = newTunnel
|
||||
if (newTunnel == null) {
|
||||
binding.config = null
|
||||
} else {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
binding.config = newTunnel.getConfigAsync()
|
||||
} catch (_: Throwable) {
|
||||
binding.config = null
|
||||
}
|
||||
}
|
||||
}
|
||||
lastState = Tunnel.State.TOGGLE
|
||||
updateStats()
|
||||
lifecycleScope.launch { updateStats() }
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
timerActive = false
|
||||
super.onStop()
|
||||
if (timer != null) {
|
||||
timer!!.cancel()
|
||||
timer = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewStateRestored(savedInstanceState: Bundle?) {
|
||||
@@ -99,24 +92,17 @@ class TunnelDetailFragment : BaseFragment() {
|
||||
super.onViewStateRestored(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun updateStats() {
|
||||
if (binding == null || !isResumed) return
|
||||
val tunnel = binding!!.tunnel ?: return
|
||||
private suspend fun updateStats() {
|
||||
val binding = binding ?: return
|
||||
val tunnel = binding.tunnel ?: return
|
||||
if (!isResumed) return
|
||||
val state = tunnel.state
|
||||
if (state != Tunnel.State.UP && lastState == state) return
|
||||
lastState = state
|
||||
tunnel.statisticsAsync.whenComplete { statistics, throwable ->
|
||||
if (throwable != null) {
|
||||
for (i in 0 until binding!!.peersLayout.childCount) {
|
||||
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding!!.peersLayout.getChildAt(i))
|
||||
?: continue
|
||||
peer.transferLabel.visibility = View.GONE
|
||||
peer.transferText.visibility = View.GONE
|
||||
}
|
||||
return@whenComplete
|
||||
}
|
||||
for (i in 0 until binding!!.peersLayout.childCount) {
|
||||
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding!!.peersLayout.getChildAt(i))
|
||||
try {
|
||||
val statistics = tunnel.getStatisticsAsync()
|
||||
for (i in 0 until binding.peersLayout.childCount) {
|
||||
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i))
|
||||
?: continue
|
||||
val publicKey = peer.item!!.publicKey
|
||||
val rx = statistics.peerRx(publicKey)
|
||||
@@ -126,10 +112,17 @@ class TunnelDetailFragment : BaseFragment() {
|
||||
peer.transferText.visibility = View.GONE
|
||||
continue
|
||||
}
|
||||
peer.transferText.text = requireContext().getString(R.string.transfer_rx_tx, formatBytes(rx), formatBytes(tx))
|
||||
peer.transferText.text = getString(R.string.transfer_rx_tx, QuantityFormatter.formatBytes(rx), QuantityFormatter.formatBytes(tx))
|
||||
peer.transferLabel.visibility = View.VISIBLE
|
||||
peer.transferText.visibility = View.VISIBLE
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
for (i in 0 until binding.peersLayout.childCount) {
|
||||
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i))
|
||||
?: continue
|
||||
peer.transferLabel.visibility = View.GONE
|
||||
peer.transferText.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package com.wireguard.android.fragment
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
@@ -13,45 +14,52 @@ import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.databinding.TunnelEditorFragmentBinding
|
||||
import com.wireguard.android.fragment.AppListDialogFragment.AppExclusionListener
|
||||
import com.wireguard.android.model.ObservableTunnel
|
||||
import com.wireguard.android.widget.EdgeToEdge.setUpRoot
|
||||
import com.wireguard.android.widget.EdgeToEdge.setUpScrollingContent
|
||||
import com.wireguard.android.util.AdminKnobs
|
||||
import com.wireguard.android.util.BiometricAuthenticator
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import com.wireguard.android.viewmodel.ConfigProxy
|
||||
import com.wireguard.config.Config
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Fragment for editing a WireGuard configuration.
|
||||
*/
|
||||
class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
|
||||
class TunnelEditorFragment : BaseFragment() {
|
||||
private var haveShownKeys = false
|
||||
private var binding: TunnelEditorFragmentBinding? = null
|
||||
private var tunnel: ObservableTunnel? = null
|
||||
|
||||
private fun onConfigLoaded(config: Config) {
|
||||
binding?.config = ConfigProxy(config)
|
||||
}
|
||||
|
||||
private fun onConfigSaved(savedTunnel: Tunnel, throwable: Throwable?) {
|
||||
val message: String
|
||||
val ctx = activity ?: Application.get()
|
||||
if (throwable == null) {
|
||||
message = getString(R.string.config_save_success, savedTunnel.name)
|
||||
val message = ctx.getString(R.string.config_save_success, savedTunnel.name)
|
||||
Log.d(TAG, message)
|
||||
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
|
||||
onFinished()
|
||||
} else {
|
||||
val error = ErrorMessages.get(throwable)
|
||||
message = getString(R.string.config_save_error, savedTunnel.name, error)
|
||||
val error = ErrorMessages[throwable]
|
||||
val message = ctx.getString(R.string.config_save_error, savedTunnel.name, error)
|
||||
Log.e(TAG, message, throwable)
|
||||
binding?.let {
|
||||
Snackbar.make(it.mainContainer, message, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
val binding = binding
|
||||
if (binding != null)
|
||||
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show()
|
||||
else
|
||||
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,25 +78,17 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
|
||||
binding = TunnelEditorFragmentBinding.inflate(inflater, container, false)
|
||||
binding?.apply {
|
||||
executePendingBindings()
|
||||
setUpRoot(root as ViewGroup)
|
||||
setUpScrollingContent(mainContainer, null)
|
||||
privateKeyTextLayout.setEndIconOnClickListener { config?.`interface`?.generateKeyPair() }
|
||||
}
|
||||
return binding?.root
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onExcludedAppsSelected(excludedApps: List<String>) {
|
||||
requireNotNull(binding) { "Tried to set excluded apps while no view was loaded" }
|
||||
binding!!.config!!.`interface`.excludedApplications.apply {
|
||||
clear()
|
||||
addAll(excludedApps)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFinished() {
|
||||
// Hide the keyboard; it rarely goes away on its own.
|
||||
val activity = activity ?: return
|
||||
@@ -98,14 +98,11 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
|
||||
inputManager?.hideSoftInputFromWindow(focusedView.windowToken,
|
||||
InputMethodManager.HIDE_NOT_ALWAYS)
|
||||
}
|
||||
// Tell the activity to finish itself or go back to the detail view.
|
||||
requireActivity().runOnUiThread {
|
||||
// TODO(smaeul): Remove this hack when fixing the Config ViewModel
|
||||
// The selected tunnel has to actually change, but we have to remember this one.
|
||||
val savedTunnel = tunnel
|
||||
if (savedTunnel === selectedTunnel) selectedTunnel = null
|
||||
selectedTunnel = savedTunnel
|
||||
}
|
||||
parentFragmentManager.popBackStackImmediate()
|
||||
|
||||
// If we just made a new one, save it to select the details page.
|
||||
if (selectedTunnel != tunnel)
|
||||
selectedTunnel = tunnel
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
@@ -113,30 +110,44 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
|
||||
binding ?: return false
|
||||
val newConfig = try {
|
||||
binding!!.config!!.resolve()
|
||||
} catch (e: Exception) {
|
||||
val error = ErrorMessages.get(e)
|
||||
} catch (e: Throwable) {
|
||||
val error = ErrorMessages[e]
|
||||
val tunnelName = if (tunnel == null) binding!!.name else tunnel!!.name
|
||||
val message = getString(R.string.config_save_error, tunnelName, error)
|
||||
Log.e(TAG, message, e)
|
||||
Snackbar.make(binding!!.mainContainer, error, Snackbar.LENGTH_LONG).show()
|
||||
return false
|
||||
}
|
||||
when {
|
||||
tunnel == null -> {
|
||||
Log.d(TAG, "Attempting to create new tunnel " + binding!!.name)
|
||||
val manager = Application.getTunnelManager()
|
||||
manager.create(binding!!.name!!, newConfig)
|
||||
.whenComplete(this::onTunnelCreated)
|
||||
}
|
||||
tunnel!!.name != binding!!.name -> {
|
||||
Log.d(TAG, "Attempting to rename tunnel to " + binding!!.name)
|
||||
tunnel!!.setName(binding!!.name!!)
|
||||
.whenComplete { _, t -> onTunnelRenamed(tunnel!!, newConfig, t) }
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "Attempting to save config of " + tunnel!!.name)
|
||||
tunnel!!.setConfig(newConfig)
|
||||
.whenComplete { _, t -> onConfigSaved(tunnel!!, t) }
|
||||
val activity = requireActivity()
|
||||
activity.lifecycleScope.launch {
|
||||
when {
|
||||
tunnel == null -> {
|
||||
Log.d(TAG, "Attempting to create new tunnel " + binding!!.name)
|
||||
val manager = Application.getTunnelManager()
|
||||
try {
|
||||
onTunnelCreated(manager.create(binding!!.name!!, newConfig), null)
|
||||
} catch (e: Throwable) {
|
||||
onTunnelCreated(null, e)
|
||||
}
|
||||
}
|
||||
tunnel!!.name != binding!!.name -> {
|
||||
Log.d(TAG, "Attempting to rename tunnel to " + binding!!.name)
|
||||
try {
|
||||
tunnel!!.setNameAsync(binding!!.name!!)
|
||||
onTunnelRenamed(tunnel!!, newConfig, null)
|
||||
} catch (e: Throwable) {
|
||||
onTunnelRenamed(tunnel!!, newConfig, e)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "Attempting to save config of " + tunnel!!.name)
|
||||
try {
|
||||
tunnel!!.setConfigAsync(newConfig)
|
||||
onConfigSaved(tunnel!!, null)
|
||||
} catch (e: Throwable) {
|
||||
onConfigSaved(tunnel!!, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
@@ -145,11 +156,35 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun onRequestSetExcludedApplications(view: View?) {
|
||||
fun onRequestSetExcludedIncludedApplications(view: View?) {
|
||||
if (binding != null) {
|
||||
val excludedApps = ArrayList(binding!!.config!!.`interface`.excludedApplications)
|
||||
val fragment = AppListDialogFragment.newInstance(excludedApps, this)
|
||||
fragment.show(parentFragmentManager, null)
|
||||
var isExcluded = true
|
||||
var selectedApps = ArrayList(binding!!.config!!.`interface`.excludedApplications)
|
||||
if (selectedApps.isEmpty()) {
|
||||
selectedApps = ArrayList(binding!!.config!!.`interface`.includedApplications)
|
||||
if (selectedApps.isNotEmpty())
|
||||
isExcluded = false
|
||||
}
|
||||
val fragment = AppListDialogFragment.newInstance(selectedApps, isExcluded)
|
||||
childFragmentManager.setFragmentResultListener(AppListDialogFragment.REQUEST_SELECTION, viewLifecycleOwner) { _, bundle ->
|
||||
requireNotNull(binding) { "Tried to set excluded/included apps while no view was loaded" }
|
||||
val newSelections = requireNotNull(bundle.getStringArray(AppListDialogFragment.KEY_SELECTED_APPS))
|
||||
val excluded = requireNotNull(bundle.getBoolean(AppListDialogFragment.KEY_IS_EXCLUDED))
|
||||
if (excluded) {
|
||||
binding!!.config!!.`interface`.includedApplications.clear()
|
||||
binding!!.config!!.`interface`.excludedApplications.apply {
|
||||
clear()
|
||||
addAll(newSelections)
|
||||
}
|
||||
} else {
|
||||
binding!!.config!!.`interface`.excludedApplications.clear()
|
||||
binding!!.config!!.`interface`.includedApplications.apply {
|
||||
clear()
|
||||
addAll(newSelections)
|
||||
}
|
||||
}
|
||||
}
|
||||
fragment.show(childFragmentManager, null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,46 +201,60 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
|
||||
binding!!.config = ConfigProxy()
|
||||
if (tunnel != null) {
|
||||
binding!!.name = tunnel!!.name
|
||||
tunnel!!.configAsync.thenAccept(this::onConfigLoaded)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
onConfigLoaded(tunnel!!.getConfigAsync())
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding!!.name = ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTunnelCreated(newTunnel: ObservableTunnel, throwable: Throwable?) {
|
||||
val message: String
|
||||
private fun onTunnelCreated(newTunnel: ObservableTunnel?, throwable: Throwable?) {
|
||||
val ctx = activity ?: Application.get()
|
||||
if (throwable == null) {
|
||||
tunnel = newTunnel
|
||||
message = getString(R.string.tunnel_create_success, tunnel!!.name)
|
||||
val message = ctx.getString(R.string.tunnel_create_success, tunnel!!.name)
|
||||
Log.d(TAG, message)
|
||||
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
|
||||
onFinished()
|
||||
} else {
|
||||
val error = ErrorMessages.get(throwable)
|
||||
message = getString(R.string.tunnel_create_error, error)
|
||||
val error = ErrorMessages[throwable]
|
||||
val message = ctx.getString(R.string.tunnel_create_error, error)
|
||||
Log.e(TAG, message, throwable)
|
||||
binding?.let {
|
||||
Snackbar.make(it.mainContainer, message, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
val binding = binding
|
||||
if (binding != null)
|
||||
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show()
|
||||
else
|
||||
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTunnelRenamed(renamedTunnel: ObservableTunnel, newConfig: Config,
|
||||
throwable: Throwable?) {
|
||||
val message: String
|
||||
private suspend fun onTunnelRenamed(renamedTunnel: ObservableTunnel, newConfig: Config,
|
||||
throwable: Throwable?) {
|
||||
val ctx = activity ?: Application.get()
|
||||
if (throwable == null) {
|
||||
message = getString(R.string.tunnel_rename_success, renamedTunnel.name)
|
||||
val message = ctx.getString(R.string.tunnel_rename_success, renamedTunnel.name)
|
||||
Log.d(TAG, message)
|
||||
// Now save the rest of configuration changes.
|
||||
Log.d(TAG, "Attempting to save config of renamed tunnel " + tunnel!!.name)
|
||||
renamedTunnel.setConfig(newConfig).whenComplete { _, t -> onConfigSaved(renamedTunnel, t) }
|
||||
} else {
|
||||
val error = ErrorMessages.get(throwable)
|
||||
message = getString(R.string.tunnel_rename_error, error)
|
||||
Log.e(TAG, message, throwable)
|
||||
binding?.let {
|
||||
Snackbar.make(it.mainContainer, message, Snackbar.LENGTH_LONG).show()
|
||||
try {
|
||||
renamedTunnel.setConfigAsync(newConfig)
|
||||
onConfigSaved(renamedTunnel, null)
|
||||
} catch (e: Throwable) {
|
||||
onConfigSaved(renamedTunnel, e)
|
||||
}
|
||||
} else {
|
||||
val error = ErrorMessages[throwable]
|
||||
val message = ctx.getString(R.string.tunnel_rename_error, error)
|
||||
Log.e(TAG, message, throwable)
|
||||
val binding = binding
|
||||
if (binding != null)
|
||||
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show()
|
||||
else
|
||||
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,9 +272,46 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
|
||||
super.onViewStateRestored(savedInstanceState)
|
||||
}
|
||||
|
||||
private var showingAuthenticator = false
|
||||
|
||||
fun onKeyClick(view: View) = onKeyFocusChange(view, true)
|
||||
|
||||
fun onKeyFocusChange(view: View, isFocused: Boolean) {
|
||||
if (!isFocused || showingAuthenticator) return
|
||||
val edit = view as? EditText ?: return
|
||||
if (edit.inputType == InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) return
|
||||
if (!haveShownKeys && edit.text.isNotEmpty()) {
|
||||
if (AdminKnobs.disableConfigExport) return
|
||||
showingAuthenticator = true
|
||||
BiometricAuthenticator.authenticate(R.string.biometric_prompt_private_key_title, this) {
|
||||
showingAuthenticator = false
|
||||
when (it) {
|
||||
is BiometricAuthenticator.Result.Success, is BiometricAuthenticator.Result.HardwareUnavailableOrDisabled -> {
|
||||
haveShownKeys = true
|
||||
showPrivateKey(edit)
|
||||
}
|
||||
is BiometricAuthenticator.Result.Failure -> {
|
||||
Snackbar.make(
|
||||
binding!!.mainContainer,
|
||||
it.message,
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showPrivateKey(edit)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showPrivateKey(edit: EditText) {
|
||||
activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
edit.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_LOCAL_CONFIG = "local_config"
|
||||
private const val KEY_ORIGINAL_NAME = "original_name"
|
||||
private val TAG = "WireGuard/" + TunnelEditorFragment::class.java.simpleName
|
||||
private const val TAG = "WireGuard/TunnelEditorFragment"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,46 +4,40 @@
|
||||
*/
|
||||
package com.wireguard.android.fragment
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.OpenableColumns
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.AnimationUtils
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.activity.TunnelCreatorActivity
|
||||
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler
|
||||
import com.wireguard.android.databinding.TunnelListFragmentBinding
|
||||
import com.wireguard.android.databinding.TunnelListItemBinding
|
||||
import com.wireguard.android.fragment.ConfigNamingDialogFragment.Companion.newInstance
|
||||
import com.wireguard.android.model.ObservableTunnel
|
||||
import com.wireguard.android.widget.EdgeToEdge.setUpFAB
|
||||
import com.wireguard.android.widget.EdgeToEdge.setUpRoot
|
||||
import com.wireguard.android.widget.EdgeToEdge.setUpScrollingContent
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import com.wireguard.android.util.TunnelImporter
|
||||
import com.wireguard.android.widget.MultiselectableRelativeLayout
|
||||
import com.wireguard.config.Config
|
||||
import java9.util.concurrent.CompletableFuture
|
||||
import java.io.BufferedReader
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.ArrayList
|
||||
import java.util.HashSet
|
||||
import java.util.Locale
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
/**
|
||||
* Fragment containing a list of known WireGuard tunnels. It allows creating and deleting tunnels.
|
||||
@@ -52,122 +46,25 @@ class TunnelListFragment : BaseFragment() {
|
||||
private val actionModeListener = ActionModeListener()
|
||||
private var actionMode: ActionMode? = null
|
||||
private var binding: TunnelListFragmentBinding? = null
|
||||
private fun importTunnel(configText: String) {
|
||||
try {
|
||||
// Ensure the config text is parseable before proceeding…
|
||||
Config.parse(ByteArrayInputStream(configText.toByteArray(StandardCharsets.UTF_8)))
|
||||
|
||||
// Config text is valid, now create the tunnel…
|
||||
newInstance(configText).show(parentFragmentManager, null)
|
||||
} catch (e: Exception) {
|
||||
onTunnelImportFinished(emptyList(), listOf<Throwable>(e))
|
||||
private val tunnelFileImportResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { data ->
|
||||
if (data == null) return@registerForActivityResult
|
||||
val activity = activity ?: return@registerForActivityResult
|
||||
val contentResolver = activity.contentResolver ?: return@registerForActivityResult
|
||||
activity.lifecycleScope.launch {
|
||||
TunnelImporter.importTunnel(contentResolver, data) { showSnackbar(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun importTunnel(uri: Uri?) {
|
||||
val activity = activity
|
||||
if (activity == null || uri == null) {
|
||||
return
|
||||
}
|
||||
val contentResolver = activity.contentResolver
|
||||
|
||||
val futureTunnels = ArrayList<CompletableFuture<ObservableTunnel>>()
|
||||
val throwables = ArrayList<Throwable>()
|
||||
Application.getAsyncWorker().supplyAsync {
|
||||
val columns = arrayOf(OpenableColumns.DISPLAY_NAME)
|
||||
var name = ""
|
||||
contentResolver.query(uri, columns, null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst() && !cursor.isNull(0)) {
|
||||
name = cursor.getString(0)
|
||||
}
|
||||
}
|
||||
if (name.isEmpty()) {
|
||||
name = Uri.decode(uri.lastPathSegment)
|
||||
}
|
||||
var idx = name.lastIndexOf('/')
|
||||
if (idx >= 0) {
|
||||
require(idx < name.length - 1) { resources.getString(R.string.illegal_filename_error, name) }
|
||||
name = name.substring(idx + 1)
|
||||
}
|
||||
val isZip = name.toLowerCase(Locale.ROOT).endsWith(".zip")
|
||||
if (name.toLowerCase(Locale.ROOT).endsWith(".conf")) {
|
||||
name = name.substring(0, name.length - ".conf".length)
|
||||
} else {
|
||||
require(isZip) { resources.getString(R.string.bad_extension_error) }
|
||||
}
|
||||
|
||||
if (isZip) {
|
||||
ZipInputStream(contentResolver.openInputStream(uri)).use { zip ->
|
||||
val reader = BufferedReader(InputStreamReader(zip, StandardCharsets.UTF_8))
|
||||
var entry: ZipEntry?
|
||||
while (true) {
|
||||
entry = zip.nextEntry ?: break
|
||||
name = entry.name
|
||||
idx = name.lastIndexOf('/')
|
||||
if (idx >= 0) {
|
||||
if (idx >= name.length - 1) {
|
||||
continue
|
||||
}
|
||||
name = name.substring(name.lastIndexOf('/') + 1)
|
||||
}
|
||||
if (name.toLowerCase(Locale.ROOT).endsWith(".conf")) {
|
||||
name = name.substring(0, name.length - ".conf".length)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
try {
|
||||
Config.parse(reader)
|
||||
} catch (e: Exception) {
|
||||
throwables.add(e)
|
||||
null
|
||||
}?.let {
|
||||
futureTunnels.add(Application.getTunnelManager().create(name, it).toCompletableFuture())
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
futureTunnels.add(
|
||||
Application.getTunnelManager().create(
|
||||
name,
|
||||
Config.parse(contentResolver.openInputStream(uri))
|
||||
).toCompletableFuture()
|
||||
)
|
||||
}
|
||||
|
||||
if (futureTunnels.isEmpty()) {
|
||||
if (throwables.size == 1) {
|
||||
throw throwables[0]
|
||||
} else {
|
||||
require(throwables.isNotEmpty()) { resources.getString(R.string.no_configs_error) }
|
||||
}
|
||||
}
|
||||
CompletableFuture.allOf(*futureTunnels.toTypedArray())
|
||||
}.whenComplete { future, exception ->
|
||||
if (exception != null) {
|
||||
onTunnelImportFinished(emptyList(), listOf(exception))
|
||||
} else {
|
||||
future.whenComplete { _, _ ->
|
||||
val tunnels = mutableListOf<ObservableTunnel>()
|
||||
for (futureTunnel in futureTunnels) {
|
||||
val tunnel: ObservableTunnel? = try {
|
||||
futureTunnel.getNow(null)
|
||||
} catch (e: Exception) {
|
||||
throwables.add(e)
|
||||
null
|
||||
}
|
||||
|
||||
if (tunnel != null) {
|
||||
tunnels.add(tunnel)
|
||||
}
|
||||
}
|
||||
onTunnelImportFinished(tunnels, throwables)
|
||||
}
|
||||
}
|
||||
}
|
||||
private val qrImportResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
val qrCode = IntentIntegrator.parseActivityResult(result.resultCode, result.data)?.contents
|
||||
?: return@registerForActivityResult
|
||||
val activity = activity ?: return@registerForActivityResult
|
||||
val fragManager = parentFragmentManager
|
||||
activity.lifecycleScope.launch { TunnelImporter.importTunnel(fragManager, qrCode) { showSnackbar(it) } }
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
if (savedInstanceState != null) {
|
||||
val checkedItems = savedInstanceState.getIntegerArrayList(CHECKED_ITEMS)
|
||||
if (checkedItems != null) {
|
||||
@@ -176,38 +73,33 @@ class TunnelListFragment : BaseFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when (requestCode) {
|
||||
REQUEST_IMPORT -> {
|
||||
if (resultCode == Activity.RESULT_OK && data != null) importTunnel(data.data)
|
||||
return
|
||||
}
|
||||
IntentIntegrator.REQUEST_CODE -> {
|
||||
val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
|
||||
if (result != null && result.contents != null) {
|
||||
importTunnel(result.contents)
|
||||
}
|
||||
return
|
||||
}
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
binding = TunnelListFragmentBinding.inflate(inflater, container, false)
|
||||
val bottomSheet = AddTunnelsSheet()
|
||||
binding?.apply {
|
||||
createFab.setOnClickListener {
|
||||
val bottomSheet = AddTunnelsSheet()
|
||||
bottomSheet.setTargetFragment(fragment, REQUEST_TARGET_FRAGMENT)
|
||||
bottomSheet.show(parentFragmentManager, "BOTTOM_SHEET")
|
||||
childFragmentManager.setFragmentResultListener(AddTunnelsSheet.REQUEST_KEY_NEW_TUNNEL, viewLifecycleOwner) { _, bundle ->
|
||||
when (bundle.getString(AddTunnelsSheet.REQUEST_METHOD)) {
|
||||
AddTunnelsSheet.REQUEST_CREATE -> {
|
||||
startActivity(Intent(requireActivity(), TunnelCreatorActivity::class.java))
|
||||
}
|
||||
AddTunnelsSheet.REQUEST_IMPORT -> {
|
||||
tunnelFileImportResultLauncher.launch("*/*")
|
||||
}
|
||||
AddTunnelsSheet.REQUEST_SCAN -> {
|
||||
qrImportResultLauncher.launch(IntentIntegrator(requireActivity())
|
||||
.setOrientationLocked(false)
|
||||
.setBeepEnabled(false)
|
||||
.setPrompt(getString(R.string.qr_code_hint))
|
||||
.createScanIntent())
|
||||
}
|
||||
}
|
||||
}
|
||||
bottomSheet.show(childFragmentManager, "BOTTOM_SHEET")
|
||||
}
|
||||
executePendingBindings()
|
||||
setUpRoot(root as ViewGroup)
|
||||
setUpFAB(createFab)
|
||||
setUpScrollingContent(tunnelList, createFab)
|
||||
}
|
||||
return binding!!.root
|
||||
}
|
||||
@@ -224,79 +116,65 @@ class TunnelListFragment : BaseFragment() {
|
||||
|
||||
override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?) {
|
||||
binding ?: return
|
||||
Application.getTunnelManager().tunnels.thenAccept { tunnels ->
|
||||
if (newTunnel != null) viewForTunnel(newTunnel, tunnels).setSingleSelected(true)
|
||||
if (oldTunnel != null) viewForTunnel(oldTunnel, tunnels).setSingleSelected(false)
|
||||
lifecycleScope.launch {
|
||||
val tunnels = Application.getTunnelManager().getTunnels()
|
||||
if (newTunnel != null) viewForTunnel(newTunnel, tunnels)?.setSingleSelected(true)
|
||||
if (oldTunnel != null) viewForTunnel(oldTunnel, tunnels)?.setSingleSelected(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTunnelDeletionFinished(count: Int, throwable: Throwable?) {
|
||||
val message: String
|
||||
val ctx = activity ?: Application.get()
|
||||
if (throwable == null) {
|
||||
message = resources.getQuantityString(R.plurals.delete_success, count, count)
|
||||
message = ctx.resources.getQuantityString(R.plurals.delete_success, count, count)
|
||||
} else {
|
||||
val error = ErrorMessages.get(throwable)
|
||||
message = resources.getQuantityString(R.plurals.delete_error, count, count, error)
|
||||
val error = ErrorMessages[throwable]
|
||||
message = ctx.resources.getQuantityString(R.plurals.delete_error, count, count, error)
|
||||
Log.e(TAG, message, throwable)
|
||||
}
|
||||
showSnackbar(message)
|
||||
}
|
||||
|
||||
private fun onTunnelImportFinished(tunnels: List<ObservableTunnel>, throwables: Collection<Throwable>) {
|
||||
var message = ""
|
||||
for (throwable in throwables) {
|
||||
val error = ErrorMessages.get(throwable)
|
||||
message = getString(R.string.import_error, error)
|
||||
Log.e(TAG, message, throwable)
|
||||
}
|
||||
if (tunnels.size == 1 && throwables.isEmpty())
|
||||
message = getString(R.string.import_success, tunnels[0].name)
|
||||
else if (tunnels.isEmpty() && throwables.size == 1)
|
||||
else if (throwables.isEmpty())
|
||||
message = resources.getQuantityString(R.plurals.import_total_success,
|
||||
tunnels.size, tunnels.size)
|
||||
else if (!throwables.isEmpty())
|
||||
message = resources.getQuantityString(R.plurals.import_partial_success,
|
||||
tunnels.size + throwables.size,
|
||||
tunnels.size, tunnels.size + throwables.size)
|
||||
showSnackbar(message)
|
||||
}
|
||||
|
||||
override fun onViewStateRestored(savedInstanceState: Bundle?) {
|
||||
super.onViewStateRestored(savedInstanceState)
|
||||
binding ?: return
|
||||
binding!!.fragment = this
|
||||
Application.getTunnelManager().tunnels.thenAccept { tunnels -> binding!!.tunnels = tunnels }
|
||||
binding!!.rowConfigurationHandler = RowConfigurationHandler { binding: TunnelListItemBinding, tunnel: ObservableTunnel, position ->
|
||||
binding.fragment = this
|
||||
binding.root.setOnClickListener {
|
||||
if (actionMode == null) {
|
||||
selectedTunnel = tunnel
|
||||
} else {
|
||||
actionModeListener.toggleItemChecked(position)
|
||||
lifecycleScope.launch { binding!!.tunnels = Application.getTunnelManager().getTunnels() }
|
||||
binding!!.rowConfigurationHandler = object : RowConfigurationHandler<TunnelListItemBinding, ObservableTunnel> {
|
||||
override fun onConfigureRow(binding: TunnelListItemBinding, item: ObservableTunnel, position: Int) {
|
||||
binding.fragment = this@TunnelListFragment
|
||||
binding.root.setOnClickListener {
|
||||
if (actionMode == null) {
|
||||
selectedTunnel = item
|
||||
} else {
|
||||
actionModeListener.toggleItemChecked(position)
|
||||
}
|
||||
}
|
||||
binding.root.setOnLongClickListener {
|
||||
actionModeListener.toggleItemChecked(position)
|
||||
true
|
||||
}
|
||||
if (actionMode != null)
|
||||
(binding.root as MultiselectableRelativeLayout).setMultiSelected(actionModeListener.checkedItems.contains(position))
|
||||
else
|
||||
(binding.root as MultiselectableRelativeLayout).setSingleSelected(selectedTunnel == item)
|
||||
}
|
||||
binding.root.setOnLongClickListener {
|
||||
actionModeListener.toggleItemChecked(position)
|
||||
true
|
||||
}
|
||||
if (actionMode != null)
|
||||
(binding.root as MultiselectableRelativeLayout).setMultiSelected(actionModeListener.checkedItems.contains(position))
|
||||
else
|
||||
(binding.root as MultiselectableRelativeLayout).setSingleSelected(selectedTunnel == tunnel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSnackbar(message: CharSequence) {
|
||||
binding?.let {
|
||||
Snackbar.make(it.mainContainer, message, Snackbar.LENGTH_LONG)
|
||||
.setAnchorView(it.createFab)
|
||||
val binding = binding
|
||||
if (binding != null)
|
||||
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG)
|
||||
.setAnchorView(binding.createFab)
|
||||
.show()
|
||||
}
|
||||
else
|
||||
Toast.makeText(activity ?: Application.get(), message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun viewForTunnel(tunnel: ObservableTunnel, tunnels: List<*>): MultiselectableRelativeLayout {
|
||||
return binding!!.tunnelList.findViewHolderForAdapterPosition(tunnels.indexOf(tunnel))!!.itemView as MultiselectableRelativeLayout
|
||||
private fun viewForTunnel(tunnel: ObservableTunnel, tunnels: List<*>): MultiselectableRelativeLayout? {
|
||||
return binding?.tunnelList?.findViewHolderForAdapterPosition(tunnels.indexOf(tunnel))?.itemView as? MultiselectableRelativeLayout
|
||||
}
|
||||
|
||||
private inner class ActionModeListener : ActionMode.Callback {
|
||||
@@ -310,21 +188,31 @@ class TunnelListFragment : BaseFragment() {
|
||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.menu_action_delete -> {
|
||||
val activity = activity ?: return true
|
||||
val copyCheckedItems = HashSet(checkedItems)
|
||||
Application.getTunnelManager().tunnels.thenAccept { tunnels ->
|
||||
val tunnelsToDelete = ArrayList<ObservableTunnel>()
|
||||
for (position in copyCheckedItems) tunnelsToDelete.add(tunnels[position])
|
||||
val futures = tunnelsToDelete.map { it.delete().toCompletableFuture() }.toTypedArray()
|
||||
CompletableFuture.allOf(*futures)
|
||||
.thenApply { futures.size }
|
||||
.whenComplete(this@TunnelListFragment::onTunnelDeletionFinished)
|
||||
binding?.createFab?.apply {
|
||||
visibility = View.VISIBLE
|
||||
scaleX = 1f
|
||||
scaleY = 1f
|
||||
}
|
||||
activity.lifecycleScope.launch {
|
||||
try {
|
||||
val tunnels = Application.getTunnelManager().getTunnels()
|
||||
val tunnelsToDelete = ArrayList<ObservableTunnel>()
|
||||
for (position in copyCheckedItems) tunnelsToDelete.add(tunnels[position])
|
||||
val futures = tunnelsToDelete.map { async(SupervisorJob()) { it.deleteAsync() } }
|
||||
onTunnelDeletionFinished(futures.awaitAll().size, null)
|
||||
} catch (e: Throwable) {
|
||||
onTunnelDeletionFinished(0, e)
|
||||
}
|
||||
}
|
||||
checkedItems.clear()
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
R.id.menu_action_select_all -> {
|
||||
Application.getTunnelManager().tunnels.thenAccept { tunnels ->
|
||||
lifecycleScope.launch {
|
||||
val tunnels = Application.getTunnelManager().getTunnels()
|
||||
for (i in 0 until tunnels.size) {
|
||||
setItemChecked(i, true)
|
||||
}
|
||||
@@ -340,14 +228,16 @@ class TunnelListFragment : BaseFragment() {
|
||||
if (activity != null) {
|
||||
resources = activity!!.resources
|
||||
}
|
||||
animateFab(binding?.createFab, false)
|
||||
mode.menuInflater.inflate(R.menu.tunnel_list_action_mode, menu)
|
||||
binding!!.tunnelList.adapter!!.notifyDataSetChanged()
|
||||
binding?.tunnelList?.adapter?.notifyDataSetChanged()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode) {
|
||||
actionMode = null
|
||||
resources = null
|
||||
animateFab(binding?.createFab, true)
|
||||
checkedItems.clear()
|
||||
binding!!.tunnelList.adapter!!.notifyDataSetChanged()
|
||||
}
|
||||
@@ -388,12 +278,30 @@ class TunnelListFragment : BaseFragment() {
|
||||
mode.title = resources!!.getQuantityString(R.plurals.delete_title, count, count)
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateFab(view: View?, show: Boolean) {
|
||||
view ?: return
|
||||
val animation = AnimationUtils.loadAnimation(
|
||||
context, if (show) R.anim.scale_up else R.anim.scale_down
|
||||
)
|
||||
animation.setAnimationListener(object : Animation.AnimationListener {
|
||||
override fun onAnimationRepeat(animation: Animation?) {
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animation?) {
|
||||
if (!show) view.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onAnimationStart(animation: Animation?) {
|
||||
if (show) view.visibility = View.VISIBLE
|
||||
}
|
||||
})
|
||||
view.startAnimation(animation)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val REQUEST_IMPORT = 1
|
||||
private const val REQUEST_TARGET_FRAGMENT = 2
|
||||
private const val CHECKED_ITEMS = "CHECKED_ITEMS"
|
||||
private val TAG = "WireGuard/" + TunnelListFragment::class.java.simpleName
|
||||
private const val TAG = "WireGuard/TunnelListFragment"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,17 +8,15 @@ import android.graphics.drawable.Drawable
|
||||
import androidx.databinding.BaseObservable
|
||||
import androidx.databinding.Bindable
|
||||
import com.wireguard.android.BR
|
||||
import com.wireguard.util.Keyed
|
||||
import com.wireguard.android.databinding.Keyed
|
||||
|
||||
class ApplicationData(val icon: Drawable, val name: String, val packageName: String, isExcludedFromTunnel : Boolean) : BaseObservable(), Keyed<String> {
|
||||
override fun getKey(): String {
|
||||
return name
|
||||
}
|
||||
class ApplicationData(val icon: Drawable, val name: String, val packageName: String, isSelected: Boolean) : BaseObservable(), Keyed<String> {
|
||||
override val key = name
|
||||
|
||||
@get:Bindable
|
||||
var isExcludedFromTunnel = isExcludedFromTunnel
|
||||
var isSelected = isSelected
|
||||
set(value) {
|
||||
field = value
|
||||
notifyPropertyChanged(BR.excludedFromTunnel)
|
||||
notifyPropertyChanged(BR.selected)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.model;
|
||||
|
||||
import com.wireguard.android.BR;
|
||||
import com.wireguard.android.backend.Statistics;
|
||||
import com.wireguard.android.backend.Tunnel;
|
||||
import com.wireguard.android.util.ExceptionLoggers;
|
||||
import com.wireguard.config.Config;
|
||||
import com.wireguard.util.Keyed;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.databinding.BaseObservable;
|
||||
import androidx.databinding.Bindable;
|
||||
import java9.util.concurrent.CompletableFuture;
|
||||
import java9.util.concurrent.CompletionStage;
|
||||
|
||||
/**
|
||||
* Encapsulates the volatile and nonvolatile state of a WireGuard tunnel.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public class ObservableTunnel extends BaseObservable implements Keyed<String>, Tunnel {
|
||||
private final TunnelManager manager;
|
||||
@Nullable private Config config;
|
||||
private String name;
|
||||
private State state;
|
||||
@Nullable private Statistics statistics;
|
||||
|
||||
ObservableTunnel(final TunnelManager manager, final String name,
|
||||
@Nullable final Config config, final State state) {
|
||||
this.name = name;
|
||||
this.manager = manager;
|
||||
this.config = config;
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public CompletionStage<Void> delete() {
|
||||
return manager.delete(this);
|
||||
}
|
||||
|
||||
@Bindable
|
||||
@Nullable
|
||||
public Config getConfig() {
|
||||
if (config == null)
|
||||
manager.getTunnelConfig(this).whenComplete(ExceptionLoggers.E);
|
||||
return config;
|
||||
}
|
||||
|
||||
public CompletionStage<Config> getConfigAsync() {
|
||||
if (config == null)
|
||||
return manager.getTunnelConfig(this);
|
||||
return CompletableFuture.completedFuture(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Bindable
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public State getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public CompletionStage<State> getStateAsync() {
|
||||
return TunnelManager.getTunnelState(this);
|
||||
}
|
||||
|
||||
@Bindable
|
||||
@Nullable
|
||||
public Statistics getStatistics() {
|
||||
if (statistics == null || statistics.isStale())
|
||||
TunnelManager.getTunnelStatistics(this).whenComplete(ExceptionLoggers.E);
|
||||
return statistics;
|
||||
}
|
||||
|
||||
public CompletionStage<Statistics> getStatisticsAsync() {
|
||||
if (statistics == null || statistics.isStale())
|
||||
return TunnelManager.getTunnelStatistics(this);
|
||||
return CompletableFuture.completedFuture(statistics);
|
||||
}
|
||||
|
||||
Config onConfigChanged(final Config config) {
|
||||
this.config = config;
|
||||
notifyPropertyChanged(BR.config);
|
||||
return config;
|
||||
}
|
||||
|
||||
String onNameChanged(final String name) {
|
||||
this.name = name;
|
||||
notifyPropertyChanged(BR.name);
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStateChange(final State newState) {
|
||||
onStateChanged(newState);
|
||||
}
|
||||
|
||||
State onStateChanged(final State state) {
|
||||
if (state != State.UP)
|
||||
onStatisticsChanged(null);
|
||||
this.state = state;
|
||||
notifyPropertyChanged(BR.state);
|
||||
return state;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Statistics onStatisticsChanged(@Nullable final Statistics statistics) {
|
||||
this.statistics = statistics;
|
||||
notifyPropertyChanged(BR.statistics);
|
||||
return statistics;
|
||||
}
|
||||
|
||||
public CompletionStage<Config> setConfig(final Config config) {
|
||||
if (!config.equals(this.config))
|
||||
return manager.setTunnelConfig(this, config);
|
||||
return CompletableFuture.completedFuture(this.config);
|
||||
}
|
||||
|
||||
public CompletionStage<String> setName(final String name) {
|
||||
if (!name.equals(this.name))
|
||||
return manager.setTunnelName(this, name);
|
||||
return CompletableFuture.completedFuture(this.name);
|
||||
}
|
||||
|
||||
public CompletionStage<State> setState(final State state) {
|
||||
if (state != this.state)
|
||||
return manager.setTunnelState(this, state);
|
||||
return CompletableFuture.completedFuture(this.state);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.model
|
||||
|
||||
import android.util.Log
|
||||
import androidx.databinding.BaseObservable
|
||||
import androidx.databinding.Bindable
|
||||
import com.wireguard.android.BR
|
||||
import com.wireguard.android.backend.Statistics
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.databinding.Keyed
|
||||
import com.wireguard.android.util.applicationScope
|
||||
import com.wireguard.config.Config
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
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
|
||||
) : BaseObservable(), Keyed<String>, Tunnel {
|
||||
override val key
|
||||
get() = name
|
||||
|
||||
@Bindable
|
||||
override fun getName() = name
|
||||
|
||||
suspend fun setNameAsync(name: String): String = withContext(Dispatchers.Main.immediate) {
|
||||
if (name != this@ObservableTunnel.name)
|
||||
manager.setTunnelName(this@ObservableTunnel, name)
|
||||
else
|
||||
this@ObservableTunnel.name
|
||||
}
|
||||
|
||||
fun onNameChanged(name: String): String {
|
||||
this.name = name
|
||||
notifyPropertyChanged(BR.name)
|
||||
return name
|
||||
}
|
||||
|
||||
|
||||
@get:Bindable
|
||||
var state = state
|
||||
private set
|
||||
|
||||
override fun onStateChange(newState: Tunnel.State) {
|
||||
onStateChanged(newState)
|
||||
}
|
||||
|
||||
fun onStateChanged(state: Tunnel.State): Tunnel.State {
|
||||
if (state != Tunnel.State.UP) onStatisticsChanged(null)
|
||||
this.state = state
|
||||
notifyPropertyChanged(BR.state)
|
||||
return state
|
||||
}
|
||||
|
||||
suspend fun setStateAsync(state: Tunnel.State): Tunnel.State = withContext(Dispatchers.Main.immediate) {
|
||||
if (state != this@ObservableTunnel.state)
|
||||
manager.setTunnelState(this@ObservableTunnel, state)
|
||||
else
|
||||
this@ObservableTunnel.state
|
||||
}
|
||||
|
||||
|
||||
@get:Bindable
|
||||
var config = config
|
||||
get() {
|
||||
if (field == null)
|
||||
// Opportunistically fetch this if we don't have a cached one, and rely on data bindings to update it eventually
|
||||
applicationScope.launch {
|
||||
try {
|
||||
manager.getTunnelConfig(this@ObservableTunnel)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
}
|
||||
return field
|
||||
}
|
||||
private set
|
||||
|
||||
suspend fun getConfigAsync(): Config = withContext(Dispatchers.Main.immediate) {
|
||||
config ?: manager.getTunnelConfig(this@ObservableTunnel)
|
||||
}
|
||||
|
||||
suspend fun setConfigAsync(config: Config): Config = withContext(Dispatchers.Main.immediate) {
|
||||
this@ObservableTunnel.config.let {
|
||||
if (config != it)
|
||||
manager.setTunnelConfig(this@ObservableTunnel, config)
|
||||
else
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
fun onConfigChanged(config: Config?): Config? {
|
||||
this.config = config
|
||||
notifyPropertyChanged(BR.config)
|
||||
return config
|
||||
}
|
||||
|
||||
|
||||
@get:Bindable
|
||||
var statistics: Statistics? = null
|
||||
get() {
|
||||
if (field == null || field?.isStale != false)
|
||||
// Opportunistically fetch this if we don't have a cached one, and rely on data bindings to update it eventually
|
||||
applicationScope.launch {
|
||||
try {
|
||||
manager.getTunnelStatistics(this@ObservableTunnel)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
}
|
||||
return field
|
||||
}
|
||||
private set
|
||||
|
||||
suspend fun getStatisticsAsync(): Statistics = withContext(Dispatchers.Main.immediate) {
|
||||
statistics.let {
|
||||
if (it == null || it.isStale)
|
||||
manager.getTunnelStatistics(this@ObservableTunnel)
|
||||
else
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
fun onStatisticsChanged(statistics: Statistics?): Statistics? {
|
||||
this.statistics = statistics
|
||||
notifyPropertyChanged(BR.statistics)
|
||||
return statistics
|
||||
}
|
||||
|
||||
|
||||
suspend fun deleteAsync() = manager.delete(this)
|
||||
|
||||
|
||||
companion object {
|
||||
private const val TAG = "WireGuard/ObservableTunnel"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.model
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
object TunnelComparator : Comparator<String> {
|
||||
private class NaturalSortString(originalString: String) {
|
||||
class NaturalSortToken(val maybeString: String?, val maybeNumber: Int?) : Comparable<NaturalSortToken> {
|
||||
override fun compareTo(other: NaturalSortToken): Int {
|
||||
if (maybeString == null) {
|
||||
if (other.maybeString != null || maybeNumber!! < other.maybeNumber!!) {
|
||||
return -1
|
||||
} else if (maybeNumber > other.maybeNumber) {
|
||||
return 1
|
||||
}
|
||||
} else if (other.maybeString == null || maybeString > other.maybeString) {
|
||||
return 1
|
||||
} else if (maybeString < other.maybeString) {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
val tokens: MutableList<NaturalSortToken> = ArrayList()
|
||||
|
||||
init {
|
||||
for (s in NATURAL_SORT_DIGIT_FINDER.findAll(originalString.split(WHITESPACE_FINDER).joinToString(" ").toLowerCase(Locale.ENGLISH))) {
|
||||
try {
|
||||
val n = s.value.toInt()
|
||||
tokens.add(NaturalSortToken(null, n))
|
||||
} catch (_: NumberFormatException) {
|
||||
tokens.add(NaturalSortToken(s.value, null))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val NATURAL_SORT_DIGIT_FINDER = Regex("""\d+|\D+""")
|
||||
private val WHITESPACE_FINDER = Regex("""\s""")
|
||||
}
|
||||
}
|
||||
|
||||
override fun compare(a: String, b: String): Int {
|
||||
if (a == b)
|
||||
return 0
|
||||
val na = NaturalSortString(a)
|
||||
val nb = NaturalSortString(b)
|
||||
for (i in 0 until nb.tokens.size) {
|
||||
if (i == na.tokens.size) {
|
||||
return -1
|
||||
}
|
||||
val c = na.tokens[i].compareTo(nb.tokens[i])
|
||||
if (c != 0)
|
||||
return c
|
||||
}
|
||||
return 1
|
||||
}
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.model;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.BR;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.backend.Statistics;
|
||||
import com.wireguard.android.backend.Tunnel;
|
||||
import com.wireguard.android.backend.Tunnel.State;
|
||||
import com.wireguard.android.configStore.ConfigStore;
|
||||
import com.wireguard.android.util.ExceptionLoggers;
|
||||
import com.wireguard.android.util.ObservableSortedKeyedArrayList;
|
||||
import com.wireguard.android.util.ObservableSortedKeyedList;
|
||||
import com.wireguard.config.Config;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.databinding.BaseObservable;
|
||||
import androidx.databinding.Bindable;
|
||||
import java9.util.Comparators;
|
||||
import java9.util.concurrent.CompletableFuture;
|
||||
import java9.util.concurrent.CompletionStage;
|
||||
import java9.util.stream.Collectors;
|
||||
import java9.util.stream.StreamSupport;
|
||||
|
||||
/**
|
||||
* Maintains and mediates changes to the set of available WireGuard tunnels,
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public final class TunnelManager extends BaseObservable {
|
||||
private static final Comparator<String> COMPARATOR = Comparators.<String>thenComparing(
|
||||
String.CASE_INSENSITIVE_ORDER, Comparators.naturalOrder());
|
||||
private static final String KEY_LAST_USED_TUNNEL = "last_used_tunnel";
|
||||
private static final String KEY_RESTORE_ON_BOOT = "restore_on_boot";
|
||||
private static final String KEY_RUNNING_TUNNELS = "enabled_configs";
|
||||
|
||||
private final CompletableFuture<ObservableSortedKeyedList<String, ObservableTunnel>> completableTunnels = new CompletableFuture<>();
|
||||
private final ConfigStore configStore;
|
||||
private final Context context = Application.get();
|
||||
private final ArrayList<CompletableFuture<Void>> delayedLoadRestoreTunnels = new ArrayList<>();
|
||||
private final ObservableSortedKeyedList<String, ObservableTunnel> tunnels = new ObservableSortedKeyedArrayList<>(COMPARATOR);
|
||||
private boolean haveLoaded;
|
||||
@Nullable private ObservableTunnel lastUsedTunnel;
|
||||
|
||||
public TunnelManager(final ConfigStore configStore) {
|
||||
this.configStore = configStore;
|
||||
}
|
||||
|
||||
static CompletionStage<State> getTunnelState(final ObservableTunnel tunnel) {
|
||||
return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getState(tunnel))
|
||||
.thenApply(tunnel::onStateChanged);
|
||||
}
|
||||
|
||||
static CompletionStage<Statistics> getTunnelStatistics(final ObservableTunnel tunnel) {
|
||||
return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getStatistics(tunnel))
|
||||
.thenApply(tunnel::onStatisticsChanged);
|
||||
}
|
||||
|
||||
private ObservableTunnel addToList(final String name, @Nullable final Config config, final State state) {
|
||||
final ObservableTunnel tunnel = new ObservableTunnel(this, name, config, state);
|
||||
tunnels.add(tunnel);
|
||||
return tunnel;
|
||||
}
|
||||
|
||||
public CompletionStage<ObservableTunnel> create(final String name, @Nullable final Config config) {
|
||||
if (Tunnel.isNameInvalid(name))
|
||||
return CompletableFuture.failedFuture(new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)));
|
||||
if (tunnels.containsKey(name)) {
|
||||
final String message = context.getString(R.string.tunnel_error_already_exists, name);
|
||||
return CompletableFuture.failedFuture(new IllegalArgumentException(message));
|
||||
}
|
||||
return Application.getAsyncWorker().supplyAsync(() -> configStore.create(name, config))
|
||||
.thenApply(savedConfig -> addToList(name, savedConfig, State.DOWN));
|
||||
}
|
||||
|
||||
CompletionStage<Void> delete(final ObservableTunnel tunnel) {
|
||||
final State originalState = tunnel.getState();
|
||||
final boolean wasLastUsed = tunnel == lastUsedTunnel;
|
||||
// Make sure nothing touches the tunnel.
|
||||
if (wasLastUsed)
|
||||
setLastUsedTunnel(null);
|
||||
tunnels.remove(tunnel);
|
||||
return Application.getAsyncWorker().runAsync(() -> {
|
||||
if (originalState == State.UP)
|
||||
Application.getBackend().setState(tunnel, State.DOWN, null);
|
||||
try {
|
||||
configStore.delete(tunnel.getName());
|
||||
} catch (final Exception e) {
|
||||
if (originalState == State.UP)
|
||||
Application.getBackend().setState(tunnel, State.UP, tunnel.getConfig());
|
||||
// Re-throw the exception to fail the completion.
|
||||
throw e;
|
||||
}
|
||||
}).whenComplete((x, e) -> {
|
||||
if (e == null)
|
||||
return;
|
||||
// Failure, put the tunnel back.
|
||||
tunnels.add(tunnel);
|
||||
if (wasLastUsed)
|
||||
setLastUsedTunnel(tunnel);
|
||||
});
|
||||
}
|
||||
|
||||
@Bindable
|
||||
@Nullable
|
||||
public ObservableTunnel getLastUsedTunnel() {
|
||||
return lastUsedTunnel;
|
||||
}
|
||||
|
||||
CompletionStage<Config> getTunnelConfig(final ObservableTunnel tunnel) {
|
||||
return Application.getAsyncWorker().supplyAsync(() -> configStore.load(tunnel.getName()))
|
||||
.thenApply(tunnel::onConfigChanged);
|
||||
}
|
||||
|
||||
public CompletableFuture<ObservableSortedKeyedList<String, ObservableTunnel>> getTunnels() {
|
||||
return completableTunnels;
|
||||
}
|
||||
|
||||
public void onCreate() {
|
||||
Application.getAsyncWorker().supplyAsync(configStore::enumerate)
|
||||
.thenAcceptBoth(Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getRunningTunnelNames()), this::onTunnelsLoaded)
|
||||
.whenComplete(ExceptionLoggers.E);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void onTunnelsLoaded(final Iterable<String> present, final Collection<String> running) {
|
||||
for (final String name : present)
|
||||
addToList(name, null, running.contains(name) ? State.UP : State.DOWN);
|
||||
final String lastUsedName = Application.getSharedPreferences().getString(KEY_LAST_USED_TUNNEL, null);
|
||||
if (lastUsedName != null)
|
||||
setLastUsedTunnel(tunnels.get(lastUsedName));
|
||||
final CompletableFuture<Void>[] toComplete;
|
||||
synchronized (delayedLoadRestoreTunnels) {
|
||||
haveLoaded = true;
|
||||
toComplete = delayedLoadRestoreTunnels.toArray(new CompletableFuture[delayedLoadRestoreTunnels.size()]);
|
||||
delayedLoadRestoreTunnels.clear();
|
||||
}
|
||||
restoreState(true).whenComplete((v, t) -> {
|
||||
for (final CompletableFuture<Void> f : toComplete) {
|
||||
if (t == null)
|
||||
f.complete(v);
|
||||
else
|
||||
f.completeExceptionally(t);
|
||||
}
|
||||
});
|
||||
|
||||
completableTunnels.complete(tunnels);
|
||||
}
|
||||
|
||||
public void refreshTunnelStates() {
|
||||
Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getRunningTunnelNames())
|
||||
.thenAccept(running -> {
|
||||
for (final ObservableTunnel tunnel : tunnels)
|
||||
tunnel.onStateChanged(running.contains(tunnel.getName()) ? State.UP : State.DOWN);
|
||||
})
|
||||
.whenComplete(ExceptionLoggers.E);
|
||||
}
|
||||
|
||||
public CompletionStage<Void> restoreState(final boolean force) {
|
||||
if (!force && !Application.getSharedPreferences().getBoolean(KEY_RESTORE_ON_BOOT, false))
|
||||
return CompletableFuture.completedFuture(null);
|
||||
synchronized (delayedLoadRestoreTunnels) {
|
||||
if (!haveLoaded) {
|
||||
final CompletableFuture<Void> f = new CompletableFuture<>();
|
||||
delayedLoadRestoreTunnels.add(f);
|
||||
return f;
|
||||
}
|
||||
}
|
||||
final Set<String> previouslyRunning = Application.getSharedPreferences().getStringSet(KEY_RUNNING_TUNNELS, null);
|
||||
if (previouslyRunning == null)
|
||||
return CompletableFuture.completedFuture(null);
|
||||
return CompletableFuture.allOf(StreamSupport.stream(tunnels)
|
||||
.filter(tunnel -> previouslyRunning.contains(tunnel.getName()))
|
||||
.map(tunnel -> setTunnelState(tunnel, State.UP))
|
||||
.toArray(CompletableFuture[]::new));
|
||||
}
|
||||
|
||||
public void saveState() {
|
||||
final Set<String> runningTunnels = StreamSupport.stream(tunnels)
|
||||
.filter(tunnel -> tunnel.getState() == State.UP)
|
||||
.map(ObservableTunnel::getName)
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
Application.getSharedPreferences().edit().putStringSet(KEY_RUNNING_TUNNELS, runningTunnels).apply();
|
||||
}
|
||||
|
||||
private void setLastUsedTunnel(@Nullable final ObservableTunnel tunnel) {
|
||||
if (tunnel == lastUsedTunnel)
|
||||
return;
|
||||
lastUsedTunnel = tunnel;
|
||||
notifyPropertyChanged(BR.lastUsedTunnel);
|
||||
if (tunnel != null)
|
||||
Application.getSharedPreferences().edit().putString(KEY_LAST_USED_TUNNEL, tunnel.getName()).apply();
|
||||
else
|
||||
Application.getSharedPreferences().edit().remove(KEY_LAST_USED_TUNNEL).apply();
|
||||
}
|
||||
|
||||
CompletionStage<Config> setTunnelConfig(final ObservableTunnel tunnel, final Config config) {
|
||||
return Application.getAsyncWorker().supplyAsync(() -> {
|
||||
Application.getBackend().setState(tunnel, tunnel.getState(), config);
|
||||
return configStore.save(tunnel.getName(), config);
|
||||
}).thenApply(tunnel::onConfigChanged);
|
||||
}
|
||||
|
||||
CompletionStage<String> setTunnelName(final ObservableTunnel tunnel, final String name) {
|
||||
if (Tunnel.isNameInvalid(name))
|
||||
return CompletableFuture.failedFuture(new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)));
|
||||
if (tunnels.containsKey(name)) {
|
||||
final String message = context.getString(R.string.tunnel_error_already_exists, name);
|
||||
return CompletableFuture.failedFuture(new IllegalArgumentException(message));
|
||||
}
|
||||
final State originalState = tunnel.getState();
|
||||
final boolean wasLastUsed = tunnel == lastUsedTunnel;
|
||||
// Make sure nothing touches the tunnel.
|
||||
if (wasLastUsed)
|
||||
setLastUsedTunnel(null);
|
||||
tunnels.remove(tunnel);
|
||||
return Application.getAsyncWorker().supplyAsync(() -> {
|
||||
if (originalState == State.UP)
|
||||
Application.getBackend().setState(tunnel, State.DOWN, null);
|
||||
configStore.rename(tunnel.getName(), name);
|
||||
final String newName = tunnel.onNameChanged(name);
|
||||
if (originalState == State.UP)
|
||||
Application.getBackend().setState(tunnel, State.UP, tunnel.getConfig());
|
||||
return newName;
|
||||
}).whenComplete((newName, e) -> {
|
||||
// On failure, we don't know what state the tunnel might be in. Fix that.
|
||||
if (e != null)
|
||||
getTunnelState(tunnel);
|
||||
// Add the tunnel back to the manager, under whatever name it thinks it has.
|
||||
tunnels.add(tunnel);
|
||||
if (wasLastUsed)
|
||||
setLastUsedTunnel(tunnel);
|
||||
});
|
||||
}
|
||||
|
||||
public CompletionStage<State> setTunnelState(final ObservableTunnel tunnel, final State state) {
|
||||
// Ensure the configuration is loaded before trying to use it.
|
||||
return tunnel.getConfigAsync().thenCompose(config ->
|
||||
Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().setState(tunnel, state, config))
|
||||
).whenComplete((newState, e) -> {
|
||||
// Ensure onStateChanged is always called (failure or not), and with the correct state.
|
||||
tunnel.onStateChanged(e == null ? newState : tunnel.getState());
|
||||
if (e == null && newState == State.UP)
|
||||
setLastUsedTunnel(tunnel);
|
||||
saveState();
|
||||
});
|
||||
}
|
||||
|
||||
public static final class IntentReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(final Context context, @Nullable final Intent intent) {
|
||||
final TunnelManager manager = Application.getTunnelManager();
|
||||
if (intent == null)
|
||||
return;
|
||||
final String action = intent.getAction();
|
||||
if (action == null)
|
||||
return;
|
||||
|
||||
if ("com.wireguard.android.action.REFRESH_TUNNEL_STATES".equals(action)) {
|
||||
manager.refreshTunnelStates();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Application.getSharedPreferences().getBoolean("allow_remote_control_intents", false))
|
||||
return;
|
||||
|
||||
final State state;
|
||||
if ("com.wireguard.android.action.SET_TUNNEL_UP".equals(action))
|
||||
state = State.UP;
|
||||
else if ("com.wireguard.android.action.SET_TUNNEL_DOWN".equals(action))
|
||||
state = State.DOWN;
|
||||
else
|
||||
return;
|
||||
|
||||
final String tunnelName = intent.getStringExtra("tunnel");
|
||||
if (tunnelName == null)
|
||||
return;
|
||||
manager.getTunnels().thenAccept(tunnels -> {
|
||||
final ObservableTunnel tunnel = tunnels.get(tunnelName);
|
||||
if (tunnel == null)
|
||||
return;
|
||||
manager.setTunnelState(tunnel, state);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.model
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.databinding.BaseObservable
|
||||
import androidx.databinding.Bindable
|
||||
import com.wireguard.android.Application.Companion.get
|
||||
import com.wireguard.android.Application.Companion.getBackend
|
||||
import com.wireguard.android.Application.Companion.getTunnelManager
|
||||
import com.wireguard.android.BR
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.backend.Statistics
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.configStore.ConfigStore
|
||||
import com.wireguard.android.databinding.ObservableSortedKeyedArrayList
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import com.wireguard.android.util.UserKnobs
|
||||
import com.wireguard.android.util.applicationScope
|
||||
import com.wireguard.config.Config
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Maintains and mediates changes to the set of available WireGuard tunnels,
|
||||
*/
|
||||
class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
|
||||
private val tunnels = CompletableDeferred<ObservableSortedKeyedArrayList<String, ObservableTunnel>>()
|
||||
private val context: Context = get()
|
||||
private val tunnelMap: ObservableSortedKeyedArrayList<String, ObservableTunnel> = ObservableSortedKeyedArrayList(TunnelComparator)
|
||||
private var haveLoaded = false
|
||||
|
||||
private fun addToList(name: String, config: Config?, state: Tunnel.State): ObservableTunnel {
|
||||
val tunnel = ObservableTunnel(this, name, config, state)
|
||||
tunnelMap.add(tunnel)
|
||||
return tunnel
|
||||
}
|
||||
|
||||
suspend fun getTunnels(): ObservableSortedKeyedArrayList<String, ObservableTunnel> = tunnels.await()
|
||||
|
||||
suspend fun create(name: String, config: Config?): ObservableTunnel = withContext(Dispatchers.Main.immediate) {
|
||||
if (Tunnel.isNameInvalid(name))
|
||||
throw IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name))
|
||||
if (tunnelMap.containsKey(name))
|
||||
throw IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name))
|
||||
addToList(name, withContext(Dispatchers.IO) { configStore.create(name, config!!) }, Tunnel.State.DOWN)
|
||||
}
|
||||
|
||||
suspend fun delete(tunnel: ObservableTunnel) = withContext(Dispatchers.Main.immediate) {
|
||||
val originalState = tunnel.state
|
||||
val wasLastUsed = tunnel == lastUsedTunnel
|
||||
// Make sure nothing touches the tunnel.
|
||||
if (wasLastUsed)
|
||||
lastUsedTunnel = null
|
||||
tunnelMap.remove(tunnel)
|
||||
try {
|
||||
if (originalState == Tunnel.State.UP)
|
||||
withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.DOWN, null) }
|
||||
try {
|
||||
withContext(Dispatchers.IO) { configStore.delete(tunnel.name) }
|
||||
} catch (e: Throwable) {
|
||||
if (originalState == Tunnel.State.UP)
|
||||
withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config) }
|
||||
throw e
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
// Failure, put the tunnel back.
|
||||
tunnelMap.add(tunnel)
|
||||
if (wasLastUsed)
|
||||
lastUsedTunnel = tunnel
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@get:Bindable
|
||||
var lastUsedTunnel: ObservableTunnel? = null
|
||||
private set(value) {
|
||||
if (value == field) return
|
||||
field = value
|
||||
notifyPropertyChanged(BR.lastUsedTunnel)
|
||||
applicationScope.launch { UserKnobs.setLastUsedTunnel(value?.name) }
|
||||
}
|
||||
|
||||
suspend fun getTunnelConfig(tunnel: ObservableTunnel): Config = withContext(Dispatchers.Main.immediate) {
|
||||
tunnel.onConfigChanged(withContext(Dispatchers.IO) { configStore.load(tunnel.name) })!!
|
||||
}
|
||||
|
||||
fun onCreate() {
|
||||
applicationScope.launch {
|
||||
try {
|
||||
onTunnelsLoaded(withContext(Dispatchers.IO) { configStore.enumerate() }, withContext(Dispatchers.IO) { getBackend().runningTunnelNames })
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTunnelsLoaded(present: Iterable<String>, running: Collection<String>) {
|
||||
for (name in present)
|
||||
addToList(name, null, if (running.contains(name)) Tunnel.State.UP else Tunnel.State.DOWN)
|
||||
applicationScope.launch {
|
||||
val lastUsedName = UserKnobs.lastUsedTunnel.first()
|
||||
if (lastUsedName != null)
|
||||
lastUsedTunnel = tunnelMap[lastUsedName]
|
||||
haveLoaded = true
|
||||
restoreState(true)
|
||||
tunnels.complete(tunnelMap)
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshTunnelStates() {
|
||||
applicationScope.launch {
|
||||
try {
|
||||
val running = withContext(Dispatchers.IO) { getBackend().runningTunnelNames }
|
||||
for (tunnel in tunnelMap)
|
||||
tunnel.onStateChanged(if (running.contains(tunnel.name)) Tunnel.State.UP else Tunnel.State.DOWN)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun restoreState(force: Boolean) {
|
||||
if (!haveLoaded || (!force && !UserKnobs.restoreOnBoot.first()))
|
||||
return
|
||||
val previouslyRunning = UserKnobs.runningTunnels.first()
|
||||
if (previouslyRunning.isEmpty()) return
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
tunnelMap.filter { previouslyRunning.contains(it.name) }.map { async(Dispatchers.IO + SupervisorJob()) { setTunnelState(it, Tunnel.State.UP) } }.awaitAll()
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveState() {
|
||||
UserKnobs.setRunningTunnels(tunnelMap.filter { it.state == Tunnel.State.UP }.map { it.name }.toSet())
|
||||
}
|
||||
|
||||
suspend fun setTunnelConfig(tunnel: ObservableTunnel, config: Config): Config = withContext(Dispatchers.Main.immediate) {
|
||||
tunnel.onConfigChanged(withContext(Dispatchers.IO) {
|
||||
getBackend().setState(tunnel, tunnel.state, config)
|
||||
configStore.save(tunnel.name, config)
|
||||
})!!
|
||||
}
|
||||
|
||||
suspend fun setTunnelName(tunnel: ObservableTunnel, name: String): String = withContext(Dispatchers.Main.immediate) {
|
||||
if (Tunnel.isNameInvalid(name))
|
||||
throw IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name))
|
||||
if (tunnelMap.containsKey(name)) {
|
||||
throw IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name))
|
||||
}
|
||||
val originalState = tunnel.state
|
||||
val wasLastUsed = tunnel == lastUsedTunnel
|
||||
// Make sure nothing touches the tunnel.
|
||||
if (wasLastUsed)
|
||||
lastUsedTunnel = null
|
||||
tunnelMap.remove(tunnel)
|
||||
var throwable: Throwable? = null
|
||||
var newName: String? = null
|
||||
try {
|
||||
if (originalState == Tunnel.State.UP)
|
||||
withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.DOWN, null) }
|
||||
withContext(Dispatchers.IO) { configStore.rename(tunnel.name, name) }
|
||||
newName = tunnel.onNameChanged(name)
|
||||
if (originalState == Tunnel.State.UP)
|
||||
withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config) }
|
||||
} catch (e: Throwable) {
|
||||
throwable = e
|
||||
// On failure, we don't know what state the tunnel might be in. Fix that.
|
||||
getTunnelState(tunnel)
|
||||
}
|
||||
// Add the tunnel back to the manager, under whatever name it thinks it has.
|
||||
tunnelMap.add(tunnel)
|
||||
if (wasLastUsed)
|
||||
lastUsedTunnel = tunnel
|
||||
if (throwable != null)
|
||||
throw throwable
|
||||
newName!!
|
||||
}
|
||||
|
||||
suspend fun setTunnelState(tunnel: ObservableTunnel, state: Tunnel.State): Tunnel.State = withContext(Dispatchers.Main.immediate) {
|
||||
var newState = tunnel.state
|
||||
var throwable: Throwable? = null
|
||||
try {
|
||||
newState = withContext(Dispatchers.IO) { getBackend().setState(tunnel, state, tunnel.getConfigAsync()) }
|
||||
if (newState == Tunnel.State.UP)
|
||||
lastUsedTunnel = tunnel
|
||||
} catch (e: Throwable) {
|
||||
throwable = e
|
||||
}
|
||||
tunnel.onStateChanged(newState)
|
||||
saveState()
|
||||
if (throwable != null)
|
||||
throw throwable
|
||||
newState
|
||||
}
|
||||
|
||||
class IntentReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
applicationScope.launch {
|
||||
val manager = getTunnelManager()
|
||||
if (intent == null) return@launch
|
||||
val action = intent.action ?: return@launch
|
||||
if ("com.wireguard.android.action.REFRESH_TUNNEL_STATES" == action) {
|
||||
manager.refreshTunnelStates()
|
||||
return@launch
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || !UserKnobs.allowRemoteControlIntents.first())
|
||||
return@launch
|
||||
val state: Tunnel.State
|
||||
state = when (action) {
|
||||
"com.wireguard.android.action.SET_TUNNEL_UP" -> Tunnel.State.UP
|
||||
"com.wireguard.android.action.SET_TUNNEL_DOWN" -> Tunnel.State.DOWN
|
||||
else -> return@launch
|
||||
}
|
||||
val tunnelName = intent.getStringExtra("tunnel") ?: return@launch
|
||||
val tunnels = manager.getTunnels()
|
||||
val tunnel = tunnels[tunnelName] ?: return@launch
|
||||
try {
|
||||
manager.setTunnelState(tunnel, state)
|
||||
} catch (e: Throwable) {
|
||||
Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getTunnelState(tunnel: ObservableTunnel): Tunnel.State = withContext(Dispatchers.Main.immediate) {
|
||||
tunnel.onStateChanged(withContext(Dispatchers.IO) { getBackend().getState(tunnel) })
|
||||
}
|
||||
|
||||
suspend fun getTunnelStatistics(tunnel: ObservableTunnel): Statistics = withContext(Dispatchers.Main.immediate) {
|
||||
tunnel.onStatisticsChanged(withContext(Dispatchers.IO) { getBackend().getStatistics(tunnel) })!!
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "WireGuard/TunnelManager"
|
||||
}
|
||||
}
|
||||
+50
-25
@@ -4,46 +4,65 @@
|
||||
*/
|
||||
package com.wireguard.android.preference
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.Preference
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.activity.SettingsActivity
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import java9.util.concurrent.CompletableFuture
|
||||
import com.wireguard.android.util.UserKnobs
|
||||
import com.wireguard.android.util.activity
|
||||
import com.wireguard.android.util.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class KernelModuleDisablerPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
||||
private var state = if (Application.getBackend() is WgQuickBackend) State.ENABLED else State.DISABLED
|
||||
private var state = State.UNKNOWN
|
||||
|
||||
override fun getSummary() = context.getString(state.summaryResourceId)
|
||||
|
||||
override fun getTitle() = context.getString(state.titleResourceId)
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
override fun onClick() {
|
||||
if (state == State.DISABLED) {
|
||||
setState(State.ENABLING)
|
||||
Application.getSharedPreferences().edit().putBoolean("disable_kernel_module", false).commit()
|
||||
} else if (state == State.ENABLED) {
|
||||
setState(State.DISABLING)
|
||||
Application.getSharedPreferences().edit().putBoolean("disable_kernel_module", true).commit()
|
||||
init {
|
||||
isVisible = false
|
||||
lifecycleScope.launch {
|
||||
setState(if (Application.getBackend() is WgQuickBackend) State.ENABLED else State.DISABLED)
|
||||
}
|
||||
Application.getAsyncWorker().runAsync {
|
||||
Application.getTunnelManager().tunnels.thenApply { observableTunnels ->
|
||||
val downings = observableTunnels.values().map { it.setState(Tunnel.State.DOWN).toCompletableFuture() }.toTypedArray()
|
||||
CompletableFuture.allOf(*downings).thenRun {
|
||||
}
|
||||
|
||||
override fun getSummary() = if (state == State.UNKNOWN) "" else context.getString(state.summaryResourceId)
|
||||
|
||||
override fun getTitle() = if (state == State.UNKNOWN) "" else context.getString(state.titleResourceId)
|
||||
|
||||
override fun onClick() {
|
||||
activity.lifecycleScope.launch {
|
||||
if (state == State.DISABLED) {
|
||||
setState(State.ENABLING)
|
||||
UserKnobs.setDisableKernelModule(false)
|
||||
} else if (state == State.ENABLED) {
|
||||
setState(State.DISABLING)
|
||||
UserKnobs.setDisableKernelModule(true)
|
||||
}
|
||||
val observableTunnels = Application.getTunnelManager().getTunnels()
|
||||
val downings = observableTunnels.map { async(SupervisorJob()) { it.setStateAsync(Tunnel.State.DOWN) } }
|
||||
try {
|
||||
downings.awaitAll()
|
||||
withContext(Dispatchers.IO) {
|
||||
val restartIntent = Intent(context, SettingsActivity::class.java)
|
||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
Application.get().startActivity(restartIntent)
|
||||
exitProcess(0)
|
||||
}
|
||||
}.join()
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,13 +70,19 @@ class KernelModuleDisablerPreference(context: Context, attrs: AttributeSet?) : P
|
||||
if (this.state == state) return
|
||||
this.state = state
|
||||
if (isEnabled != state.shouldEnableView) isEnabled = state.shouldEnableView
|
||||
if (isVisible != state.visible) isVisible = state.visible
|
||||
notifyChanged()
|
||||
}
|
||||
|
||||
private enum class State(val titleResourceId: Int, val summaryResourceId: Int, val shouldEnableView: Boolean) {
|
||||
ENABLED(R.string.module_disabler_enabled_title, R.string.module_disabler_enabled_summary, true),
|
||||
DISABLED(R.string.module_disabler_disabled_title, R.string.module_disabler_disabled_summary, true),
|
||||
ENABLING(R.string.module_disabler_disabled_title, R.string.success_application_will_restart, false),
|
||||
DISABLING(R.string.module_disabler_enabled_title, R.string.success_application_will_restart, false);
|
||||
private enum class State(val titleResourceId: Int, val summaryResourceId: Int, val shouldEnableView: Boolean, val visible: Boolean) {
|
||||
UNKNOWN(0, 0, false, false),
|
||||
ENABLED(R.string.module_disabler_enabled_title, R.string.module_disabler_enabled_summary, true, true),
|
||||
DISABLED(R.string.module_disabler_disabled_title, R.string.module_disabler_disabled_summary, true, true),
|
||||
ENABLING(R.string.module_disabler_disabled_title, R.string.success_application_will_restart, false, true),
|
||||
DISABLING(R.string.module_disabler_enabled_title, R.string.success_application_will_restart, false, true);
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "WireGuard/KernelModuleDisablerPreference"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.preference
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import androidx.preference.Preference
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.util.DownloadsFileSaver
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import com.wireguard.android.util.FragmentUtils
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
|
||||
/**
|
||||
* Preference implementing a button that asynchronously exports logs.
|
||||
*/
|
||||
class LogExporterPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
||||
private var exportedFilePath: String? = null
|
||||
private fun exportLog() {
|
||||
Application.getAsyncWorker().supplyAsync {
|
||||
val outputFile = DownloadsFileSaver.save(context, "wireguard-log.txt", "text/plain", true)
|
||||
try {
|
||||
val process = Runtime.getRuntime().exec(arrayOf(
|
||||
"logcat", "-b", "all", "-d", "-v", "threadtime", "*:V"))
|
||||
BufferedReader(InputStreamReader(process.inputStream)).use { stdout ->
|
||||
BufferedReader(InputStreamReader(process.errorStream)).use { stderr ->
|
||||
while (true) {
|
||||
val line = stdout.readLine() ?: break
|
||||
outputFile.outputStream.write(line.toByteArray())
|
||||
outputFile.outputStream.write('\n'.toInt())
|
||||
}
|
||||
outputFile.outputStream.close()
|
||||
if (process.waitFor() != 0) {
|
||||
val errors = StringBuilder()
|
||||
errors.append(R.string.logcat_error)
|
||||
while (true) {
|
||||
val line = stderr.readLine() ?: break
|
||||
errors.append(line)
|
||||
}
|
||||
throw Exception(errors.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
outputFile.delete()
|
||||
throw e
|
||||
}
|
||||
outputFile.fileName
|
||||
}.whenComplete(this::exportLogComplete)
|
||||
}
|
||||
|
||||
private fun exportLogComplete(filePath: String, throwable: Throwable?) {
|
||||
if (throwable != null) {
|
||||
val error = ErrorMessages.get(throwable)
|
||||
val message = context.getString(R.string.log_export_error, error)
|
||||
Log.e(TAG, message, throwable)
|
||||
Snackbar.make(
|
||||
FragmentUtils.getPrefActivity(this).findViewById(android.R.id.content),
|
||||
message, Snackbar.LENGTH_LONG).show()
|
||||
isEnabled = true
|
||||
} else {
|
||||
exportedFilePath = filePath
|
||||
notifyChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSummary() = if (exportedFilePath == null)
|
||||
context.getString(R.string.log_export_summary)
|
||||
else
|
||||
context.getString(R.string.log_export_success, exportedFilePath)
|
||||
|
||||
override fun getTitle() = context.getString(R.string.log_export_title)
|
||||
|
||||
override fun onClick() {
|
||||
FragmentUtils.getPrefActivity(this)
|
||||
.ensurePermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, grantResults ->
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
isEnabled = false
|
||||
exportLog()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = "WireGuard/" + LogExporterPreference::class.java.simpleName
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
package com.wireguard.android.preference
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.system.OsConstants
|
||||
@@ -15,40 +14,42 @@ import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.activity.SettingsActivity
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import com.wireguard.android.util.UserKnobs
|
||||
import com.wireguard.android.util.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class ModuleDownloaderPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
||||
private var state = State.INITIAL
|
||||
|
||||
override fun getSummary() = context.getString(state.messageResourceId)
|
||||
|
||||
override fun getTitle() = context.getString(R.string.module_installer_title)
|
||||
|
||||
override fun onClick() {
|
||||
setState(State.WORKING)
|
||||
Application.getAsyncWorker().supplyAsync(Application.getModuleLoader()::download).whenComplete(this::onDownloadResult)
|
||||
}
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
private fun onDownloadResult(result: Int, throwable: Throwable?) {
|
||||
when {
|
||||
throwable != null -> {
|
||||
setState(State.FAILURE)
|
||||
Toast.makeText(context, ErrorMessages.get(throwable), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
result == OsConstants.ENOENT -> setState(State.NOTFOUND)
|
||||
result == OsConstants.EXIT_SUCCESS -> {
|
||||
setState(State.SUCCESS)
|
||||
Application.getSharedPreferences().edit().remove("disable_kernel_module").commit()
|
||||
Application.getAsyncWorker().runAsync {
|
||||
val restartIntent = Intent(context, SettingsActivity::class.java)
|
||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
Application.get().startActivity(restartIntent)
|
||||
exitProcess(0)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
when (withContext(Dispatchers.IO) { Application.getModuleLoader().download() }) {
|
||||
OsConstants.ENOENT -> setState(State.NOTFOUND)
|
||||
OsConstants.EXIT_SUCCESS -> {
|
||||
setState(State.SUCCESS)
|
||||
UserKnobs.setDisableKernelModule(null)
|
||||
withContext(Dispatchers.IO) {
|
||||
val restartIntent = Intent(context, SettingsActivity::class.java)
|
||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
Application.get().startActivity(restartIntent)
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
||||
else -> setState(State.FAILURE)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
setState(State.FAILURE)
|
||||
Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_LONG).show()
|
||||
}
|
||||
else -> setState(State.FAILURE)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.preference
|
||||
|
||||
import androidx.datastore.DataStore
|
||||
import androidx.datastore.preferences.Preferences
|
||||
import androidx.datastore.preferences.edit
|
||||
import androidx.datastore.preferences.preferencesKey
|
||||
import androidx.datastore.preferences.preferencesSetKey
|
||||
import androidx.datastore.preferences.remove
|
||||
import androidx.preference.PreferenceDataStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class PreferencesPreferenceDataStore(private val coroutineScope: CoroutineScope, private val dataStore: DataStore<Preferences>) : PreferenceDataStore() {
|
||||
override fun putString(key: String?, value: String?) {
|
||||
if (key == null) return
|
||||
val pk = preferencesKey<String>(key)
|
||||
coroutineScope.launch {
|
||||
dataStore.edit {
|
||||
if (value == null) it.remove(pk)
|
||||
else it[pk] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun putStringSet(key: String?, values: Set<String?>?) {
|
||||
if (key == null) return
|
||||
val pk = preferencesSetKey<String>(key)
|
||||
val filteredValues = values?.filterNotNull()?.toSet()
|
||||
coroutineScope.launch {
|
||||
dataStore.edit {
|
||||
if (filteredValues == null || filteredValues.isEmpty()) it.remove(pk)
|
||||
else it[pk] = filteredValues
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun putInt(key: String?, value: Int) {
|
||||
if (key == null) return
|
||||
val pk = preferencesKey<Int>(key)
|
||||
coroutineScope.launch {
|
||||
dataStore.edit {
|
||||
it[pk] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun putLong(key: String?, value: Long) {
|
||||
if (key == null) return
|
||||
val pk = preferencesKey<Long>(key)
|
||||
coroutineScope.launch {
|
||||
dataStore.edit {
|
||||
it[pk] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun putFloat(key: String?, value: Float) {
|
||||
if (key == null) return
|
||||
val pk = preferencesKey<Float>(key)
|
||||
coroutineScope.launch {
|
||||
dataStore.edit {
|
||||
it[pk] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun putBoolean(key: String?, value: Boolean) {
|
||||
if (key == null) return
|
||||
val pk = preferencesKey<Boolean>(key)
|
||||
coroutineScope.launch {
|
||||
dataStore.edit {
|
||||
it[pk] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getString(key: String?, defValue: String?): String? {
|
||||
if (key == null) return defValue
|
||||
val pk = preferencesKey<String>(key)
|
||||
return runBlocking {
|
||||
dataStore.data.map { it[pk] ?: defValue }.first()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getStringSet(key: String?, defValues: Set<String?>?): Set<String?>? {
|
||||
if (key == null) return defValues
|
||||
val pk = preferencesSetKey<String>(key)
|
||||
return runBlocking {
|
||||
dataStore.data.map { it[pk] ?: defValues }.first()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getInt(key: String?, defValue: Int): Int {
|
||||
if (key == null) return defValue
|
||||
val pk = preferencesKey<Int>(key)
|
||||
return runBlocking {
|
||||
dataStore.data.map { it[pk] ?: defValue }.first()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLong(key: String?, defValue: Long): Long {
|
||||
if (key == null) return defValue
|
||||
val pk = preferencesKey<Long>(key)
|
||||
return runBlocking {
|
||||
dataStore.data.map { it[pk] ?: defValue }.first()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFloat(key: String?, defValue: Float): Float {
|
||||
if (key == null) return defValue
|
||||
val pk = preferencesKey<Float>(key)
|
||||
return runBlocking {
|
||||
dataStore.data.map { it[pk] ?: defValue }.first()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
|
||||
if (key == null) return defValue
|
||||
val pk = preferencesKey<Boolean>(key)
|
||||
return runBlocking {
|
||||
dataStore.data.map { it[pk] ?: defValue }.first()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,10 @@ import androidx.preference.Preference
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.util.ToolsInstaller
|
||||
import com.wireguard.android.util.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Preference implementing a button that asynchronously runs `ToolsInstaller` and displays the
|
||||
@@ -17,37 +21,41 @@ import com.wireguard.android.util.ToolsInstaller
|
||||
*/
|
||||
class ToolsInstallerPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
||||
private var state = State.INITIAL
|
||||
|
||||
override fun getSummary() = context.getString(state.messageResourceId)
|
||||
|
||||
override fun getTitle() = context.getString(R.string.tools_installer_title)
|
||||
|
||||
override fun onAttached() {
|
||||
super.onAttached()
|
||||
Application.getAsyncWorker().supplyAsync(Application.getToolsInstaller()::areInstalled).whenComplete(this::onCheckResult)
|
||||
}
|
||||
|
||||
private fun onCheckResult(state: Int, throwable: Throwable?) {
|
||||
when {
|
||||
throwable != null || state == ToolsInstaller.ERROR -> setState(State.INITIAL)
|
||||
state and ToolsInstaller.YES == ToolsInstaller.YES -> setState(State.ALREADY)
|
||||
state and (ToolsInstaller.MAGISK or ToolsInstaller.NO) == ToolsInstaller.MAGISK or ToolsInstaller.NO -> setState(State.INITIAL_MAGISK)
|
||||
state and (ToolsInstaller.SYSTEM or ToolsInstaller.NO) == ToolsInstaller.SYSTEM or ToolsInstaller.NO -> setState(State.INITIAL_SYSTEM)
|
||||
else -> setState(State.INITIAL)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val state = withContext(Dispatchers.IO) { Application.getToolsInstaller().areInstalled() }
|
||||
when {
|
||||
state == ToolsInstaller.ERROR -> setState(State.INITIAL)
|
||||
state and ToolsInstaller.YES == ToolsInstaller.YES -> setState(State.ALREADY)
|
||||
state and (ToolsInstaller.MAGISK or ToolsInstaller.NO) == ToolsInstaller.MAGISK or ToolsInstaller.NO -> setState(State.INITIAL_MAGISK)
|
||||
state and (ToolsInstaller.SYSTEM or ToolsInstaller.NO) == ToolsInstaller.SYSTEM or ToolsInstaller.NO -> setState(State.INITIAL_SYSTEM)
|
||||
else -> setState(State.INITIAL)
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
setState(State.INITIAL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
setState(State.WORKING)
|
||||
Application.getAsyncWorker().supplyAsync { Application.getToolsInstaller().install() }.whenComplete { result: Int, throwable: Throwable? -> onInstallResult(result, throwable) }
|
||||
}
|
||||
|
||||
private fun onInstallResult(result: Int, throwable: Throwable?) {
|
||||
when {
|
||||
throwable != null -> setState(State.FAILURE)
|
||||
result and (ToolsInstaller.YES or ToolsInstaller.MAGISK) == ToolsInstaller.YES or ToolsInstaller.MAGISK -> setState(State.SUCCESS_MAGISK)
|
||||
result and (ToolsInstaller.YES or ToolsInstaller.SYSTEM) == ToolsInstaller.YES or ToolsInstaller.SYSTEM -> setState(State.SUCCESS_SYSTEM)
|
||||
else -> setState(State.FAILURE)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val result = withContext(Dispatchers.IO) { Application.getToolsInstaller().install() }
|
||||
when {
|
||||
result and (ToolsInstaller.YES or ToolsInstaller.MAGISK) == ToolsInstaller.YES or ToolsInstaller.MAGISK -> setState(State.SUCCESS_MAGISK)
|
||||
result and (ToolsInstaller.YES or ToolsInstaller.SYSTEM) == ToolsInstaller.YES or ToolsInstaller.SYSTEM -> setState(State.SUCCESS_SYSTEM)
|
||||
else -> setState(State.FAILURE)
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
setState(State.FAILURE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ import com.wireguard.android.R
|
||||
import com.wireguard.android.backend.Backend
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.wireguard.android.util.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Locale
|
||||
|
||||
class VersionPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
||||
@@ -43,15 +47,16 @@ class VersionPreference(context: Context, attrs: AttributeSet?) : Preference(con
|
||||
}
|
||||
|
||||
init {
|
||||
Application.getBackendAsync().thenAccept { backend: Backend ->
|
||||
lifecycleScope.launch {
|
||||
val backend = Application.getBackend()
|
||||
versionSummary = getContext().getString(R.string.version_summary_checking, getBackendPrettyName(context, backend).toLowerCase(Locale.ENGLISH))
|
||||
Application.getAsyncWorker().supplyAsync(backend::getVersion).whenComplete { version, exception ->
|
||||
versionSummary = if (exception == null)
|
||||
getContext().getString(R.string.version_summary, getBackendPrettyName(context, backend), version)
|
||||
else
|
||||
getContext().getString(R.string.version_summary_unknown, getBackendPrettyName(context, backend).toLowerCase(Locale.ENGLISH))
|
||||
notifyChanged()
|
||||
notifyChanged()
|
||||
versionSummary = try {
|
||||
getContext().getString(R.string.version_summary, getBackendPrettyName(context, backend), withContext(Dispatchers.IO) { backend.version })
|
||||
} catch (_: Throwable) {
|
||||
getContext().getString(R.string.version_summary_unknown, getBackendPrettyName(context, backend).toLowerCase(Locale.ENGLISH))
|
||||
}
|
||||
notifyChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,20 +4,25 @@
|
||||
*/
|
||||
package com.wireguard.android.preference
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import androidx.preference.Preference
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.model.ObservableTunnel
|
||||
import com.wireguard.android.util.AdminKnobs
|
||||
import com.wireguard.android.util.BiometricAuthenticator
|
||||
import com.wireguard.android.util.DownloadsFileSaver
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import com.wireguard.android.util.FragmentUtils
|
||||
import java9.util.concurrent.CompletableFuture
|
||||
import com.wireguard.android.util.activity
|
||||
import com.wireguard.android.util.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
@@ -27,52 +32,48 @@ import java.util.zip.ZipOutputStream
|
||||
*/
|
||||
class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
||||
private var exportedFilePath: String? = null
|
||||
private val downloadsFileSaver = DownloadsFileSaver(activity)
|
||||
|
||||
private fun exportZip() {
|
||||
Application.getTunnelManager().tunnels.thenAccept(this::exportZip)
|
||||
}
|
||||
|
||||
private fun exportZip(tunnels: List<ObservableTunnel>) {
|
||||
val futureConfigs = tunnels.map { it.configAsync.toCompletableFuture() }.toTypedArray()
|
||||
if (futureConfigs.isEmpty()) {
|
||||
exportZipComplete(null, IllegalArgumentException(
|
||||
context.getString(R.string.no_tunnels_error)))
|
||||
return
|
||||
}
|
||||
CompletableFuture.allOf(*futureConfigs)
|
||||
.whenComplete { _, exception ->
|
||||
Application.getAsyncWorker().supplyAsync {
|
||||
if (exception != null) throw exception
|
||||
val outputFile = DownloadsFileSaver.save(context, "wireguard-export.zip", "application/zip", true)
|
||||
try {
|
||||
ZipOutputStream(outputFile.outputStream).use { zip ->
|
||||
for (i in futureConfigs.indices) {
|
||||
zip.putNextEntry(ZipEntry(tunnels[i].name + ".conf"))
|
||||
zip.write(futureConfigs[i].getNow(null)!!.toWgQuickString().toByteArray(StandardCharsets.UTF_8))
|
||||
}
|
||||
zip.closeEntry()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
outputFile.delete()
|
||||
throw e
|
||||
lifecycleScope.launch {
|
||||
val tunnels = Application.getTunnelManager().getTunnels()
|
||||
try {
|
||||
exportedFilePath = withContext(Dispatchers.IO) {
|
||||
val configs = tunnels.map { async(SupervisorJob()) { it.getConfigAsync() } }.awaitAll()
|
||||
if (configs.isEmpty()) {
|
||||
throw IllegalArgumentException(context.getString(R.string.no_tunnels_error))
|
||||
}
|
||||
val outputFile = downloadsFileSaver.save("wireguard-export.zip", "application/zip", true)
|
||||
if (outputFile == null) {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
isEnabled = true
|
||||
}
|
||||
outputFile.fileName
|
||||
}.whenComplete(this::exportZipComplete)
|
||||
return@withContext null
|
||||
}
|
||||
try {
|
||||
ZipOutputStream(outputFile.outputStream).use { zip ->
|
||||
for (i in configs.indices) {
|
||||
zip.putNextEntry(ZipEntry(tunnels[i].name + ".conf"))
|
||||
zip.write(configs[i].toWgQuickString().toByteArray(StandardCharsets.UTF_8))
|
||||
}
|
||||
zip.closeEntry()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
outputFile.delete()
|
||||
throw e
|
||||
}
|
||||
outputFile.fileName
|
||||
}
|
||||
}
|
||||
|
||||
private fun exportZipComplete(filePath: String?, throwable: Throwable?) {
|
||||
if (throwable != null) {
|
||||
val error = ErrorMessages.get(throwable)
|
||||
val message = context.getString(R.string.zip_export_error, error)
|
||||
Log.e(TAG, message, throwable)
|
||||
Snackbar.make(
|
||||
FragmentUtils.getPrefActivity(this).findViewById(android.R.id.content),
|
||||
message, Snackbar.LENGTH_LONG).show()
|
||||
isEnabled = true
|
||||
} else {
|
||||
exportedFilePath = filePath
|
||||
notifyChanged()
|
||||
notifyChanged()
|
||||
} catch (e: Throwable) {
|
||||
val error = ErrorMessages[e]
|
||||
val message = context.getString(R.string.zip_export_error, error)
|
||||
Log.e(TAG, message, e)
|
||||
Snackbar.make(
|
||||
activity.findViewById(android.R.id.content),
|
||||
message, Snackbar.LENGTH_LONG).show()
|
||||
isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,16 +82,27 @@ class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference
|
||||
override fun getTitle() = context.getString(R.string.zip_export_title)
|
||||
|
||||
override fun onClick() {
|
||||
FragmentUtils.getPrefActivity(this)
|
||||
.ensurePermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, grantResults ->
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
isEnabled = false
|
||||
exportZip()
|
||||
}
|
||||
if (AdminKnobs.disableConfigExport) return
|
||||
val fragment = activity.supportFragmentManager.fragments.first()
|
||||
BiometricAuthenticator.authenticate(R.string.biometric_prompt_zip_exporter_title, fragment) {
|
||||
when (it) {
|
||||
// When we have successful authentication, or when there is no biometric hardware available.
|
||||
is BiometricAuthenticator.Result.Success, is BiometricAuthenticator.Result.HardwareUnavailableOrDisabled -> {
|
||||
isEnabled = false
|
||||
exportZip()
|
||||
}
|
||||
is BiometricAuthenticator.Result.Failure -> {
|
||||
Snackbar.make(
|
||||
activity.findViewById(android.R.id.content),
|
||||
it.message,
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = "WireGuard/" + ZipExporterPreference::class.java.simpleName
|
||||
private const val TAG = "WireGuard/ZipExporterPreference"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util
|
||||
|
||||
import android.content.RestrictionsManager
|
||||
import androidx.core.content.getSystemService
|
||||
import com.wireguard.android.Application
|
||||
|
||||
object AdminKnobs {
|
||||
private val restrictions: RestrictionsManager? = Application.get().getSystemService()
|
||||
val disableConfigExport: Boolean
|
||||
get() = restrictions?.applicationRestrictions?.getBoolean("disable_config_export", false)
|
||||
?: false
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.os.Handler;
|
||||
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import java9.util.concurrent.CompletableFuture;
|
||||
import java9.util.concurrent.CompletionStage;
|
||||
|
||||
/**
|
||||
* Helper class for running asynchronous tasks and ensuring they are completed on the main thread.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public class AsyncWorker {
|
||||
private final Executor executor;
|
||||
private final Handler handler;
|
||||
|
||||
public AsyncWorker(final Executor executor, final Handler handler) {
|
||||
this.executor = executor;
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
public CompletionStage<Void> runAsync(final AsyncRunnable<?> runnable) {
|
||||
final CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
runnable.run();
|
||||
handler.post(() -> future.complete(null));
|
||||
} catch (final Throwable t) {
|
||||
handler.post(() -> future.completeExceptionally(t));
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
public <T> CompletionStage<T> supplyAsync(final AsyncSupplier<T, ?> supplier) {
|
||||
final CompletableFuture<T> future = new CompletableFuture<>();
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
final T result = supplier.get();
|
||||
handler.post(() -> future.complete(result));
|
||||
} catch (final Throwable t) {
|
||||
handler.post(() -> future.completeExceptionally(t));
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface AsyncRunnable<E extends Throwable> {
|
||||
void run() throws E;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface AsyncSupplier<T, E extends Throwable> {
|
||||
T get() throws E;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricManager.Authenticators
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.wireguard.android.R
|
||||
|
||||
|
||||
object BiometricAuthenticator {
|
||||
private const val TAG = "WireGuard/BiometricAuthenticator"
|
||||
|
||||
// Not all devices support strong biometric auth so we're allowing both device credentials as
|
||||
// well as weak biometrics.
|
||||
private const val allowedAuthenticators = Authenticators.DEVICE_CREDENTIAL or Authenticators.BIOMETRIC_WEAK
|
||||
|
||||
sealed class Result {
|
||||
data class Success(val cryptoObject: BiometricPrompt.CryptoObject?) : Result()
|
||||
data class Failure(val code: Int?, val message: CharSequence) : Result()
|
||||
object HardwareUnavailableOrDisabled : Result()
|
||||
object Cancelled : Result()
|
||||
}
|
||||
|
||||
fun authenticate(
|
||||
@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
|
||||
}
|
||||
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() {
|
||||
super.onAuthenticationFailed()
|
||||
callback(Result.Failure(null, fragment.getString(R.string.biometric_auth_error)))
|
||||
}
|
||||
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
callback(Result.Success(result.cryptoObject))
|
||||
}
|
||||
}
|
||||
val biometricPrompt = BiometricPrompt(fragment, { Handler(Looper.getMainLooper()).post(it) }, authCallback)
|
||||
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(fragment.getString(dialogTitleRes))
|
||||
.setAllowedAuthenticators(allowedAuthenticators)
|
||||
.build()
|
||||
if (BiometricManager.from(fragment.requireContext()).canAuthenticate(allowedAuthenticators) == BiometricManager.BIOMETRIC_SUCCESS) {
|
||||
biometricPrompt.authenticate(promptInfo)
|
||||
} else {
|
||||
callback(Result.HardwareUnavailableOrDisabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
/**
|
||||
* Standalone utilities for interacting with the system clipboard.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public final class ClipboardUtils {
|
||||
private ClipboardUtils() {
|
||||
// Prevent instantiation
|
||||
}
|
||||
|
||||
public static void copyTextView(final View view) {
|
||||
if (!(view instanceof TextView))
|
||||
return;
|
||||
final CharSequence text = ((TextView) view).getText();
|
||||
if (text == null || text.length() == 0)
|
||||
return;
|
||||
final Object service = view.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
if (!(service instanceof ClipboardManager))
|
||||
return;
|
||||
final CharSequence description = view.getContentDescription();
|
||||
((ClipboardManager) service).setPrimaryClip(ClipData.newPlainText(description, text));
|
||||
Snackbar.make(view, description + " copied to clipboard", Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.util
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.getSystemService
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.wireguard.android.R
|
||||
|
||||
/**
|
||||
* Standalone utilities for interacting with the system clipboard.
|
||||
*/
|
||||
object ClipboardUtils {
|
||||
@JvmStatic
|
||||
fun copyTextView(view: View) {
|
||||
val data = when (view) {
|
||||
is TextInputEditText -> Pair(view.editableText, view.hint)
|
||||
is TextView -> Pair(view.text, view.contentDescription)
|
||||
else -> return
|
||||
}
|
||||
if (data.first == null || data.first.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val service = view.context.getSystemService<ClipboardManager>() ?: return
|
||||
service.setPrimaryClip(ClipData.newPlainText(data.second, data.first))
|
||||
Snackbar.make(view, view.context.getString(R.string.copied_to_clipboard, data.second), Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.MediaStore.MediaColumns;
|
||||
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
@NonNullForAll
|
||||
public final class DownloadsFileSaver {
|
||||
|
||||
private DownloadsFileSaver() {
|
||||
// Prevent instantiation
|
||||
}
|
||||
|
||||
public static DownloadsFile save(final Context context, final String name, final String mimeType, final boolean overwriteExisting) throws Exception {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
final ContentResolver contentResolver = context.getContentResolver();
|
||||
if (overwriteExisting)
|
||||
contentResolver.delete(MediaStore.Downloads.EXTERNAL_CONTENT_URI, String.format("%s = ?", MediaColumns.DISPLAY_NAME), new String[]{name});
|
||||
final ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(MediaColumns.DISPLAY_NAME, name);
|
||||
contentValues.put(MediaColumns.MIME_TYPE, mimeType);
|
||||
final Uri contentUri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues);
|
||||
if (contentUri == null)
|
||||
throw new IOException(context.getString(R.string.create_downloads_file_error));
|
||||
final OutputStream contentStream = contentResolver.openOutputStream(contentUri);
|
||||
if (contentStream == null)
|
||||
throw new IOException(context.getString(R.string.create_downloads_file_error));
|
||||
@SuppressWarnings("deprecation")
|
||||
Cursor cursor = contentResolver.query(contentUri, new String[]{MediaColumns.DATA}, null, null, null);
|
||||
String path = null;
|
||||
if (cursor != null) {
|
||||
try {
|
||||
if (cursor.moveToFirst())
|
||||
path = cursor.getString(0);
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
if (path == null) {
|
||||
path = "Download/";
|
||||
cursor = contentResolver.query(contentUri, new String[]{MediaColumns.DISPLAY_NAME}, null, null, null);
|
||||
if (cursor != null) {
|
||||
try {
|
||||
if (cursor.moveToFirst())
|
||||
path += cursor.getString(0);
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
return new DownloadsFile(context, contentStream, path, contentUri);
|
||||
} else {
|
||||
@SuppressWarnings("deprecation") final File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||
final File file = new File(path, name);
|
||||
if (!path.isDirectory() && !path.mkdirs())
|
||||
throw new IOException(context.getString(R.string.create_output_dir_error));
|
||||
return new DownloadsFile(context, new FileOutputStream(file), file.getAbsolutePath(), null);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class DownloadsFile {
|
||||
private final Context context;
|
||||
private final String fileName;
|
||||
private final OutputStream outputStream;
|
||||
private final Uri uri;
|
||||
|
||||
private DownloadsFile(final Context context, final OutputStream outputStream, final String fileName, final Uri uri) {
|
||||
this.context = context;
|
||||
this.outputStream = outputStream;
|
||||
this.fileName = fileName;
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
public void delete() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||
context.getContentResolver().delete(uri, null, null);
|
||||
else
|
||||
new File(fileName).delete();
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public OutputStream getOutputStream() {
|
||||
return outputStream;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright © 2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.util
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.provider.MediaStore.MediaColumns
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.wireguard.android.R
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
|
||||
class DownloadsFileSaver(private val context: ComponentActivity) {
|
||||
private lateinit var activityResult: ActivityResultLauncher<String>
|
||||
private lateinit var futureGrant: CompletableDeferred<Boolean>
|
||||
|
||||
init {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
futureGrant = CompletableDeferred()
|
||||
activityResult = context.registerForActivityResult(ActivityResultContracts.RequestPermission()) { ret -> futureGrant.complete(ret) }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun save(name: String, mimeType: String?, overwriteExisting: Boolean) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val contentResolver = context.contentResolver
|
||||
if (overwriteExisting)
|
||||
contentResolver.delete(MediaStore.Downloads.EXTERNAL_CONTENT_URI, String.format("%s = ?", MediaColumns.DISPLAY_NAME), arrayOf(name))
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(MediaColumns.DISPLAY_NAME, name)
|
||||
contentValues.put(MediaColumns.MIME_TYPE, mimeType)
|
||||
val contentUri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
||||
?: throw IOException(context.getString(R.string.create_downloads_file_error))
|
||||
val contentStream = contentResolver.openOutputStream(contentUri)
|
||||
?: throw IOException(context.getString(R.string.create_downloads_file_error))
|
||||
@Suppress("DEPRECATION") var cursor = contentResolver.query(contentUri, arrayOf(MediaColumns.DATA), null, null, null)
|
||||
var path: String? = null
|
||||
if (cursor != null) {
|
||||
try {
|
||||
if (cursor.moveToFirst())
|
||||
path = cursor.getString(0)
|
||||
} finally {
|
||||
cursor.close()
|
||||
}
|
||||
}
|
||||
if (path == null) {
|
||||
path = "Download/"
|
||||
cursor = contentResolver.query(contentUri, arrayOf(MediaColumns.DISPLAY_NAME), null, null, null)
|
||||
if (cursor != null) {
|
||||
try {
|
||||
if (cursor.moveToFirst())
|
||||
path += cursor.getString(0)
|
||||
} finally {
|
||||
cursor.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
DownloadsFile(context, contentStream, path, contentUri)
|
||||
}
|
||||
} else {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
activityResult.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
val granted = futureGrant.await()
|
||||
if (!granted) {
|
||||
futureGrant = CompletableDeferred()
|
||||
return@withContext null
|
||||
}
|
||||
}
|
||||
@Suppress("DEPRECATION") val path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
withContext(Dispatchers.IO) {
|
||||
val file = File(path, name)
|
||||
if (!path.isDirectory && !path.mkdirs())
|
||||
throw IOException(context.getString(R.string.create_output_dir_error))
|
||||
DownloadsFile(context, FileOutputStream(file), file.absolutePath, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadsFile(private val context: Context, val outputStream: OutputStream, val fileName: String, private val uri: Uri?) {
|
||||
suspend fun delete() = withContext(Dispatchers.IO) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||
context.contentResolver.delete(uri!!, null, null)
|
||||
else
|
||||
File(fileName).delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.backend.BackendException;
|
||||
import com.wireguard.android.util.RootShell.RootShellException;
|
||||
import com.wireguard.config.BadConfigException;
|
||||
import com.wireguard.config.BadConfigException.Location;
|
||||
import com.wireguard.config.InetEndpoint;
|
||||
import com.wireguard.config.InetNetwork;
|
||||
import com.wireguard.config.ParseException;
|
||||
import com.wireguard.crypto.Key.Format;
|
||||
import com.wireguard.crypto.KeyFormatException;
|
||||
import com.wireguard.crypto.KeyFormatException.Type;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java9.util.Maps;
|
||||
|
||||
@NonNullForAll
|
||||
public final class ErrorMessages {
|
||||
private static final Map<BadConfigException.Reason, Integer> BCE_REASON_MAP = new EnumMap<>(Maps.of(
|
||||
BadConfigException.Reason.INVALID_KEY, R.string.bad_config_reason_invalid_key,
|
||||
BadConfigException.Reason.INVALID_NUMBER, R.string.bad_config_reason_invalid_number,
|
||||
BadConfigException.Reason.INVALID_VALUE, R.string.bad_config_reason_invalid_value,
|
||||
BadConfigException.Reason.MISSING_ATTRIBUTE, R.string.bad_config_reason_missing_attribute,
|
||||
BadConfigException.Reason.MISSING_SECTION, R.string.bad_config_reason_missing_section,
|
||||
BadConfigException.Reason.SYNTAX_ERROR, R.string.bad_config_reason_syntax_error,
|
||||
BadConfigException.Reason.UNKNOWN_ATTRIBUTE, R.string.bad_config_reason_unknown_attribute,
|
||||
BadConfigException.Reason.UNKNOWN_SECTION, R.string.bad_config_reason_unknown_section
|
||||
));
|
||||
private static final Map<BackendException.Reason, Integer> BE_REASON_MAP = new EnumMap<>(Maps.of(
|
||||
BackendException.Reason.UNKNOWN_KERNEL_MODULE_NAME, R.string.module_version_error,
|
||||
BackendException.Reason.WG_QUICK_CONFIG_ERROR_CODE, R.string.tunnel_config_error,
|
||||
BackendException.Reason.TUNNEL_MISSING_CONFIG, R.string.no_config_error,
|
||||
BackendException.Reason.VPN_NOT_AUTHORIZED, R.string.vpn_not_authorized_error,
|
||||
BackendException.Reason.UNABLE_TO_START_VPN, R.string.vpn_start_error,
|
||||
BackendException.Reason.TUN_CREATION_ERROR, R.string.tun_create_error,
|
||||
BackendException.Reason.GO_ACTIVATION_ERROR_CODE, R.string.tunnel_on_error
|
||||
));
|
||||
private static final Map<Format, Integer> KFE_FORMAT_MAP = new EnumMap<>(Maps.of(
|
||||
Format.BASE64, R.string.key_length_explanation_base64,
|
||||
Format.BINARY, R.string.key_length_explanation_binary,
|
||||
Format.HEX, R.string.key_length_explanation_hex
|
||||
));
|
||||
private static final Map<Type, Integer> KFE_TYPE_MAP = new EnumMap<>(Maps.of(
|
||||
Type.CONTENTS, R.string.key_contents_error,
|
||||
Type.LENGTH, R.string.key_length_error
|
||||
));
|
||||
private static final Map<Class, Integer> PE_CLASS_MAP = Maps.of(
|
||||
InetAddress.class, R.string.parse_error_inet_address,
|
||||
InetEndpoint.class, R.string.parse_error_inet_endpoint,
|
||||
InetNetwork.class, R.string.parse_error_inet_network,
|
||||
Integer.class, R.string.parse_error_integer
|
||||
);
|
||||
private static final Map<RootShellException.Reason, Integer> RSE_REASON_MAP = new EnumMap<>(Maps.of(
|
||||
RootShellException.Reason.NO_ROOT_ACCESS, R.string.error_root,
|
||||
RootShellException.Reason.SHELL_MARKER_COUNT_ERROR, R.string.shell_marker_count_error,
|
||||
RootShellException.Reason.SHELL_EXIT_STATUS_READ_ERROR, R.string.shell_exit_status_read_error,
|
||||
RootShellException.Reason.SHELL_START_ERROR, R.string.shell_start_error,
|
||||
RootShellException.Reason.CREATE_BIN_DIR_ERROR, R.string.create_bin_dir_error,
|
||||
RootShellException.Reason.CREATE_TEMP_DIR_ERROR, R.string.create_temp_dir_error
|
||||
));
|
||||
|
||||
private ErrorMessages() {
|
||||
// Prevent instantiation
|
||||
}
|
||||
|
||||
public static String get(@Nullable final Throwable throwable) {
|
||||
final Resources resources = Application.get().getResources();
|
||||
if (throwable == null)
|
||||
return resources.getString(R.string.unknown_error);
|
||||
final Throwable rootCause = rootCause(throwable);
|
||||
final String message;
|
||||
if (rootCause instanceof BadConfigException) {
|
||||
final BadConfigException bce = (BadConfigException) rootCause;
|
||||
final String reason = getBadConfigExceptionReason(resources, bce);
|
||||
final String context = bce.getLocation() == Location.TOP_LEVEL ?
|
||||
resources.getString(R.string.bad_config_context_top_level,
|
||||
bce.getSection().getName()) :
|
||||
resources.getString(R.string.bad_config_context,
|
||||
bce.getSection().getName(),
|
||||
bce.getLocation().getName());
|
||||
final String explanation = getBadConfigExceptionExplanation(resources, bce);
|
||||
message = resources.getString(R.string.bad_config_error, reason, context) + explanation;
|
||||
} else if (rootCause instanceof BackendException) {
|
||||
final BackendException be = (BackendException) rootCause;
|
||||
message = resources.getString(BE_REASON_MAP.get(be.getReason()), be.getFormat());
|
||||
} else if (rootCause instanceof RootShellException) {
|
||||
final RootShellException rse = (RootShellException) rootCause;
|
||||
message = resources.getString(RSE_REASON_MAP.get(rse.getReason()), rse.getFormat());
|
||||
} else if (rootCause.getMessage() != null) {
|
||||
message = rootCause.getMessage();
|
||||
} else {
|
||||
final String errorType = rootCause.getClass().getSimpleName();
|
||||
message = resources.getString(R.string.generic_error, errorType);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
private static String getBadConfigExceptionExplanation(final Resources resources,
|
||||
final BadConfigException bce) {
|
||||
if (bce.getCause() instanceof KeyFormatException) {
|
||||
final KeyFormatException kfe = (KeyFormatException) bce.getCause();
|
||||
if (kfe.getType() == Type.LENGTH)
|
||||
return resources.getString(KFE_FORMAT_MAP.get(kfe.getFormat()));
|
||||
} else if (bce.getCause() instanceof ParseException) {
|
||||
final ParseException pe = (ParseException) bce.getCause();
|
||||
if (pe.getMessage() != null)
|
||||
return ": " + pe.getMessage();
|
||||
} else if (bce.getLocation() == Location.LISTEN_PORT) {
|
||||
return resources.getString(R.string.bad_config_explanation_udp_port);
|
||||
} else if (bce.getLocation() == Location.MTU) {
|
||||
return resources.getString(R.string.bad_config_explanation_positive_number);
|
||||
} else if (bce.getLocation() == Location.PERSISTENT_KEEPALIVE) {
|
||||
return resources.getString(R.string.bad_config_explanation_pka);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String getBadConfigExceptionReason(final Resources resources,
|
||||
final BadConfigException bce) {
|
||||
if (bce.getCause() instanceof KeyFormatException) {
|
||||
final KeyFormatException kfe = (KeyFormatException) bce.getCause();
|
||||
return resources.getString(KFE_TYPE_MAP.get(kfe.getType()));
|
||||
} else if (bce.getCause() instanceof ParseException) {
|
||||
final ParseException pe = (ParseException) bce.getCause();
|
||||
final String type = resources.getString(PE_CLASS_MAP.containsKey(pe.getParsingClass()) ?
|
||||
PE_CLASS_MAP.get(pe.getParsingClass()) : R.string.parse_error_generic);
|
||||
return resources.getString(R.string.parse_error_reason, type, pe.getText());
|
||||
}
|
||||
return resources.getString(BCE_REASON_MAP.get(bce.getReason()), bce.getText());
|
||||
}
|
||||
|
||||
private static Throwable rootCause(final Throwable throwable) {
|
||||
Throwable cause = throwable;
|
||||
while (cause.getCause() != null) {
|
||||
if (cause instanceof BadConfigException || cause instanceof BackendException ||
|
||||
cause instanceof RootShellException)
|
||||
break;
|
||||
final Throwable nextCause = cause.getCause();
|
||||
if (nextCause instanceof RemoteException)
|
||||
break;
|
||||
cause = nextCause;
|
||||
}
|
||||
return cause;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.util
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.os.RemoteException
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.backend.BackendException
|
||||
import com.wireguard.android.util.RootShell.RootShellException
|
||||
import com.wireguard.config.BadConfigException
|
||||
import com.wireguard.config.InetEndpoint
|
||||
import com.wireguard.config.InetNetwork
|
||||
import com.wireguard.config.ParseException
|
||||
import com.wireguard.crypto.Key
|
||||
import com.wireguard.crypto.KeyFormatException
|
||||
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
|
||||
)
|
||||
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
|
||||
)
|
||||
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
|
||||
)
|
||||
private val KFE_TYPE_MAP = mapOf(
|
||||
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
|
||||
)
|
||||
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
|
||||
)
|
||||
|
||||
operator fun get(throwable: Throwable?): String {
|
||||
val resources = Application.get().resources
|
||||
if (throwable == null) return resources.getString(R.string.unknown_error)
|
||||
val rootCause = rootCause(throwable)
|
||||
return when {
|
||||
rootCause is BadConfigException -> {
|
||||
val reason = getBadConfigExceptionReason(resources, rootCause)
|
||||
val context = if (rootCause.location == BadConfigException.Location.TOP_LEVEL) {
|
||||
resources.getString(R.string.bad_config_context_top_level, rootCause.section.getName())
|
||||
} else {
|
||||
resources.getString(R.string.bad_config_context, rootCause.section.getName(), rootCause.location.getName())
|
||||
}
|
||||
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.message != null -> {
|
||||
rootCause.message!!
|
||||
}
|
||||
else -> {
|
||||
val errorType = rootCause.javaClass.simpleName
|
||||
resources.getString(R.string.generic_error, errorType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
} else if (bce.cause is ParseException) {
|
||||
val pe = bce.cause as ParseException?
|
||||
if (pe!!.message != null) return ": ${pe.message}"
|
||||
} else if (bce.location == BadConfigException.Location.LISTEN_PORT) {
|
||||
return resources.getString(R.string.bad_config_explanation_udp_port)
|
||||
} else if (bce.location == BadConfigException.Location.MTU) {
|
||||
return resources.getString(R.string.bad_config_explanation_positive_number)
|
||||
} else if (bce.location == BadConfigException.Location.PERSISTENT_KEEPALIVE) {
|
||||
return resources.getString(R.string.bad_config_explanation_pka)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
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))
|
||||
} else if (bce.cause is ParseException) {
|
||||
val pe = bce.cause as ParseException?
|
||||
val type = resources.getString((if (PE_CLASS_MAP.containsKey(pe!!.parsingClass)) PE_CLASS_MAP[pe.parsingClass] else R.string.parse_error_generic)!!)
|
||||
return resources.getString(R.string.parse_error_reason, type, pe.text)
|
||||
}
|
||||
return resources.getString(BCE_REASON_MAP.getValue(bce.reason), bce.text)
|
||||
}
|
||||
|
||||
private fun rootCause(throwable: Throwable): Throwable {
|
||||
var cause = throwable
|
||||
while (cause.cause != null) {
|
||||
if (cause is BadConfigException || cause is BackendException ||
|
||||
cause is RootShellException) break
|
||||
val nextCause = cause.cause!!
|
||||
if (nextCause is RemoteException) break
|
||||
cause = nextCause
|
||||
}
|
||||
return cause
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java9.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* Helpers for logging exceptions from asynchronous tasks. These can be passed to
|
||||
* {@code CompletionStage.whenComplete()} at the end of an asynchronous future chain.
|
||||
*/
|
||||
|
||||
public enum ExceptionLoggers implements BiConsumer<Object, Throwable> {
|
||||
D(Log.DEBUG),
|
||||
E(Log.ERROR);
|
||||
|
||||
private static final String TAG = "WireGuard/" + ExceptionLoggers.class.getSimpleName();
|
||||
private final int priority;
|
||||
|
||||
ExceptionLoggers(final int priority) {
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(final Object result, @Nullable final Throwable throwable) {
|
||||
if (throwable != null)
|
||||
Log.println(Log.ERROR, TAG, Log.getStackTraceString(throwable));
|
||||
else if (priority <= Log.DEBUG)
|
||||
Log.println(priority, TAG, "Future completed successfully");
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,24 @@ package com.wireguard.android.util
|
||||
import android.content.Context
|
||||
import android.util.TypedValue
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.Preference
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.activity.SettingsActivity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
fun Context.resolveAttribute(@AttrRes attrRes: Int): Int {
|
||||
val typedValue = TypedValue()
|
||||
theme.resolveAttribute(attrRes, typedValue, true)
|
||||
return typedValue.data
|
||||
}
|
||||
|
||||
val Any.applicationScope: CoroutineScope
|
||||
get() = Application.getCoroutineScope()
|
||||
|
||||
val Preference.activity: SettingsActivity
|
||||
get() = context as? SettingsActivity
|
||||
?: throw IllegalStateException("Failed to resolve SettingsActivity")
|
||||
|
||||
val Preference.lifecycleScope: CoroutineScope
|
||||
get() = activity.lifecycleScope
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.ContextThemeWrapper;
|
||||
|
||||
import com.wireguard.android.activity.SettingsActivity;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
|
||||
@NonNullForAll
|
||||
public final class FragmentUtils {
|
||||
private FragmentUtils() {
|
||||
// Prevent instantiation
|
||||
}
|
||||
|
||||
public static SettingsActivity getPrefActivity(final Preference preference) {
|
||||
final Context context = preference.getContext();
|
||||
if (context instanceof ContextThemeWrapper) {
|
||||
if (context instanceof SettingsActivity) {
|
||||
return ((SettingsActivity) context);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import com.wireguard.util.Keyed;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.databinding.ObservableArrayList;
|
||||
|
||||
/**
|
||||
* ArrayList that allows looking up elements by some key property. As the key property must always
|
||||
* be retrievable, this list cannot hold {@code null} elements. Because this class places no
|
||||
* restrictions on the order or duplication of keys, lookup by key, as well as all list modification
|
||||
* operations, require O(n) time.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public class ObservableKeyedArrayList<K, E extends Keyed<? extends K>>
|
||||
extends ObservableArrayList<E> implements ObservableKeyedList<K, E> {
|
||||
@Override
|
||||
public boolean add(@Nullable final E e) {
|
||||
if (e == null)
|
||||
throw new NullPointerException("Trying to add a null element");
|
||||
return super.add(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(final int index, @Nullable final E e) {
|
||||
if (e == null)
|
||||
throw new NullPointerException("Trying to add a null element");
|
||||
super.add(index, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(final Collection<? extends E> c) {
|
||||
if (c.contains(null))
|
||||
throw new NullPointerException("Trying to add a collection with null element(s)");
|
||||
return super.addAll(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(final int index, final Collection<? extends E> c) {
|
||||
if (c.contains(null))
|
||||
throw new NullPointerException("Trying to add a collection with null element(s)");
|
||||
return super.addAll(index, c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsAllKeys(final Collection<K> keys) {
|
||||
for (final K key : keys)
|
||||
if (!containsKey(key))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(final K key) {
|
||||
return indexOfKey(key) >= 0;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public E get(final K key) {
|
||||
final int index = indexOfKey(key);
|
||||
return index >= 0 ? get(index) : null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public E getLast(final K key) {
|
||||
final int index = lastIndexOfKey(key);
|
||||
return index >= 0 ? get(index) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int indexOfKey(final K key) {
|
||||
final ListIterator<E> iterator = listIterator();
|
||||
while (iterator.hasNext()) {
|
||||
final int index = iterator.nextIndex();
|
||||
if (Objects.equals(iterator.next().getKey(), key))
|
||||
return index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int lastIndexOfKey(final K key) {
|
||||
final ListIterator<E> iterator = listIterator(size());
|
||||
while (iterator.hasPrevious()) {
|
||||
final int index = iterator.previousIndex();
|
||||
if (Objects.equals(iterator.previous().getKey(), key))
|
||||
return index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public E set(final int index, @Nullable final E e) {
|
||||
if (e == null)
|
||||
throw new NullPointerException("Trying to set a null key");
|
||||
return super.set(index, e);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user