Compare commits

...

90 Commits

Author SHA1 Message Date
Jason A. Donenfeld 7b5ceac9f7 version: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-04-03 19:07:33 -06:00
Jason A. Donenfeld 9fe0019ec1 tunnel: libwg-go: use deterministic verdef name
From the ld.gold source:

  const char* name = parameters->options().soname();
  if (name == NULL)
    name = parameters->options().output_file_name();

That means by default it uses `-o {output}` as the verdef aux name,
which is random every time due to Go's build system. By passing in
`--soname={somethingexplicit}` we can instead have a deterministic
verdef.

This commit makes wireguard-android reproducible.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-04-03 19:07:33 -06:00
Jason A. Donenfeld 84654a0302 gradle: upgrade to 6.3
It looks like F-Droid finally updated.

This reverts commit cd43444d1f.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-04-03 19:07:33 -06:00
Jason A. Donenfeld cd43444d1f gradle: downgrade to 6.2.1 to deal with f-droid organizational mismanagement
The fdroidserver repo hardcodes hashes of all gradle versions that are
allowed to be used. This is currently up to date, having 6.3. However,
the f-droid build server is not up to date, because it is controlled by
the founder of f-droid, who is generally unavailable and does not update
it. Meanwhile other members of the project would like to stand up their
own build server instance, but this seems potentially arduous and
there's still the question of the signing key. At least that's the story
I was able to glean from asking around.

So, in order to work around this organizational brokeness, we just
downgrade to 6.2.1.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-04-01 23:43:24 -06:00
Jason A. Donenfeld b7028896c7 ObservableTunnel: do not cache key
Reported-by: Reza Island's <rezza.aji.ras@gmail.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-04-01 22:05:24 -06:00
Jason A. Donenfeld c1e86acb3c tunnel: libwg-go: remove -x option from flock
It's already the default and the macOS port of flock doesn't support it.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-04-01 22:05:24 -06:00
Harsh Shandilya 7d31bd2be9 build: update fragment to 1.2.4
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-01 22:05:24 -06:00
Jason A. Donenfeld 2e573a66a4 build: update agp to 3.6.2
Harsh likes to be first in line.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-04-01 22:05:24 -06:00
Harsh Shandilya 8d128cf2e9 ui: disable LongLogTag lint
We know what we're doing

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-31 03:48:00 +05:30
Jason A. Donenfeld d5ffa08480 TunnelEditorFragment: don't show bioauth if already visible
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-30 14:53:25 -06:00
Jason A. Donenfeld 43ce69bef4 version: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-30 14:14:54 -06:00
Harsh Shandilya 44a0f53e58 strings: sync translations
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-30 14:08:24 -06:00
Jason A. Donenfeld d74b988f75 global: cleanup code style
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-30 13:57:43 -06:00
Harsh Shandilya 10e910186e ui: update proguard rules to keep crash logs readable
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-30 13:49:41 -06:00
Jason A. Donenfeld 017f420d42 LogViewerActivity: don't crash if pipe closes
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-30 03:25:47 -06:00
Jason A. Donenfeld bc186fe6ad TunnelDetail: set singleLine=true for API <=23
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-30 03:23:32 -06:00
Jason A. Donenfeld 09b40cdec7 BiometricAuthenticator: rework logic and bugs
Otherwise there's a frameworks bug that causes the fragment's activity
to become null.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-30 03:23:32 -06:00
Harsh Shandilya d2721f2d7d BiometricAuthenticator: implement biometric authentication for sensitive operations
When biometric hardware is available, it will be used to authenticate
the user before private keys are shown on screen or when zip exports
are executed.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-30 03:23:32 -06:00
Harsh Shandilya 3095e19e13 ObservableTunnel: Don't cache configAsync
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-30 03:23:32 -06:00
Jason A. Donenfeld c547d033c3 ui: replace generate icon with 'sync' instead of 'toys'
I dislike this change, but Harsh thinks the toys icon is utterly insane
and maybe he's right.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-30 03:23:32 -06:00
Jason A. Donenfeld 4d4764eefb BindingAdapters: use sleeker lambda
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-30 03:23:32 -06:00
Jason A. Donenfeld d44a83faaa TunnelEditorFragment: add hooks for biometric auth
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-30 03:23:32 -06:00
Vincenzo Reale 2337fe37be strings: update Italian translation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-29 17:26:14 -06:00
Jason A. Donenfeld c5b71cb484 README: mention translations
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-29 17:16:09 -06:00
Jason A. Donenfeld 480c95d4d6 ui: remove unused stream support
The tunnel/ module still uses it and exposes it as an "api", but nothing
inside of ui/ should be using it now.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-29 17:00:31 -06:00
Harsh Shandilya 8a45e965eb strings: Sync German translations
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-29 14:10:39 +05:30
Jason A. Donenfeld c9717693d0 tunnel: libwg-go: check sha256 of downloaded tarball
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-29 02:36:29 -06:00
Jason A. Donenfeld 0fa3fe3d43 tunnel: libwg-go: stick go tarball in gradle cache
This way we don't have to build over and over.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-29 02:28:41 -06:00
Jason A. Donenfeld ed090f7ecb strings: fix positional specifier in russian translation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-29 01:52:01 -06:00
Jason A. Donenfeld f916f96761 tunnel: libwg-go: prevent parallel downloads
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-29 01:31:39 -06:00
Jason A. Donenfeld 79e766c4e1 tunnel: fix package name being passed through to cmake
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-29 01:18:46 -06:00
Harsh Shandilya 183273dcf5 AddTunnelsSheet: Make behaviour nullable
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-29 11:04:59 +05:30
Harsh Shandilya b3bb7c694b build: Improve R8 rules and enable aggressive optimization modes
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-29 10:52:54 +05:30
Jason A. Donenfeld 456a74db05 global: hardcode tags so that minification doesn't ruin the log
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-28 23:21:12 -06:00
Jason A. Donenfeld fde724a658 version: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-28 23:07:50 -06:00
Jason A. Donenfeld 574ee5d0bb LogViewerActivity: only scroll every quarter for the first 2.5 seconds of dumping
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-28 23:07:50 -06:00
Harsh Shandilya 03a838ba2d ui: Remove unnecessary non-null assertion in TAG fields
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-28 23:07:50 -06:00
Harsh Shandilya b00aacbc41 Fix tunnel editor theming
Also set textInputStyle in the base theme to avoid setting styles to all elements

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-28 22:38:04 -06:00
Jason A. Donenfeld 532c33a13b MonkeyedTextInputEditText: make more robust
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-28 22:38:04 -06:00
Jason A. Donenfeld 6a7396bc1d ui: use kotlin class instead of java class for tag
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-28 16:26:22 -06:00
Jason A. Donenfeld 870b2bf36d ObservableTunnel: do not cache stats
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-28 16:26:22 -06:00
Jason A. Donenfeld 07b69be7bf TunnelEditor: fix up allowedips margins
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-28 16:26:22 -06:00
Jason A. Donenfeld b41640837c TunnelEditor: get rid of weird gravity on add peer button
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-28 16:26:22 -06:00
Jason A. Donenfeld bee6ebe3b4 TunnelEditor: move generate button to inside box
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-28 16:26:22 -06:00
Jason A. Donenfeld 5989298d3f TunnelEditor: subdue section font
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-28 16:26:22 -06:00
Jason A. Donenfeld 0235f19543 TunnelEditor: abstract out section font
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-28 16:26:22 -06:00
Jason A. Donenfeld cd6c2f68ca TunnelEditor: properly align mtu and listen port
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-28 16:26:22 -06:00
Jason A. Donenfeld 75252cf9d5 TunnelEditor: move hint to label and add discouraged optional
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-28 16:26:22 -06:00
Jason A. Donenfeld 1da714852f TunnelEditor: clean up xml
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-28 16:26:22 -06:00
Jason A. Donenfeld fe6b788f6b MonkeyedTextInputEditText: introduce a new horror
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-28 16:26:22 -06:00
Harsh Shandilya fb3fec299f TunnelEditor: replace tunnel detail view with ConstraintLayout implementation
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-28 15:22:11 -06:00
Harsh Shandilya d2f435b265 ClipboardUtils: Update to handle TextInputEditText
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-28 15:22:06 -06:00
Harsh Shandilya 3a163acd6d gradle: Remove useless comment
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-28 19:50:34 +05:30
Harsh Shandilya df4bf9b688 Disable screen capture in tunnel editor
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-28 19:49:54 +05:30
Harsh Shandilya 0f67a2f194 Upgrade streamsupport dependency
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-28 19:21:53 +05:30
Jason A. Donenfeld b75946af46 TunnelComparator: naturally sort tunnel list
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-28 01:42:06 -06:00
Harsh Shandilya b9b188693c strings: Sync from Crowdin
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-28 02:55:21 +05:30
Harsh Shandilya 9fe008d407 ObservableTunnel: Don't recurse in getConfig
The correct way to retrieve the value inside a getter/setter is to use `field` to ensure
you don't invoke the getter while inside the getter and trigger a stack overflow

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-28 02:35:01 +05:30
Jason A. Donenfeld e905c355f9 README: mention docs location
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-27 14:26:08 -06:00
Alexey 12821fb70d strings: update Russian translate
Signed-off-by: Alexey <zasranecc@bk.ru>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-27 13:31:20 -06:00
Jason A. Donenfeld 2aaa316280 gradle: bump threetenabp to 1.2.3
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-27 13:20:51 -06:00
Jason A. Donenfeld 918d9b8b1f databinding: simplify and address warnings
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-27 15:38:04 +05:30
Harsh Shandilya 536a6f3f83 ui: Replace ErrorMessages getter with indexing operator
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-27 15:38:04 +05:30
Harsh Shandilya b9fd3d37f2 Convert ErrorMessages to Kotlin
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-27 15:38:04 +05:30
Harsh Shandilya e0b87c3ff2 Convert AsyncWorker to Kotlin
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-27 03:09:24 -06:00
Jason A. Donenfeld 48a9fd46a6 databinding: rewrite in kotlin
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-27 03:08:47 -06:00
Jason A. Donenfeld 8669c01eaa util: begin conversion to kotlin
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-27 03:08:47 -06:00
Jason A. Donenfeld 37949ba1ec TunnelManager: convert to kotlin
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-26 23:21:54 -06:00
Harsh Shandilya b2bbaf050c util: Start converting to Kotlin
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-26 22:33:01 -06:00
Jason A. Donenfeld 4d6837ea53 ObservableTunnel: port to kotlin
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-26 22:30:10 -06:00
Jason A. Donenfeld c8ac970d11 LogViewerActivity: merge lines that don't match regex
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-26 16:59:54 -06:00
Jason A. Donenfeld a3a429bc41 LogViewerActivity: re-add error case to log export
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-26 15:58:02 -06:00
Alexey 0726b1b4d9 strings: update Russian translate
Signed-off-by: Alexey <zasranecc@bk.ru>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-26 15:43:41 -06:00
Jason A. Donenfeld defc4f45ff LogViewerActivity: constify request
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-26 14:56:13 -06:00
Jason A. Donenfeld a984127e28 LogViewerActivity: set a separate title
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-26 14:59:31 +05:30
Jason A. Donenfeld 85dd303c88 ui: root: rewrite in kotlin
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-26 01:55:44 -06:00
Jason A. Donenfeld 2958144fd0 ui: cleanup various pieces of kotlin
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-26 00:51:36 -06:00
Jason A. Donenfeld ade8f18a95 xml: cleanup
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-26 00:49:01 -06:00
Jason A. Donenfeld 46e2e29ead preferences: add key to all items to squelch warnings
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-26 00:49:01 -06:00
Harsh Shandilya 63a395125a Introduce realtime log viewer
This contains a share button and a save button, the former using a
custom content provider.

Co-authored-by: Jason A. Donenfeld <Jason@zx2c4.com>
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-26 00:49:01 -06:00
Alexey 6f973afa36 strings: update Russian translate
Signed-off-by: Alexey <zasranecc@bk.ru>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-25 12:43:54 -06:00
Harsh Shandilya 1ad0ef3f61 LogExporterPreference: Don't ask for storage permissions on Android 10 and above
We use the proper MediaStore implementation on Android 10 which makes it unnecessary.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-25 21:02:02 +05:30
Eiji Tanioka 90bf46e8d3 strings: Explicit indexing for 'import_partial_success'
When translate it to Japanese, values are exchanged.
For example, '1 of 10' is '10 個中の 1'.

So this string should be explicit indexed.

Signed-off-by: Eiji Tanioka <tanioka404@gmail.com>
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-25 20:23:45 +05:30
Harsh Shandilya c1c285db86 Upgrade to Gradle 6.3
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-25 02:05:12 +05:30
Jason A. Donenfeld f1b541a1eb ui: squelch warnings
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-24 14:17:18 -06:00
Jason A. Donenfeld 585257c995 TunnelManager: disallow remote apps for L
I don't trust old Android versions to have gotten this right.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-24 14:17:15 -06:00
Harsh Shandilya 93f80cdc50 Resolve manifest warnings
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-24 11:30:28 +05:30
Jason A. Donenfeld a832193010 TunnelManager: save settings before restart
Otherwise these get lost and then the restored state is confusing.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-23 18:14:23 -06:00
Harsh Shandilya 383659fb8a Upgrade to Kotlin 1.3.71
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-23 22:50:04 +05:30
Jason A. Donenfeld 4725e55090 libwg-go: update go modules
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-22 14:08:20 -06:00
129 changed files with 3503 additions and 3047 deletions
+7 -1
View File
@@ -12,9 +12,11 @@ $ 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'
@@ -28,3 +30,7 @@ compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
}
```
## Translating
Please help us translate the app into several languages on [our translation platform](https://crowdin.com/project/WireGuard).
+15 -15
View File
@@ -1,3 +1,4 @@
allprojects {
repositories {
google()
@@ -7,29 +8,28 @@ allprojects {
buildscript {
ext {
agpVersion = '3.6.2'
annotationsVersion = '1.1.0'
appcompatVersion = '1.1.0'
bintrayPluginVersion = '1.8.4'
biometricVersion = '1.0.1'
cardviewVersion = '1.0.0'
collectionVersion = '1.1.0'
coreKtxVersion = '1.2.0'
constraintLayoutVersion = '1.1.3'
coordinatorLayoutVersion = '1.1.0'
agpVersion = '3.6.1'
fragmentVersion = '1.2.2'
materialComponentsVersion = '1.1.0'
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.
zxingEmbeddedVersion = '3.6.0'
coreKtxVersion = '1.2.0'
coroutinesVersion = '1.3.5'
eddsaVersion = '0.3.0'
bintrayPluginVersion = '1.8.4'
mavenPluginVersion = '2.1'
fragmentVersion = '1.2.4'
jsr305Version = '3.0.2'
junitVersion = '4.13'
kotlinVersion = '1.3.71'
materialComponentsVersion = '1.1.0'
mavenPluginVersion = '2.1'
preferenceVersion = '1.1.0'
streamsupportVersion = '1.7.2'
threetenabpVersion = '1.2.3'
zxingEmbeddedVersion = '3.6.0'
groupName = 'com.wireguard.android'
}
+14
View File
@@ -17,3 +17,17 @@ 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
# R8 Full mode
android.enableR8.fullMode=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
Binary file not shown.
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+16
View File
@@ -26,6 +26,22 @@ android {
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}"
}
}
}
}
}
dependencies {
+2 -1
View File
@@ -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>
@@ -11,6 +11,7 @@ import com.wireguard.util.NonNullForAll;
public final class BackendException extends Exception {
private final Object[] format;
private final Reason reason;
public BackendException(final Reason reason, final Object... format) {
this.reason = reason;
this.format = format;
@@ -35,13 +35,14 @@ import androidx.collection.ArraySet;
@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 GoBackend(final Context context) {
SharedLibraryLoader.loadSharedLibrary(context, "wg-go");
this.context = context;
@@ -41,7 +41,7 @@ import java9.util.stream.Stream;
@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<>();
@@ -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;
@@ -189,6 +189,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;
@@ -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);
}
}
}
+1
View File
@@ -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}
+24 -8
View File
@@ -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.14.1
GO_PLATFORM := $(shell uname -s | tr '[:upper:]' '[:lower:]')-$(NDK_GO_ARCH_MAP_$(shell uname -m))
GO_TARBALL := go$(GO_VERSION).$(GO_PLATFORM).tar.gz
GO_HASH_darwin-amd64 := 6632f9d53fd95632e431e8c34295349cca3f0a124e3a28b760ae5c42b32816e3
GO_HASH_linux-amd64 := 2f49eb17ce8b48c680cdb166ffd7389702c0dec6effa090c324804a5cac8a7f8
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:
+4 -4
View File
@@ -3,8 +3,8 @@ module golang.zx2c4.com/wireguard/android
go 1.13
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/crypto v0.0.0-20200320181102-891825fb96df // indirect
golang.org/x/net v0.0.0-20200320220750-118fecf932d8 // indirect
golang.org/x/sys v0.0.0-20200321134203-328b4cd54aae
golang.zx2c4.com/wireguard v0.0.20200320
)
+9 -9
View File
@@ -1,19 +1,19 @@
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-20200320181102-891825fb96df h1:lDWgvUvNnaTnNBc/dwOty86cFeKoKWbwy2wQj0gIxbU=
golang.org/x/crypto v0.0.0-20200320181102-891825fb96df/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-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-20200320220750-118fecf932d8 h1:1+zQlQqEEhUeStBTi653GZAnAuivZq/2hz+Iz+OP7rg=
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/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-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200321134203-328b4cd54aae h1:3tcmuaB7wwSZtelmiv479UjUB+vviwABz7a133ZwOKQ=
golang.org/x/sys v0.0.0-20200321134203-328b4cd54aae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.zx2c4.com/wireguard v0.0.20200121 h1:vcswa5Q6f+sylDfjqyrVNNrjsFUUbPsgAQTBCAg/Qf8=
golang.zx2c4.com/wireguard v0.0.20200121/go.mod h1:P2HsVp8SKwZEufsnezXZA4GRX/T49/HlU7DGuelXsU4=
golang.zx2c4.com/wireguard v0.0.20200320 h1:1vE6zVeO7fix9cJX1Z9ZQ+ikPIIx7vIyU0o0tLDD88g=
golang.zx2c4.com/wireguard v0.0.20200320/go.mod h1:lDian4Sw4poJ04SgHh35nzMVwGSYlPumkdnHcucAQoY=
+8 -12
View File
@@ -22,6 +22,7 @@ android {
}
compileSdkVersion 29
dataBinding.enabled true
viewBinding.enabled true
defaultConfig {
applicationId 'com.wireguard.android'
minSdkVersion 21
@@ -50,24 +51,17 @@ 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 {
@@ -77,6 +71,7 @@ dependencies {
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"
@@ -84,8 +79,9 @@ dependencies {
implementation "com.google.android.material:material:$materialComponentsVersion"
implementation "com.journeyapps:zxing-android-embedded:$zxingEmbeddedVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
implementation "net.sourceforge.streamsupport:android-retrofuture:$streamsupportVersion"
implementation "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion"
}
tasks.withType(JavaCompile) {
+36
View File
@@ -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>(...);
}
+11
View File
@@ -1,3 +1,14 @@
# Squelch all warnings, they're harmless but ProGuard
# escalates them as errors.
-dontwarn sun.misc.Unsafe
# Retain some information to keep stacktraces usable
-keepattributes SourceFile,LineNumberTable
# Fragment 1.2.4 allows Fragment classes to be obfuscated but
# databinding references in XML seem to not be rewritten to
# match, so we preserve the names as 1.2.3 did.
-if public class ** extends androidx.fragment.app.Fragment
-keep public class <1> {
public <init>();
}
+30 -4
View File
@@ -9,7 +9,15 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<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,17 +29,21 @@
<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>
@@ -56,6 +68,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" />
@@ -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,159 @@
/*
* 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.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.AsyncTask
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.StrictMode
import android.os.StrictMode.VmPolicy
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.PreferenceManager
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.GoBackend
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.configStore.FileConfigStore
import com.wireguard.android.model.TunnelManager
import com.wireguard.android.util.AsyncWorker
import com.wireguard.android.util.ExceptionLoggers
import com.wireguard.android.util.ModuleLoader
import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller
import java9.util.concurrent.CompletableFuture
import java.lang.ref.WeakReference
import java.util.Locale
class Application : android.app.Application(), OnSharedPreferenceChangeListener {
private val futureBackend = CompletableFuture<Backend>()
private lateinit var asyncWorker: AsyncWorker
private var backend: Backend? = null
private lateinit var moduleLoader: ModuleLoader
private lateinit var rootShell: RootShell
private lateinit var sharedPreferences: SharedPreferences
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())
}
}
override fun onCreate() {
Log.i(TAG, USER_AGENT)
super.onCreate()
asyncWorker = AsyncWorker(AsyncTask.SERIAL_EXECUTOR, Handler(Looper.getMainLooper()))
rootShell = RootShell(applicationContext)
toolsInstaller = ToolsInstaller(applicationContext, rootShell)
moduleLoader = ModuleLoader(applicationContext, rootShell, USER_AGENT)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
AppCompatDelegate.setDefaultNightMode(
if (sharedPreferences.getBoolean("dark_theme", false)) AppCompatDelegate.MODE_NIGHT_YES else AppCompatDelegate.MODE_NIGHT_NO)
} else {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
}
tunnelManager = TunnelManager(FileConfigStore(applicationContext))
tunnelManager.onCreate()
asyncWorker.supplyAsync(Companion::getBackend).thenAccept { futureBackend.complete(it) }
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
if ("multiple_tunnels" == key && backend != null && backend is WgQuickBackend)
(backend as WgQuickBackend).setMultipleTunnels(sharedPreferences.getBoolean(key, false))
}
override fun onTerminate() {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
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
fun getAsyncWorker() = get().asyncWorker
@JvmStatic
fun getBackend(): Backend {
val app = get()
synchronized(app.futureBackend) {
if (app.backend == null) {
var backend: Backend? = null
var didStartRootShell = false
if (!ModuleLoader.isModuleLoaded() && app.moduleLoader.moduleMightExist()) {
try {
app.rootShell.start()
didStartRootShell = true
app.moduleLoader.loadModule()
} catch (ignored: Exception) {
}
}
if (!app.sharedPreferences.getBoolean("disable_kernel_module", false) && ModuleLoader.isModuleLoaded()) {
try {
if (!didStartRootShell)
app.rootShell.start()
val wgQuickBackend = WgQuickBackend(app.applicationContext, app.rootShell, app.toolsInstaller)
wgQuickBackend.setMultipleTunnels(app.sharedPreferences.getBoolean("multiple_tunnels", false))
backend = wgQuickBackend
} catch (ignored: Exception) {
}
}
if (backend == null) {
backend = GoBackend(app.applicationContext)
GoBackend.setAlwaysOnCallback { get().tunnelManager.restoreState(true).whenComplete(ExceptionLoggers.D) }
}
app.backend = backend
}
return app.backend!!
}
}
@JvmStatic
fun getBackendAsync() = get().futureBackend
@JvmStatic
fun getModuleLoader() = get().moduleLoader
@JvmStatic
fun getRootShell() = get().rootShell
@JvmStatic
fun getSharedPreferences() = get().sharedPreferences
@JvmStatic
fun getToolsInstaller() = get().toolsInstaller
@JvmStatic
fun getTunnelManager() = get().tunnelManager
}
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.Backend
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.ExceptionLoggers
class BootShutdownReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Application.getBackendAsync().thenAccept { backend: Backend? ->
if (backend !is WgQuickBackend) return@thenAccept
val action = intent.action ?: return@thenAccept
val tunnelManager = Application.getTunnelManager()
if (Intent.ACTION_BOOT_COMPLETED == action) {
Log.i(TAG, "Broadcast receiver restoring state (boot)")
tunnelManager.restoreState(false).whenComplete(ExceptionLoggers.D)
} 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,158 @@
/*
* 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.widget.SlashDrawable
/**
* 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: Exception) {
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()
}
tunnel!!.setStateAsync(Tunnel.State.TOGGLE).whenComplete { _, t ->
if (t == null) {
updateTile()
} else {
val toggleIntent = Intent(this, 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"
}
}
@@ -23,6 +23,7 @@ abstract class BaseActivity : ThemeChangeAwareActivity() {
onSelectedTunnelChanged(oldTunnel, value)
selectionChangeRegistry.notifyCallbacks(oldTunnel, 0, value)
}
fun addOnSelectedTunnelChangedListener(listener: OnSelectedTunnelChangedListener) {
selectionChangeRegistry.add(listener)
}
@@ -0,0 +1,356 @@
/*
* 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.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ShareCompat
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.android.widget.EdgeToEdge.setUpFAB
import com.wireguard.android.widget.EdgeToEdge.setUpRoot
import com.wireguard.android.widget.EdgeToEdge.setUpScrollingContent
import com.wireguard.crypto.KeyPair
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.BufferedReader
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 coroutineScope = CoroutineScope(Dispatchers.Default)
private val year by lazy {
val yearFormatter: DateFormat = SimpleDateFormat("yyyy", Locale.US)
yearFormatter.format(Date())
}
@Suppress("Deprecation")
private val defaultColor by lazy { resources.getColor(R.color.primary_text_color) }
@Suppress("Deprecation")
private val debugColor by lazy { resources.getColor(R.color.debug_tag_color) }
@Suppress("Deprecation")
private val errorColor by lazy { resources.getColor(R.color.error_tag_color) }
@Suppress("Deprecation")
private val infoColor by lazy { resources.getColor(R.color.info_tag_color) }
@Suppress("Deprecation")
private val warningColor by lazy { resources.getColor(R.color.warning_tag_color) }
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)
setUpFAB(binding.shareFab)
setUpRoot(binding.root)
setUpScrollingContent(binding.recyclerView, binding.shareFab)
logAdapter = LogEntryAdapter()
binding.recyclerView.apply {
recyclerView = this
layoutManager = LinearLayoutManager(context)
adapter = logAdapter
addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL))
}
coroutineScope.launch { streamingLog() }
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)
startActivityForResult(shareIntent, SHARE_ACTIVITY_REQUEST)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == SHARE_ACTIVITY_REQUEST) {
revokeLastUri()
}
super.onActivityResult(requestCode, resultCode, data)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.log_viewer, menu)
saveButton = menu?.findItem(R.id.save_log)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
finish()
true
}
R.id.save_log -> {
coroutineScope.launch { saveLog() }
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onDestroy() {
super.onDestroy()
coroutineScope.cancel()
}
private suspend fun saveLog() {
val context = this
withContext(Dispatchers.Main) {
saveButton?.isEnabled = false
withContext(Dispatchers.IO) {
var exception: Throwable? = null
var outputFile: DownloadsFileSaver.DownloadsFile? = null
try {
outputFile = DownloadsFileSaver.save(context, "wireguard-log.txt", "text/plain", true)
outputFile.outputStream.use {
it.write(rawLogLines.toString().toByteArray(Charsets.UTF_8))
}
} catch (e: Throwable) {
outputFile?.delete()
exception = e
}
withContext(Dispatchers.Main) {
saveButton?.isEnabled = true
Snackbar.make(findViewById(android.R.id.content),
if (exception == null) getString(R.string.log_export_success, outputFile?.fileName)
else getString(R.string.log_export_error, ErrorMessages[exception]),
if (exception == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG)
.setAnchorView(binding.shareFab)
.show()
}
}
}
}
private suspend fun streamingLog() = withContext(Dispatchers.IO) {
val builder = ProcessBuilder().command("logcat", "-b", "all", "-v", "threadtime", "*:V")
builder.environment()["LC_ALL"] = "C"
val process = try {
builder.start()
} catch (e: IOException) {
e.printStackTrace()
return@withContext
}
val stdout = BufferedReader(InputStreamReader(process!!.inputStream, StandardCharsets.UTF_8))
var haveScrolled = false
val start = System.nanoTime()
var startPeriod = start
while (true) {
val line = stdout.readLine() ?: break
rawLogLines.append(line)
rawLogLines.append('\n')
val logLine = parseLine(line)
withContext(Dispatchers.Main) {
if (logLine != null) {
recyclerView?.let {
val shouldScroll = haveScrolled && !it.canScrollVertically(1)
logLines.add(logLine)
if (haveScrolled) logAdapter.notifyDataSetChanged()
if (shouldScroll)
it.scrollToPosition(logLines.size - 1)
}
} else {
/* I'd prefer for the next line to be:
* logLines.lastOrNull()?.msg += "\n$line"
* However, as of writing, that causes the kotlin compiler to freak out and crash, spewing bytecode.
*/
logLines.lastOrNull()?.apply { msg += "\n$line" }
if (haveScrolled) logAdapter.notifyDataSetChanged()
}
if (!haveScrolled) {
val end = System.nanoTime()
val scroll = (end - start) > 1000000000L * 2.5 || !stdout.ready()
if (logLines.isNotEmpty() && (scroll || (end - startPeriod) > 1000000000L / 4)) {
logAdapter.notifyDataSetChanged()
recyclerView?.scrollToPosition(logLines.size - 1)
startPeriod = end
}
if (scroll) haveScrolled = true
}
}
}
}
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 SHARE_ACTIVITY_REQUEST = 49133
}
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 (_: Exception) {
}
}
}
}
}
@@ -4,6 +4,7 @@
*/
package com.wireguard.android.activity
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
@@ -82,6 +83,10 @@ 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)
}
val wgQuickOnlyPrefs = arrayOf(
preferenceManager.findPreference("tools_installer"),
preferenceManager.findPreference("restore_on_boot"),
@@ -96,6 +101,10 @@ class SettingsActivity : ThemeChangeAwareActivity() {
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
@@ -23,7 +23,7 @@ class TunnelToggleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val tunnel = Application.getTunnelManager().lastUsedTunnel ?: return
tunnel.setState(Tunnel.State.TOGGLE).whenComplete { _, t ->
tunnel.setStateAsync(Tunnel.State.TOGGLE).whenComplete { _, t ->
TileService.requestListeningState(this, ComponentName(this, QuickTileService::class.java))
onToggleFinished(t)
finishAffinity()
@@ -32,13 +32,13 @@ class TunnelToggleActivity : AppCompatActivity() {
private fun onToggleFinished(throwable: Throwable?) {
if (throwable == null) return
val error = ErrorMessages.get(throwable)
val error = ErrorMessages[throwable]
val message = getString(R.string.toggle_error, error)
Log.e(TAG, message, throwable)
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
}
companion object {
private val TAG = "WireGuard/" + TunnelToggleActivity::class.java.simpleName
private const val TAG = "WireGuard/TunnelToggleActivity"
}
}
@@ -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,147 @@
/*
* 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 java9.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 setText(view: TextView, text: Optional<*>) {
view.text = text.map { it.toString() }.orElse("")
}
@JvmStatic
@BindingAdapter("android:text")
fun setText(view: TextView, networks: Iterable<InetNetwork?>?) {
view.text = if (networks != null) Attribute.join(networks) else ""
}
}
@@ -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
}
}
@@ -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;
}
}
}
@@ -0,0 +1,104 @@
/*
* 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()
}
}
@@ -23,7 +23,7 @@ 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) {
}
@@ -51,7 +51,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,7 +78,7 @@ class AddTunnelsSheet : BottomSheetDialogFragment() {
override fun dismiss() {
super.dismiss()
behavior.removeBottomSheetCallback(bottomSheetCallback)
behavior?.removeBottomSheetCallback(bottomSheetCallback)
}
private fun requireTargetFragment(): Fragment {
@@ -15,13 +15,12 @@ import androidx.fragment.app.Fragment
import com.wireguard.android.Application
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
class AppListDialogFragment : DialogFragment() {
private val appData: ObservableKeyedList<String, ApplicationData> = ObservableKeyedArrayList()
private val appData: ObservableKeyedArrayList<String, ApplicationData> = ObservableKeyedArrayList()
private var currentlyExcludedApps = emptyList<String>()
private fun loadData() {
@@ -43,7 +42,7 @@ class AppListDialogFragment : DialogFragment() {
appData.clear()
appData.addAll(data)
} else {
val error = ErrorMessages.get(throwable)
val error = ErrorMessages[throwable]
val message = activity.getString(R.string.error_fetching_apps, error)
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
dismissAllowingStateLoss()
@@ -85,9 +85,9 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
}
private fun setTunnelStateWithPermissionsResult(tunnel: ObservableTunnel, checked: Boolean) {
tunnel.setState(Tunnel.State.of(checked)).whenComplete { _, throwable ->
tunnel.setStateAsync(Tunnel.State.of(checked)).whenComplete { _, throwable ->
if (throwable == null) return@whenComplete
val error = ErrorMessages.get(throwable)
val error = ErrorMessages[throwable]
val messageResId = if (checked) R.string.error_up else R.string.error_down
val message = requireContext().getString(messageResId, error)
val view = view
@@ -103,6 +103,6 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
companion object {
private const val REQUEST_CODE_VPN_PERMISSION = 23491
private val TAG = "WireGuard/" + BaseFragment::class.java.simpleName
private const val TAG = "WireGuard/BaseFragment"
}
}
@@ -49,8 +49,8 @@ class ConfigNamingDialogFragment : DialogFragment() {
val configBytes = configText!!.toByteArray(StandardCharsets.UTF_8)
config = try {
Config.parse(ByteArrayInputStream(configBytes))
} catch(e: Exception) {
when(e) {
} catch (e: Exception) {
when (e) {
is BadConfigException, is IOException -> throw IllegalArgumentException("Invalid config passed to ${javaClass.simpleName}", e)
else -> throw e
}
@@ -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,7 +14,9 @@ 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 com.google.android.material.snackbar.Snackbar
import com.wireguard.android.Application
@@ -22,16 +25,18 @@ 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.BiometricAuthenticator
import com.wireguard.android.util.ErrorMessages
import com.wireguard.android.viewmodel.ConfigProxy
import com.wireguard.android.widget.EdgeToEdge.setUpRoot
import com.wireguard.android.widget.EdgeToEdge.setUpScrollingContent
import com.wireguard.config.Config
/**
* Fragment for editing a WireGuard configuration.
*/
class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
private var haveShownKeys = false
private var binding: TunnelEditorFragmentBinding? = null
private var tunnel: ObservableTunnel? = null
private fun onConfigLoaded(config: Config) {
@@ -46,7 +51,7 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
onFinished()
} else {
val error = ErrorMessages.get(throwable)
val error = ErrorMessages[throwable]
message = getString(R.string.config_save_error, savedTunnel.name, error)
Log.e(TAG, message, throwable)
binding?.let {
@@ -72,11 +77,13 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
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()
}
@@ -99,7 +106,7 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
InputMethodManager.HIDE_NOT_ALWAYS)
}
// Tell the activity to finish itself or go back to the detail view.
requireActivity().runOnUiThread {
activity.runOnUiThread {
// TODO(smaeul): Remove this hack when fixing the Config ViewModel
// The selected tunnel has to actually change, but we have to remember this one.
val savedTunnel = tunnel
@@ -114,7 +121,7 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
val newConfig = try {
binding!!.config!!.resolve()
} catch (e: Exception) {
val error = ErrorMessages.get(e)
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)
@@ -125,17 +132,15 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
tunnel == null -> {
Log.d(TAG, "Attempting to create new tunnel " + binding!!.name)
val manager = Application.getTunnelManager()
manager.create(binding!!.name!!, newConfig)
.whenComplete(this::onTunnelCreated)
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) }
tunnel!!.setNameAsync(binding!!.name!!).whenComplete { _, t -> onTunnelRenamed(tunnel!!, newConfig, t) }
}
else -> {
Log.d(TAG, "Attempting to save config of " + tunnel!!.name)
tunnel!!.setConfig(newConfig)
tunnel!!.setConfigAsync(newConfig)
.whenComplete { _, t -> onConfigSaved(tunnel!!, t) }
}
}
@@ -181,7 +186,7 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
onFinished()
} else {
val error = ErrorMessages.get(throwable)
val error = ErrorMessages[throwable]
message = getString(R.string.tunnel_create_error, error)
Log.e(TAG, message, throwable)
binding?.let {
@@ -198,9 +203,9 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
Log.d(TAG, message)
// Now save the rest of configuration changes.
Log.d(TAG, "Attempting to save config of renamed tunnel " + tunnel!!.name)
renamedTunnel.setConfig(newConfig).whenComplete { _, t -> onConfigSaved(renamedTunnel, t) }
renamedTunnel.setConfigAsync(newConfig).whenComplete { _, t -> onConfigSaved(renamedTunnel, t) }
} else {
val error = ErrorMessages.get(throwable)
val error = ErrorMessages[throwable]
message = getString(R.string.tunnel_rename_error, error)
Log.e(TAG, message, throwable)
binding?.let {
@@ -223,9 +228,45 @@ 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()) {
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"
}
}
@@ -28,10 +28,10 @@ import com.wireguard.android.databinding.TunnelListFragmentBinding
import com.wireguard.android.databinding.TunnelListItemBinding
import com.wireguard.android.fragment.ConfigNamingDialogFragment.Companion.newInstance
import com.wireguard.android.model.ObservableTunnel
import com.wireguard.android.util.ErrorMessages
import com.wireguard.android.widget.EdgeToEdge.setUpFAB
import com.wireguard.android.widget.EdgeToEdge.setUpRoot
import com.wireguard.android.widget.EdgeToEdge.setUpScrollingContent
import com.wireguard.android.util.ErrorMessages
import com.wireguard.android.widget.MultiselectableRelativeLayout
import com.wireguard.config.Config
import java9.util.concurrent.CompletableFuture
@@ -129,7 +129,7 @@ class TunnelListFragment : BaseFragment() {
futureTunnels.add(
Application.getTunnelManager().create(
name,
Config.parse(contentResolver.openInputStream(uri))
Config.parse(contentResolver.openInputStream(uri)!!)
).toCompletableFuture()
)
}
@@ -235,7 +235,7 @@ class TunnelListFragment : BaseFragment() {
if (throwable == null) {
message = resources.getQuantityString(R.plurals.delete_success, count, count)
} else {
val error = ErrorMessages.get(throwable)
val error = ErrorMessages[throwable]
message = resources.getQuantityString(R.plurals.delete_error, count, count, error)
Log.e(TAG, message, throwable)
}
@@ -245,7 +245,7 @@ class TunnelListFragment : BaseFragment() {
private fun onTunnelImportFinished(tunnels: List<ObservableTunnel>, throwables: Collection<Throwable>) {
var message = ""
for (throwable in throwables) {
val error = ErrorMessages.get(throwable)
val error = ErrorMessages[throwable]
message = getString(R.string.import_error, error)
Log.e(TAG, message, throwable)
}
@@ -254,11 +254,11 @@ class TunnelListFragment : BaseFragment() {
else if (tunnels.isEmpty() && throwables.size == 1)
else if (throwables.isEmpty())
message = resources.getQuantityString(R.plurals.import_total_success,
tunnels.size, tunnels.size)
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)
tunnels.size + throwables.size,
tunnels.size, tunnels.size + throwables.size)
showSnackbar(message)
}
@@ -267,23 +267,26 @@ class TunnelListFragment : BaseFragment() {
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)
val parent = this
binding!!.rowConfigurationHandler = object : RowConfigurationHandler<TunnelListItemBinding, ObservableTunnel> {
override fun onConfigureRow(binding: TunnelListItemBinding, item: ObservableTunnel, position: Int) {
binding.fragment = parent
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)
}
}
@@ -394,6 +397,6 @@ class TunnelListFragment : BaseFragment() {
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,12 +8,10 @@ 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, isExcludedFromTunnel: Boolean) : BaseObservable(), Keyed<String> {
override val key = name
@get:Bindable
var isExcludedFromTunnel = isExcludedFromTunnel
@@ -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,116 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.model
import androidx.databinding.BaseObservable
import androidx.databinding.Bindable
import com.wireguard.android.BR
import com.wireguard.android.backend.Statistics
import com.wireguard.android.backend.Tunnel
import com.wireguard.android.databinding.Keyed
import com.wireguard.android.util.ExceptionLoggers
import com.wireguard.config.Config
import java9.util.concurrent.CompletableFuture
import java9.util.concurrent.CompletionStage
/**
* 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
fun setNameAsync(name: String): CompletionStage<String> = if (name != this.name)
manager.setTunnelName(this, name)
else
CompletableFuture.completedFuture(this.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
}
fun setStateAsync(state: Tunnel.State): CompletionStage<Tunnel.State> = if (state != this.state)
manager.setTunnelState(this, state)
else
CompletableFuture.completedFuture(this.state)
@get:Bindable
var config = config
get() {
if (field == null)
manager.getTunnelConfig(this).whenComplete(ExceptionLoggers.E)
return field
}
private set
val configAsync: CompletionStage<Config>
get() = if (config == null)
manager.getTunnelConfig(this)
else
CompletableFuture.completedFuture(config)
fun setConfigAsync(config: Config): CompletionStage<Config> = if (config != this.config)
manager.setTunnelConfig(this, config)
else
CompletableFuture.completedFuture(this.config)
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)
manager.getTunnelStatistics(this).whenComplete(ExceptionLoggers.E)
return field
}
private set
val statisticsAsync: CompletionStage<Statistics>
get() = if (statistics == null || statistics!!.isStale)
manager.getTunnelStatistics(this)
else
CompletableFuture.completedFuture(statistics)
fun onStatisticsChanged(statistics: Statistics?): Statistics? {
this.statistics = statistics
notifyPropertyChanged(BR.statistics)
return statistics
}
fun delete(): CompletionStage<Void> = manager.delete(this)
}
@@ -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,236 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.model
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.databinding.BaseObservable
import androidx.databinding.Bindable
import com.wireguard.android.Application.Companion.get
import com.wireguard.android.Application.Companion.getAsyncWorker
import com.wireguard.android.Application.Companion.getBackend
import com.wireguard.android.Application.Companion.getSharedPreferences
import com.wireguard.android.Application.Companion.getTunnelManager
import com.wireguard.android.BR
import com.wireguard.android.R
import com.wireguard.android.backend.Statistics
import com.wireguard.android.backend.Tunnel
import com.wireguard.android.configStore.ConfigStore
import com.wireguard.android.databinding.ObservableSortedKeyedArrayList
import com.wireguard.android.util.ExceptionLoggers
import com.wireguard.config.Config
import java9.util.concurrent.CompletableFuture
import java9.util.concurrent.CompletionStage
import java.util.ArrayList
/**
* Maintains and mediates changes to the set of available WireGuard tunnels,
*/
class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
val tunnels = CompletableFuture<ObservableSortedKeyedArrayList<String, ObservableTunnel>>()
private val context: Context = get()
private val delayedLoadRestoreTunnels = ArrayList<CompletableFuture<Void>>()
private val tunnelMap: ObservableSortedKeyedArrayList<String, ObservableTunnel> = ObservableSortedKeyedArrayList(TunnelComparator)
private var haveLoaded = false
private fun addToList(name: String, config: Config?, state: Tunnel.State): ObservableTunnel? {
val tunnel = ObservableTunnel(this, name, config, state)
tunnelMap.add(tunnel)
return tunnel
}
fun create(name: String, config: Config?): CompletionStage<ObservableTunnel> {
if (Tunnel.isNameInvalid(name))
return CompletableFuture.failedFuture(IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)))
if (tunnelMap.containsKey(name))
return CompletableFuture.failedFuture(IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name)))
return getAsyncWorker().supplyAsync { configStore.create(name, config!!) }.thenApply { addToList(name, it, Tunnel.State.DOWN) }
}
fun delete(tunnel: ObservableTunnel): CompletionStage<Void> {
val originalState = tunnel.state
val wasLastUsed = tunnel == lastUsedTunnel
// Make sure nothing touches the tunnel.
if (wasLastUsed)
lastUsedTunnel = null
tunnelMap.remove(tunnel)
return getAsyncWorker().runAsync {
if (originalState == Tunnel.State.UP)
getBackend().setState(tunnel, Tunnel.State.DOWN, null)
try {
configStore.delete(tunnel.name)
} catch (e: Exception) {
if (originalState == Tunnel.State.UP)
getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config)
throw e
}
}.whenComplete { _, e ->
if (e == null)
return@whenComplete
// Failure, put the tunnel back.
tunnelMap.add(tunnel)
if (wasLastUsed)
lastUsedTunnel = tunnel
}
}
@get:Bindable
@SuppressLint("ApplySharedPref")
var lastUsedTunnel: ObservableTunnel? = null
private set(value) {
if (value == field) return
field = value
notifyPropertyChanged(BR.lastUsedTunnel)
if (value != null)
getSharedPreferences().edit().putString(KEY_LAST_USED_TUNNEL, value.name).commit()
else
getSharedPreferences().edit().remove(KEY_LAST_USED_TUNNEL).commit()
}
fun getTunnelConfig(tunnel: ObservableTunnel): CompletionStage<Config> = getAsyncWorker()
.supplyAsync { configStore.load(tunnel.name) }.thenApply(tunnel::onConfigChanged)
fun onCreate() {
getAsyncWorker().supplyAsync { configStore.enumerate() }
.thenAcceptBoth(getAsyncWorker().supplyAsync { getBackend().runningTunnelNames }, this::onTunnelsLoaded)
.whenComplete(ExceptionLoggers.E)
}
private fun onTunnelsLoaded(present: Iterable<String>, running: Collection<String>) {
for (name in present)
addToList(name, null, if (running.contains(name)) Tunnel.State.UP else Tunnel.State.DOWN)
val lastUsedName = getSharedPreferences().getString(KEY_LAST_USED_TUNNEL, null)
if (lastUsedName != null)
lastUsedTunnel = tunnelMap[lastUsedName]
var toComplete: Array<CompletableFuture<Void>>
synchronized(delayedLoadRestoreTunnels) {
haveLoaded = true
toComplete = delayedLoadRestoreTunnels.toTypedArray()
delayedLoadRestoreTunnels.clear()
}
restoreState(true).whenComplete { v: Void?, t: Throwable? ->
for (f in toComplete) {
if (t == null)
f.complete(v)
else
f.completeExceptionally(t)
}
}
tunnels.complete(tunnelMap)
}
fun refreshTunnelStates() {
getAsyncWorker().supplyAsync { getBackend().runningTunnelNames }
.thenAccept { running: Set<String> -> for (tunnel in tunnelMap) tunnel.onStateChanged(if (running.contains(tunnel.name)) Tunnel.State.UP else Tunnel.State.DOWN) }
.whenComplete(ExceptionLoggers.E)
}
fun restoreState(force: Boolean): CompletionStage<Void> {
if (!force && !getSharedPreferences().getBoolean(KEY_RESTORE_ON_BOOT, false))
return CompletableFuture.completedFuture(null)
synchronized(delayedLoadRestoreTunnels) {
if (!haveLoaded) {
val f = CompletableFuture<Void>()
delayedLoadRestoreTunnels.add(f)
return f
}
}
val previouslyRunning = getSharedPreferences().getStringSet(KEY_RUNNING_TUNNELS, null)
?: return CompletableFuture.completedFuture(null)
return CompletableFuture.allOf(*tunnelMap.filter { previouslyRunning.contains(it.name) }.map { setTunnelState(it, Tunnel.State.UP).toCompletableFuture() }.toTypedArray())
}
@SuppressLint("ApplySharedPref")
fun saveState() {
getSharedPreferences().edit().putStringSet(KEY_RUNNING_TUNNELS, tunnelMap.filter { it.state == Tunnel.State.UP }.map { it.name }.toSet()).commit()
}
fun setTunnelConfig(tunnel: ObservableTunnel, config: Config): CompletionStage<Config> = getAsyncWorker().supplyAsync {
getBackend().setState(tunnel, tunnel.state, config)
configStore.save(tunnel.name, config)
}.thenApply { tunnel.onConfigChanged(it) }
fun setTunnelName(tunnel: ObservableTunnel, name: String): CompletionStage<String> {
if (Tunnel.isNameInvalid(name))
return CompletableFuture.failedFuture(IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)))
if (tunnelMap.containsKey(name)) {
return CompletableFuture.failedFuture(IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name)))
}
val originalState = tunnel.state
val wasLastUsed = tunnel == lastUsedTunnel
// Make sure nothing touches the tunnel.
if (wasLastUsed)
lastUsedTunnel = null
tunnelMap.remove(tunnel)
return getAsyncWorker().supplyAsync {
if (originalState == Tunnel.State.UP)
getBackend().setState(tunnel, Tunnel.State.DOWN, null)
configStore.rename(tunnel.name, name)
val newName = tunnel.onNameChanged(name)
if (originalState == Tunnel.State.UP)
getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config)
newName
}.whenComplete { _, e ->
// On failure, we don't know what state the tunnel might be in. Fix that.
if (e != null)
getTunnelState(tunnel)
// Add the tunnel back to the manager, under whatever name it thinks it has.
tunnelMap.add(tunnel)
if (wasLastUsed)
lastUsedTunnel = tunnel
}
}
fun setTunnelState(tunnel: ObservableTunnel, state: Tunnel.State): CompletionStage<Tunnel.State> = tunnel.configAsync
.thenCompose { getAsyncWorker().supplyAsync { getBackend().setState(tunnel, state, it) } }
.whenComplete { newState, e ->
// Ensure onStateChanged is always called (failure or not), and with the correct state.
tunnel.onStateChanged(if (e == null) newState else tunnel.state)
if (e == null && newState == Tunnel.State.UP)
lastUsedTunnel = tunnel
saveState()
}
class IntentReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
val manager = getTunnelManager()
if (intent == null) return
val action = intent.action ?: return
if ("com.wireguard.android.action.REFRESH_TUNNEL_STATES" == action) {
manager.refreshTunnelStates()
return
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || !getSharedPreferences().getBoolean("allow_remote_control_intents", false))
return
val state: Tunnel.State
state = when (action) {
"com.wireguard.android.action.SET_TUNNEL_UP" -> Tunnel.State.UP
"com.wireguard.android.action.SET_TUNNEL_DOWN" -> Tunnel.State.DOWN
else -> return
}
val tunnelName = intent.getStringExtra("tunnel") ?: return
manager.tunnels.thenAccept {
val tunnel = it[tunnelName] ?: return@thenAccept
manager.setTunnelState(tunnel, state)
}
}
}
fun getTunnelState(tunnel: ObservableTunnel): CompletionStage<Tunnel.State> = getAsyncWorker()
.supplyAsync { getBackend().getState(tunnel) }.thenApply(tunnel::onStateChanged)
fun getTunnelStatistics(tunnel: ObservableTunnel): CompletionStage<Statistics> = getAsyncWorker()
.supplyAsync { getBackend().getStatistics(tunnel) }.thenApply(tunnel::onStatisticsChanged)
companion object {
private const val KEY_LAST_USED_TUNNEL = "last_used_tunnel"
private const val KEY_RESTORE_ON_BOOT = "restore_on_boot"
private const val KEY_RUNNING_TUNNELS = "enabled_configs"
}
}
@@ -35,7 +35,7 @@ class KernelModuleDisablerPreference(context: Context, attrs: AttributeSet?) : P
}
Application.getAsyncWorker().runAsync {
Application.getTunnelManager().tunnels.thenApply { observableTunnels ->
val downings = observableTunnels.values().map { it.setState(Tunnel.State.DOWN).toCompletableFuture() }.toTypedArray()
val downings = observableTunnels.map { it.setStateAsync(Tunnel.State.DOWN).toCompletableFuture() }.toTypedArray()
CompletableFuture.allOf(*downings).thenRun {
val restartIntent = Intent(context, SettingsActivity::class.java)
restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
@@ -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
}
}
@@ -34,7 +34,7 @@ class ModuleDownloaderPreference(context: Context, attrs: AttributeSet?) : Prefe
when {
throwable != null -> {
setState(State.FAILURE)
Toast.makeText(context, ErrorMessages.get(throwable), Toast.LENGTH_LONG).show()
Toast.makeText(context, ErrorMessages[throwable], Toast.LENGTH_LONG).show()
}
result == OsConstants.ENOENT -> setState(State.NOTFOUND)
result == OsConstants.EXIT_SUCCESS -> {
@@ -43,7 +43,7 @@ class VersionPreference(context: Context, attrs: AttributeSet?) : Preference(con
}
init {
Application.getBackendAsync().thenAccept { backend: Backend ->
Application.getBackendAsync().thenAccept { backend ->
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)
@@ -14,6 +14,7 @@ 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.BiometricAuthenticator
import com.wireguard.android.util.DownloadsFileSaver
import com.wireguard.android.util.ErrorMessages
import com.wireguard.android.util.FragmentUtils
@@ -63,7 +64,7 @@ class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference
private fun exportZipComplete(filePath: String?, throwable: Throwable?) {
if (throwable != null) {
val error = ErrorMessages.get(throwable)
val error = ErrorMessages[throwable]
val message = context.getString(R.string.zip_export_error, error)
Log.e(TAG, message, throwable)
Snackbar.make(
@@ -81,16 +82,31 @@ 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()
val prefActivity = FragmentUtils.getPrefActivity(this)
val fragment = prefActivity.supportFragmentManager.fragments.first()
BiometricAuthenticator.authenticate(R.string.biometric_prompt_zip_exporter_title, fragment) {
when (it) {
// When we have successful authentication, or when there is no biometric hardware available.
is BiometricAuthenticator.Result.Success, is BiometricAuthenticator.Result.HardwareUnavailableOrDisabled -> {
prefActivity.ensurePermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, grantResults ->
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
isEnabled = false
exportZip()
}
}
}
is BiometricAuthenticator.Result.Failure -> {
Snackbar.make(
prefActivity.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"
}
}
@@ -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,43 @@
/*
* Copyright © 2017-2020 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util
import android.os.Handler
import java9.util.concurrent.CompletableFuture
import java9.util.concurrent.CompletionStage
import java.util.concurrent.Executor
/**
* Helper class for running asynchronous tasks and ensuring they are completed on the main thread.
*/
class AsyncWorker(private val executor: Executor, private val handler: Handler) {
fun runAsync(run: () -> Unit): CompletionStage<Void> {
val future = CompletableFuture<Void>()
executor.execute {
try {
run()
handler.post { future.complete(null) }
} catch (t: Throwable) {
handler.post { future.completeExceptionally(t) }
}
}
return future
}
fun <T> supplyAsync(get: () -> T?): CompletionStage<T> {
val future = CompletableFuture<T>()
executor.execute {
try {
val result = get()
handler.post { future.complete(result) }
} catch (t: Throwable) {
handler.post { future.completeExceptionally(t) }
}
}
return future
}
}
@@ -0,0 +1,91 @@
/*
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util
import android.annotation.SuppressLint
import android.app.KeyguardManager
import android.content.Context
import android.os.Build
import android.os.Handler
import android.util.Log
import androidx.annotation.StringRes
import androidx.biometric.BiometricConstants
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.getSystemService
import androidx.fragment.app.Fragment
import com.wireguard.android.R
object BiometricAuthenticator {
private const val TAG = "WireGuard/BiometricAuthenticator"
private val handler = Handler()
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()
}
@SuppressLint("PrivateApi")
private fun isPinEnabled(context: Context): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
return context.getSystemService<KeyguardManager>()!!.isDeviceSecure
return try {
val lockUtilsClass = Class.forName("com.android.internal.widget.LockPatternUtils")
val lockUtils = lockUtilsClass.getConstructor(Context::class.java).newInstance(context)
val method = lockUtilsClass.getMethod("isLockScreenDisabled")
!(method.invoke(lockUtils) as Boolean)
} catch (e: Exception) {
false
}
}
fun authenticate(
@StringRes dialogTitleRes: Int,
fragment: Fragment,
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) {
BiometricConstants.ERROR_CANCELED, BiometricConstants.ERROR_USER_CANCELED,
BiometricConstants.ERROR_NEGATIVE_BUTTON -> {
Result.Cancelled
}
BiometricConstants.ERROR_HW_NOT_PRESENT, BiometricConstants.ERROR_HW_UNAVAILABLE,
BiometricConstants.ERROR_NO_BIOMETRICS, BiometricConstants.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.post(it) }, authCallback)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(fragment.getString(dialogTitleRes))
.setDeviceCredentialAllowed(true)
.build()
if (BiometricManager.from(fragment.requireContext()).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS || isPinEnabled(fragment.requireContext())) {
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,33 @@
/*
* 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
/**
* 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, "${data.second} copied to clipboard", 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,72 @@
/*
* Copyright © 2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util
import android.content.ContentValues
import android.content.Context
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 java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
object DownloadsFileSaver {
@Throws(Exception::class)
fun save(context: Context, name: String, mimeType: String?, overwriteExisting: Boolean) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentResolver = context.contentResolver
if (overwriteExisting)
contentResolver.delete(MediaStore.Downloads.EXTERNAL_CONTENT_URI, String.format("%s = ?", MediaColumns.DISPLAY_NAME), arrayOf(name))
val contentValues = ContentValues()
contentValues.put(MediaColumns.DISPLAY_NAME, name)
contentValues.put(MediaColumns.MIME_TYPE, mimeType)
val contentUri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
?: throw IOException(context.getString(R.string.create_downloads_file_error))
val contentStream = contentResolver.openOutputStream(contentUri)
?: throw IOException(context.getString(R.string.create_downloads_file_error))
@Suppress("DEPRECATION") var cursor = contentResolver.query(contentUri, arrayOf(MediaColumns.DATA), null, null, null)
var path: String? = null
if (cursor != null) {
try {
if (cursor.moveToFirst())
path = cursor.getString(0)
} finally {
cursor.close()
}
}
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 {
@Suppress("DEPRECATION") val path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(path, name)
if (!path.isDirectory && !path.mkdirs())
throw IOException(context.getString(R.string.create_output_dir_error))
DownloadsFile(context, FileOutputStream(file), file.absolutePath, null)
}
class DownloadsFile(private val context: Context, val outputStream: OutputStream, val fileName: String, private val uri: Uri?) {
fun delete() {
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.Companion.get
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 = 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");
}
}
@@ -0,0 +1,27 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util
import android.util.Log
import java9.util.function.BiConsumer
/**
* Helpers for logging exceptions from asynchronous tasks. These can be passed to
* `CompletionStage.whenComplete()` at the end of an asynchronous future chain.
*/
enum class ExceptionLoggers(private val priority: Int) : BiConsumer<Any?, Throwable?> {
D(Log.DEBUG), E(Log.ERROR);
override fun accept(result: Any?, throwable: Throwable?) {
if (throwable != null)
Log.println(Log.ERROR, TAG, Log.getStackTraceString(throwable))
else if (priority <= Log.DEBUG)
Log.println(priority, TAG, "Future completed successfully")
}
companion object {
private const val TAG = "WireGuard/ExceptionLoggers"
}
}
@@ -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;
}
}
@@ -0,0 +1,21 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util
import android.view.ContextThemeWrapper
import androidx.preference.Preference
import com.wireguard.android.activity.SettingsActivity
object FragmentUtils {
fun getPrefActivity(preference: Preference): SettingsActivity {
val context = preference.context
if (context is ContextThemeWrapper) {
if (context is SettingsActivity) {
return context
}
}
throw IllegalStateException("Failed to resolve SettingsActivity")
}
}
@@ -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);
}
}
@@ -1,21 +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.KeyedList;
import com.wireguard.util.NonNullForAll;
import androidx.databinding.ObservableList;
/**
* A list that is both keyed and observable.
*/
@NonNullForAll
public interface ObservableKeyedList<K, E extends Keyed<? extends K>>
extends KeyedList<K, E>, ObservableList<E> {
}
@@ -1,200 +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 com.wireguard.util.SortedKeyedList;
import java.util.AbstractList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.Spliterator;
import androidx.annotation.Nullable;
/**
* 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.
*/
@NonNullForAll
public class ObservableSortedKeyedArrayList<K, E extends Keyed<? extends K>>
extends ObservableKeyedArrayList<K, E> implements ObservableSortedKeyedList<K, E> {
@Nullable private final Comparator<? super K> comparator;
private final transient KeyList<K, E> keyList = new KeyList<>(this);
@SuppressWarnings("WeakerAccess")
public ObservableSortedKeyedArrayList() {
comparator = null;
}
public ObservableSortedKeyedArrayList(final Comparator<? super K> comparator) {
this.comparator = comparator;
}
public ObservableSortedKeyedArrayList(final Collection<? extends E> c) {
this();
addAll(c);
}
public ObservableSortedKeyedArrayList(final SortedKeyedList<K, E> other) {
this(other.comparator());
addAll(other);
}
@Override
public boolean add(final E e) {
final int insertionPoint = getInsertionPoint(e);
if (insertionPoint < 0) {
// Skipping insertion is non-destructive if the new and existing objects are the same.
if (e == get(-insertionPoint - 1))
return false;
throw new IllegalArgumentException("Element with same key already exists in list");
}
super.add(insertionPoint, e);
return true;
}
@Override
public void add(final int index, final E e) {
final int insertionPoint = getInsertionPoint(e);
if (insertionPoint < 0)
throw new IllegalArgumentException("Element with same key already exists in list");
if (insertionPoint != index)
throw new IndexOutOfBoundsException("Wrong index given for element");
super.add(index, e);
}
@Override
public boolean addAll(final Collection<? extends E> c) {
boolean didChange = false;
for (final E e : c)
if (add(e))
didChange = true;
return didChange;
}
@Override
public boolean addAll(int index, final Collection<? extends E> c) {
for (final E e : c)
add(index++, e);
return true;
}
@Nullable
@Override
public Comparator<? super K> comparator() {
return comparator;
}
@Override
public K firstKey() {
if (isEmpty())
// The parameter in the exception is only to shut
// lint up, we never care for the exception message.
throw new NoSuchElementException("Empty set");
return get(0).getKey();
}
private int getInsertionPoint(final E e) {
if (comparator != null) {
return -Collections.binarySearch(keyList, e.getKey(), comparator) - 1;
} else {
@SuppressWarnings("unchecked") final List<Comparable<? super K>> list =
(List<Comparable<? super K>>) keyList;
return -Collections.binarySearch(list, e.getKey()) - 1;
}
}
@Override
public int indexOfKey(final K key) {
final int index;
if (comparator != null) {
index = Collections.binarySearch(keyList, key, comparator);
} else {
@SuppressWarnings("unchecked") final List<Comparable<? super K>> list =
(List<Comparable<? super K>>) keyList;
index = Collections.binarySearch(list, key);
}
return index >= 0 ? index : -1;
}
@Override
public Set<K> keySet() {
return keyList;
}
@Override
public int lastIndexOfKey(final K key) {
// There can never be more than one element with the same key in the list.
return indexOfKey(key);
}
@Override
public K lastKey() {
if (isEmpty())
// The parameter in the exception is only to shut
// lint up, we never care for the exception message.
throw new NoSuchElementException("Empty set");
return get(size() - 1).getKey();
}
@Override
public E set(final int index, final E e) {
final int order;
if (comparator != null) {
order = comparator.compare(e.getKey(), get(index).getKey());
} else {
@SuppressWarnings("unchecked") final Comparable<? super K> key =
(Comparable<? super K>) e.getKey();
order = key.compareTo(get(index).getKey());
}
if (order != 0) {
// Allow replacement if the new key would be inserted adjacent to the replaced element.
final int insertionPoint = getInsertionPoint(e);
if (insertionPoint < index || insertionPoint > index + 1)
throw new IndexOutOfBoundsException("Wrong index given for element");
}
return super.set(index, e);
}
@Override
public Collection<E> values() {
return this;
}
private static final class KeyList<K, E extends Keyed<? extends K>>
extends AbstractList<K> implements Set<K> {
private final ObservableSortedKeyedArrayList<K, E> list;
private KeyList(final ObservableSortedKeyedArrayList<K, E> list) {
this.list = list;
}
@Override
public K get(final int index) {
return list.get(index).getKey();
}
@Override
public int size() {
return list.size();
}
@Override
@SuppressWarnings("EmptyMethod")
public Spliterator<K> spliterator() {
return super.spliterator();
}
}
}
@@ -1,19 +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 com.wireguard.util.SortedKeyedList;
/**
* A list that is both sorted/keyed and observable.
*/
@NonNullForAll
public interface ObservableSortedKeyedList<K, E extends Keyed<? extends K>>
extends ObservableKeyedList<K, E>, SortedKeyedList<K, E> {
}
@@ -6,7 +6,12 @@ package com.wireguard.android.widget
import android.view.View
import android.view.ViewGroup
import androidx.core.view.*
import androidx.core.view.marginBottom
import androidx.core.view.marginLeft
import androidx.core.view.marginRight
import androidx.core.view.marginTop
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
/**
@@ -18,7 +23,7 @@ object EdgeToEdge {
@JvmStatic
fun setUpRoot(root: ViewGroup) {
root.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
}
@JvmStatic
@@ -33,9 +38,9 @@ object EdgeToEdge {
scrollingContent.setOnApplyWindowInsetsListener { _, windowInsets ->
scrollingContent.updatePadding(
left = originalPaddingLeft + windowInsets.systemWindowInsetLeft,
right = originalPaddingRight + windowInsets.systemWindowInsetRight,
bottom = originalPaddingBottom + fabPaddingBottom + windowInsets.systemWindowInsetBottom
left = originalPaddingLeft + windowInsets.systemWindowInsetLeft,
right = originalPaddingRight + windowInsets.systemWindowInsetRight,
bottom = originalPaddingBottom + fabPaddingBottom + windowInsets.systemWindowInsetBottom
)
scrollingContent.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = originalMarginTop + windowInsets.systemWindowInsetTop
@@ -0,0 +1,30 @@
/*
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget
import android.content.Context
import android.text.Editable
import android.text.SpannableStringBuilder
import android.util.AttributeSet
import com.google.android.material.R
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
class MonkeyedTextInputEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = R.attr.editTextStyle) : TextInputEditText(context, attrs, defStyleAttr) {
@Override
override fun getText(): Editable? {
val text = super.getText()
if (!text.isNullOrEmpty())
return text
/* We want this expression in TextInputLayout.java to be true if there's a hint set:
* final boolean hasText = editText != null && !TextUtils.isEmpty(editText.getText());
* But for everyone else it should return the real value, so we check the caller.
*/
if (!hint.isNullOrEmpty() && Thread.currentThread().stackTrace[3].className == TextInputLayout::class.qualifiedName)
return SpannableStringBuilder(hint)
return text
}
}
@@ -6,13 +6,11 @@
package com.wireguard.android.widget
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.content.res.ColorStateList
import android.graphics.*
import android.graphics.drawable.Drawable
import android.os.Build
import android.util.FloatProperty
import android.util.Property
import androidx.annotation.ColorInt
import androidx.annotation.IntRange
import androidx.annotation.RequiresApi
@@ -23,6 +21,7 @@ class SlashDrawable(private val mDrawable: Drawable) : Drawable() {
private val mPath = Path()
private val mSlashRect = RectF()
private var mAnimationEnabled = true
// Animate this value on change
private var mCurrentSlashLength = 0f
private var mRotation = 0f
@@ -59,7 +58,9 @@ class SlashDrawable(private val mDrawable: Drawable) : Drawable() {
mPath.addRoundRect(mSlashRect, 1f * width, 1f * height, Path.Direction.CW)
m.setRotate(mRotation + DEFAULT_ROTATION, width / 2f, height / 2f)
mPath.transform(m)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) canvas.clipPath(mPath, Region.Op.DIFFERENCE) else canvas.clipOutPath(mPath)
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
canvas.clipPath(mPath, Region.Op.DIFFERENCE) else canvas.clipOutPath(mPath)
mDrawable.draw(canvas)
canvas.restore()
}
@@ -1,15 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.util;
/**
* Interface for objects that have a identifying key of the given type.
*/
@NonNullForAll
public interface Keyed<K> {
K getKey();
}
@@ -1,33 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.util;
import java.util.Collection;
import java.util.List;
import androidx.annotation.Nullable;
/**
* A list containing elements that can be looked up by key. A {@code KeyedList} cannot contain
* {@code null} elements.
*/
@NonNullForAll
public interface KeyedList<K, E extends Keyed<? extends K>> extends List<E> {
boolean containsAllKeys(Collection<K> keys);
boolean containsKey(K key);
@Nullable
E get(K key);
@Nullable
E getLast(K key);
int indexOfKey(K key);
int lastIndexOfKey(K key);
}
@@ -1,32 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.util;
import java.util.Collection;
import java.util.Comparator;
import java.util.Set;
import androidx.annotation.Nullable;
/**
* A keyed list where all elements are sorted by the comparator returned by {@code comparator()}
* applied to their keys.
*/
@NonNullForAll
public interface SortedKeyedList<K, E extends Keyed<? extends K>> extends KeyedList<K, E> {
Comparator<? super K> comparator();
@Nullable
K firstKey();
Set<K> keySet();
@Nullable
K lastKey();
Collection<E> values();
}
@@ -1,8 +1,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="?attr/colorOnSecondary"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
@@ -1,8 +1,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorOnPrimary"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
+2 -2
View File
@@ -1,8 +1,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorOnPrimary"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
@@ -1,8 +1,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorOnSecondary"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,4L12,1L8,5l4,4L12,6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z" />
</vector>
@@ -1,8 +1,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorOnSecondary"
android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6L6,2zM13,9L13,3.5L18.5,9L13,9z" />
+2 -2
View File
@@ -2,8 +2,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorOnPrimary"
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z" />
@@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorOnSecondary"
android:pathData="M4,4H10V10H4V4M20,4V10H14V4H20M14,15H16V13H14V11H16V13H18V11H20V13H18V15H20V18H18V20H16V18H13V20H11V16H14V15M16,15V18H18V15H16M4,20V14H10V20H4M6,6V8H8V6H6M16,6V8H18V6H16M6,16V18H8V16H6M4,11H6V13H4V11M9,11H13V15H11V13H9V11M11,6H13V10H11V6M2,2V6H0V2A2,2 0 0,1 2,0H6V2H2M22,0A2,2 0 0,1 24,2V6H22V2H18V0H22M2,18V22H6V24H2A2,2 0 0,1 0,22V18H2M22,22V18H24V22A2,2 0 0,1 22,24H18V22H22Z" />
</vector>
</vector>
@@ -2,8 +2,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorOnPrimary"
android:pathData="M3 5L5 5 5 3C3.9 3 3 3.9 3 5Zm0 8l2 0 0 -2 -2 0 0 2zm4 8l2 0 0 -2 -2 0 0 2zM3 9L5 9 5 7 3 7 3 9Zm10 -6l-2 0 0 2 2 0 0 -2zm6 0l0 2 2 0C21 3.9 20.1 3 19 3ZM5 21L5 19 3 19c0 1.1 0.9 2 2 2zm-2 -4l2 0 0 -2 -2 0 0 2zM9 3L7 3 7 5 9 5 9 3Zm2 18l2 0 0 -2 -2 0 0 2zm8 -8l2 0 0 -2 -2 0 0 2zm0 8c1.1 0 2 -0.9 2 -2l-2 0 0 2zm0 -12l2 0 0 -2 -2 0 0 2zm0 8l2 0 0 -2 -2 0 0 2zm-4 4l2 0 0 -2 -2 0 0 2zm0 -16l2 0 0 -2 -2 0 0 2zM7 17L17 17 17 7 7 7 7 17Zm2 -8l6 0 0 6 -6 0 0 -6z" />
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" />
</vector>
@@ -2,8 +2,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="2160"
android:viewportWidth="2160">
android:viewportWidth="2160"
android:viewportHeight="2160">
<group
android:scaleX="1"
android:scaleY="-1"
@@ -32,4 +32,4 @@
</group>
</group>
</group>
</vector>
</vector>
+2 -2
View File
@@ -1,8 +1,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?android:attr/colorForeground"
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
+8 -8
View File
@@ -1,24 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="400dp"
android:height="400dp"
android:viewportHeight="400.0"
android:viewportWidth="400.0">
android:viewportWidth="400.0"
android:viewportHeight="400.0">
<path
android:fillAlpha="1"
android:fillColor="#ffffff"
android:pathData="m197.7,0c-6.2,0.1 -12.5,0.5 -19,1.1 12.4,2.9 23.5,5.5 34.7,8.1 -0.1,0.7 -0.3,1.4 -0.4,2 -14.9,2 -29.1,-3.5 -43.7,-5.5 5.3,3.1 10.7,6 16.2,8.5 5.6,2.5 11.4,4.7 17.2,7 -7.4,6.3 -14.8,7.7 -24,5.6 -5.1,-1.2 -10.4,-1.8 -15.6,-1.5 -5.3,0.3 -10.7,1.6 -15.6,4.8 5.2,2.6 10,4.8 14.5,7.5 1.9,1.1 4,2.9 4.5,4.9 1.2,4.6 1.6,9.4 2.3,14.1 -8.5,1 -23.5,9.6 -26.5,15.3 13.1,2.5 27.3,-0.5 39.8,7.9 -4.1,3.1 -13.7,7 -17.2,9.6 4.3,1.1 14.4,0.6 18.3,0.3 3.3,-0.2 4.8,-0.3 6.2,0.8l38.5,30.1c4,3.3 20.4,18.7 24.7,28.5 3.6,8.3 4.1,15.3 4.1,17.1 -0,4.6 -0.6,11.8 -3.7,19.9 -1.3,3.4 -5.2,10.9 -13.3,19.6 -12.5,13.5 -28.6,20.9 -46.2,24.5 -40.9,8.4 -74.9,52.1 -65.3,100.2 11.2,56.2 73.2,86.6 123.9,59.9 32.8,-17.3 50.1,-51 45.5,-87.7 -2.8,-22.2 -12.8,-40.2 -29.7,-54.8 -2.2,-1.9 -3.6,-1.9 -6.2,-0.3 -8.7,5.6 -17.7,10.9 -26.8,15.9 -5.2,2.9 -10.9,5 -17.4,7.9 2.2,0.6 3.3,0.9 4.4,1.1 24.4,6.5 37.5,28 31.7,52 -5.1,21.4 -26.8,35 -47.9,31.4 -17.5,-3 -32.8,-17.6 -35.4,-35 -2.8,-19 6.7,-37.3 23.5,-44.9 9.3,-4.2 18.9,-7.9 28.3,-12.2 10.6,-4.9 22.1,-8.7 31.3,-15.5 23,-16.9 37.1,-40.1 42.7,-68.1 3.3,-16.8 3.1,-33.5 -4.6,-49.4 -5.9,-12.2 -15.6,-21.1 -26,-29.2 -10.7,-8.3 -22,-15.9 -32.7,-24.3 -2.9,-2.3 -4.8,-6.2 -6.2,-9.7 -0.6,-1.5 1.3,-5.6 2.5,-5.8 6.5,-1.2 13.2,-1.8 19.8,-2 7.7,-0.3 15.4,-0 23,0.1 1.7,0 3.9,-0.2 4.9,0.7 4,3.9 7.1,1.4 9.9,-1.2 2.3,-2.2 4,-5 5.8,-7.5 -1.1,-0.2 -3.4,-0.7 -5.7,-0.8 -7.7,-0.2 -15.4,-0.1 -23.2,-0.3 -1.4,-0.1 -2.7,-1.5 -4,-2.2 1.4,-0.6 2.8,-1.6 4.3,-1.6 13.3,-0.1 26.6,-0.1 40,-0.1 0,-6.9 -9.2,-16.4 -17.5,-19 -0.1,0.9 -0.1,1.8 -0.2,2.7 -8.2,0.2 -16.2,0 -23.5,-3.8 -1.9,-1 -3.2,-3.3 -4.7,-5 -2,-2.1 -3.6,-4.9 -6,-6.3 -4.9,-2.9 -10.3,-4.9 -15.4,-7.4C224.3,1.7 211.3,-0.1 197.7,0ZM249.6,29.4c0.6,-0 1.2,0.2 1.9,0.8 1,0.8 2,1.7 3.3,2.8 -1.5,0.8 -2.8,1.5 -4.1,2.2 -1.8,0.9 -3.1,0.3 -4.2,-1.1 -0.9,-1.2 -1,-2.3 0.3,-3.3 0.9,-0.7 1.8,-1.3 2.9,-1.3z"
android:strokeColor="#00000000"
android:strokeWidth="1.33333325" />
android:strokeWidth="1.33333325"
android:strokeColor="#00000000" />
<path
android:fillAlpha="1"
android:fillColor="#ffffff"
android:pathData="m97.9,307.6c-7.8,2 -15.4,4.9 -23.4,7.5 3.9,-26.3 34.7,-50.6 60.8,-47.8 -8.1,10.9 -11.8,23.3 -12.7,35.6 -8.7,1.6 -16.8,2.7 -24.8,4.7"
android:strokeColor="#00000000"
android:strokeWidth="1.33333325" />
android:strokeWidth="1.33333325"
android:strokeColor="#00000000" />
<path
android:fillAlpha="1"
android:fillColor="#ffffff"
android:pathData="m134.3,124c41.3,-25.3 94,-9.8 113.8,28.2 3.7,7.2 4.2,18.3 1.8,25.8 -8.2,26.1 -27.5,40.7 -54.1,46.9 7.8,-6.7 14.1,-14.3 16,-24.8 2,-10.6 -0.1,-20.1 -6.2,-28.8 -9.3,-13.2 -27.3,-18.6 -42.4,-12.9 -16.3,6.2 -25.3,21.1 -23.7,39.4 1.5,17 14.4,28.1 38.6,32.2 -3.6,1.9 -6.4,3.3 -9.1,4.8 -11.1,6.2 -20.7,14.3 -28.2,24.6 -2.5,3.3 -4.1,3.6 -7.9,1.3 -48.6,-29.7 -51.7,-104.3 1.4,-136.8"
android:strokeColor="#00000000"
android:strokeWidth="1.33333325" />
android:strokeWidth="1.33333325"
android:strokeColor="#00000000" />
</vector>
@@ -3,7 +3,9 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<item>
<selector>
<item app:state_multiselected="true" android:state_activated="true">
<item
android:state_activated="true"
app:state_multiselected="true">
<color android:color="?attr/colorMultiselectActiveBackground" />
</item>
</selector>
@@ -4,6 +4,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.MainActivity">
<LinearLayout
android:id="@+id/master_detail_wrapper"
android:layout_width="match_parent"
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root"
android:layout_width="match_parent"
@@ -9,65 +8,65 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/create_from_file"
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:layout_marginLeft="@dimen/normal_margin"
android:layout_marginRight="@dimen/normal_margin"
android:layout_marginStart="@dimen/normal_margin"
android:layout_marginLeft="@dimen/normal_margin"
android:layout_marginEnd="@dimen/normal_margin"
android:layout_marginRight="@dimen/normal_margin"
android:text="@string/create_from_file"
android:textAlignment="viewStart"
android:textColor="?attr/colorOnSurface"
app:icon="@drawable/ic_action_open_white"
app:iconPadding="@dimen/bottom_sheet_icon_padding"
app:iconTint="?attr/colorSecondary"
app:layout_constraintTop_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/create_from_qrcode"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:rippleColor="?attr/colorSecondary"
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"/>
app:layout_constraintTop_toBottomOf="parent"
app:rippleColor="?attr/colorSecondary" />
<com.google.android.material.button.MaterialButton
android:id="@+id/create_from_qrcode"
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:layout_marginLeft="@dimen/normal_margin"
android:layout_marginRight="@dimen/normal_margin"
android:layout_marginStart="@dimen/normal_margin"
android:layout_marginLeft="@dimen/normal_margin"
android:layout_marginEnd="@dimen/normal_margin"
android:layout_marginRight="@dimen/normal_margin"
android:text="@string/create_from_qr_code"
android:textAlignment="viewStart"
android:textColor="?attr/colorOnSurface"
app:icon="@drawable/ic_action_scan_qr_code_white"
app:iconPadding="@dimen/bottom_sheet_icon_padding"
app:iconTint="?attr/colorSecondary"
app:layout_constraintTop_toBottomOf="@+id/create_from_file"
app:layout_constraintBottom_toBottomOf="@+id/create_empty"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:rippleColor="?attr/colorSecondary"
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"/>
app:layout_constraintTop_toBottomOf="@+id/create_from_file"
app:rippleColor="?attr/colorSecondary" />
<com.google.android.material.button.MaterialButton
android:id="@+id/create_empty"
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:layout_marginLeft="@dimen/normal_margin"
android:layout_marginRight="@dimen/normal_margin"
android:layout_marginStart="@dimen/normal_margin"
android:layout_marginLeft="@dimen/normal_margin"
android:layout_marginEnd="@dimen/normal_margin"
android:layout_marginRight="@dimen/normal_margin"
android:text="@string/create_empty"
android:textAlignment="viewStart"
android:textColor="?attr/colorOnSurface"
app:icon="@drawable/ic_action_edit"
app:iconPadding="@dimen/bottom_sheet_icon_padding"
app:iconTint="?attr/colorSecondary"
app:layout_constraintTop_toBottomOf="@+id/create_from_qrcode"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:rippleColor="?attr/colorSecondary"
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"/>
app:layout_constraintTop_toBottomOf="@+id/create_from_qrcode"
app:rippleColor="?attr/colorSecondary" />
</androidx.constraintlayout.widget.ConstraintLayout>

Some files were not shown because too many files have changed in this diff Show More