Compare commits

...

332 Commits

Author SHA1 Message Date
Jason A. Donenfeld 3785752364 version: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-20 12:50:11 +02:00
Jason A. Donenfeld 398d8a1e41 AddTunnelsSheet: disable qrcode scanning if no camera
Part of the enhancements for Android TV.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-20 12:47:13 +02:00
Harsh Shandilya dfd8ca6f79 ui: add tooling label for exclusions button
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-09-20 13:33:48 +05:30
Harsh Shandilya 7cff4367d7 ui: add navigation hints for D-Pad and IME
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-09-20 13:33:48 +05:30
Jason A. Donenfeld 9eaed5e745 version: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-18 20:29:23 +02:00
Harsh Shandilya 68350bb4df ui: add xhdpi banner resource
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-09-18 20:29:23 +02:00
Jason A. Donenfeld 12be972fcd SettingsActivity: account for module present but no root
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-18 20:29:23 +02:00
Jason A. Donenfeld d200437813 ui: move to Jetpack DataStore instead of SharedPrefs
Hopefully PreferencesPreferenceDataStore gets to go away sometime down
the line.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-18 20:29:23 +02:00
Jason A. Donenfeld 3ffe7a5e68 ui: reformat code
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-18 14:55:27 +02:00
Jason A. Donenfeld 08ff9f5ece gradle: downgrade androidx.{fragment,activity} to alpha07
The alpha08 version introduced regressions that we can't deal with at
the moment.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-17 23:47:14 +02:00
Harsh Shandilya 4bee579e48 ui: retire EdgeToEdge
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-09-17 23:09:05 +05:30
Harsh Shandilya a906c478c9 ui: replace deprecated onActivityCreated with onViewCreated
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-09-17 20:00:37 +05:30
Harsh Shandilya 306d0648c6 ui: refactor AddTunnelsSheet's selection communication
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-09-17 19:36:44 +05:30
Harsh Shandilya e99ccf9013 ui: refactor AppListDialogFragment's selection communication
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-09-17 14:51:50 +02:00
Jason A. Donenfeld 59935a12b9 activityx: use contracts more and refine
This is the beginning; there are still many of the old API's callsites
to convert.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-17 14:50:37 +02:00
Jason A. Donenfeld a9ec828506 DownloadsFileSaver: encapsulate permission checks
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-17 14:50:37 +02:00
Jason A. Donenfeld eebeece856 LogViewerActivity: simplify scoping
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-16 18:01:06 +02:00
Jason A. Donenfeld 746ab00794 ZipExporterPreference: don't ask for storage permissions on newer android
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-16 18:01:06 +02:00
Jonathan Davies b877593d55 libwg-go: use PeekLookAtSocketFd6(), not PeekLookAtSocketFd4()
Signed-off-by: Jonathan Davies <jpds@protonmail.com>
Fixes: 3d088411 ("libwg-go: use conn.Bind for socketfd peek")
Cc: David Crawshaw <crawshaw@tailscale.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-16 18:01:06 +02:00
Harsh Shandilya dcd596907a ui: resolve getColor deprecation in LogViewerActivity
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-09-16 18:01:06 +02:00
Jason A. Donenfeld 44c2afbfba LogViewerActivity: destroy process when coroutine scope is cancelled
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-16 18:01:06 +02:00
Harsh Shandilya bd1679b7e0 ui: await activity creation to change selected tunnel
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-09-16 18:01:06 +02:00
Harsh Shandilya ff7d7e0edd tunnel: document more public API from backend package
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-09-16 18:01:06 +02:00
Harsh Shandilya 2f088938c6 ui: replace GlobalScope with a hand-rolled CoroutineScope
GlobalScope has numerous problems[1] that make it unfit for
use in most applications and making it behave correctly requires
an excessive amount of verbosity that's alleviated simply by using
any other scope. Since we run multiple operations in the context
of the application's lifecycle, introduce a new scope that is created
when our application is, and cancelled upon its termination.

While at it, make the scope default to Dispatchers.IO to reduce pressure
on the UI event loop. Tasks requiring access to the UI thread appropriately
switch context making the change completely safe.

1: https://medium.com/@elizarov/the-reason-to-avoid-globalscope-835337445abc

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-09-16 18:01:06 +02:00
Jason A. Donenfeld 53adb0e9a6 Ed25519: use implementation from Tink
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-16 18:01:06 +02:00
Harsh Shandilya 6789c11a7b ConfigNamingDialogFragment: fix focus request for config naming dialog
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-09-16 18:01:06 +02:00
Jason A. Donenfeld c56065fcfe TunnelEditorFragment: move backwards using fragment manager instead of hack
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-16 18:01:06 +02:00
Jason A. Donenfeld 52a2ae36f6 TunnelEditorFragment: avoid extra trip through event loop
onSelectedTunnelChanged is already queueing us to Dispatchers.Main
(rather than Dispatchers.Main.immediate, which would crash, but why?),
so avoid the extra trip through the event loop by toggling the selected
tunnel right away.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-16 10:51:56 +02:00
Jason A. Donenfeld abcb51d2a6 Extensions: use more idiomatic kotlin
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-16 10:44:50 +02:00
Jason A. Donenfeld 8b9a40b3d7 global: lint codebase with recent changes
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-16 10:37:21 +02:00
Harsh Shandilya 4b36df504c ui: don't use low-level logger API
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-09-16 13:04:42 +05:30
Harsh Shandilya 35fe5bd5f0 ui: update manifest for API 30 changes
- Mark WRITE_EXTERNAL_STORAGE as unused above API 29 since we defer to Storage Access Framework on P and above

- Adds a <queries> tag to allow listing apps for exclusion/inclusion dialog

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-09-16 10:20:10 +05:30
Jason A. Donenfeld 79ae85c728 coroutines: lifecycleScope is by default on Main.immediate
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-16 00:00:31 +02:00
Jason A. Donenfeld 49ac61304e coroutines: use lifecycleScope where appropriate
There's still a bit of GlobalScope lingering around, which might be
removable.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-15 23:46:11 +02:00
Jason A. Donenfeld d79cdb0d41 MonkeyedTextInputEditText: au revoir
Remember to go back to using com.google.android.material when
1.3.0-alpha03 comes out.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-15 18:53:30 +02:00
Jason A. Donenfeld a3726b07bf wireguard-tools: bump to fix invalid free
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-15 16:21:09 +02:00
Jason A. Donenfeld 80c35a2053 TunnelListFragment: set selection on Main, not Main.immediate
Otherwise, we crash when saving the config.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-15 16:07:16 +02:00
Jason A. Donenfeld 601b58b670 libwg-go: update to go 1.15.2
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-15 13:40:13 +02:00
Jason A. Donenfeld 9cf049775f MonkeyedTextInputEditText: add note about sunset plan
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-15 12:44:55 +02:00
Jason A. Donenfeld 92122e60c6 idea: import new import sorting rules
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-15 12:30:15 +02:00
Jason A. Donenfeld f20d0f0659 gradle: desugar retrofuture and remove old deps
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-15 12:30:15 +02:00
Jason A. Donenfeld 9346a63753 gradle: do not use retrofuture in ui
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-15 12:30:15 +02:00
Jason A. Donenfeld bab70ab51e coroutines: convert the rest
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-15 12:30:15 +02:00
Jason A. Donenfeld 2fc0bb1a03 coroutines: convert low-hanging fruits
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-14 14:40:10 +02:00
Harsh Shandilya dd0ff8fe60 ui: remove hacky manual check for keyguard
Setting the correct value for the allowedAuthenticators field lets the library correctly detect this by itself as verified on an API 21 emulator

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-09-13 18:34:17 +05:30
Harsh Shandilya 45a179580d ui: update BiometricAuthenticator for API changes
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-09-13 18:33:17 +05:30
Harsh Shandilya 0bcee7f9cc ui: fix memory leak from statically held Handler instance
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-09-13 17:52:14 +05:30
Harsh Shandilya af10b117b4 build: uprev dependencies and fix script block order
- buildscript must always be the first block in a Gradle build

- ConstraintLayout, Kotlin and bintray plugin are updated to their latest stable revisions

- Biometrics is updated to the latest alpha release to make use of multiple memory leak fixes that plague the 1.0.x implementations

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-09-13 17:51:30 +05:30
Harsh Shandilya 7aa7825209 build: update to Gradle 6.6.1
While praying F-Droid gets their shit together by the time we do our next release

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-08-30 23:23:04 +05:30
Harsh Shandilya 8b7617294e tools: bump for Android 11 ndc fix
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-08-26 18:59:57 +05:30
Harsh Shandilya 9985b9b08e build: target SDK 30
We're all set to support it from the application side of things.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-08-25 21:17:05 +05:30
Harsh Shandilya 840d65881e build: switch fragment and preference to -ktx artifacts
Google recommends all dependencies with -ktx variants depend on them directly since they transitively pull in the main artifacts and offer extensions for better usage from Kotlin

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-08-23 01:07:27 +05:30
Harsh Shandilya c18f6818e8 build: uprev core-ktx and material components
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-08-23 01:07:26 +05:30
Harsh Shandilya dcd91cad1b ui: fix SDK 30 deprecation warning for implicit Looper in Handler init
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-08-23 01:03:49 +05:30
Harsh Shandilya 898bb679d2 ui: also enable StrictMode thread policy in debug builds
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-08-23 01:03:48 +05:30
Harsh Shandilya 348d430cd3 build: remove explicit buildToolsVersion
AGP sets it automatically, let's rely on that

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-08-23 00:49:11 +05:30
Harsh Shandilya e3d98633fb build: update AndroidX dependencies
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-08-23 00:43:25 +05:30
Harsh Shandilya b451920408 build: uprev to Kotlin 1.4
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-08-23 00:43:14 +05:30
Harsh Shandilya 1fa15e76e3 build: minor cleanups and reorganization
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-08-23 00:42:51 +05:30
Harsh Shandilya 8a58270e03 build: uprev to Gradle 6.6
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-08-12 13:28:00 +05:30
Jason A. Donenfeld e08d873cc3 version: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-07-24 09:50:31 +02:00
Jason A. Donenfeld 3748a1da88 AdminKnobs: allow enterprise admins to disable private key export
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-07-23 17:35:47 +02:00
Jason A. Donenfeld 9597d719ac build: bump AGP to 4.0.1
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-07-23 16:04:22 +02:00
Harsh Shandilya 041ce2134f build: upgrade to Gradle 6.5.1
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-07-10 15:07:54 +05:30
Harsh Shandilya 4c17fc51e7 gitignore: add gradlew.bat
We do not support building on Windows for now and deleting this file on every wrapper update is just lame

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-06-25 01:36:31 +05:30
Harsh Shandilya 6cf7439053 gradle: upgrade to 6.5
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-06-25 01:36:30 +05:30
Harsh Shandilya 814ab4937d build: bump core-ktx, coroutines and fragments
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-06-25 01:30:00 +05:30
David Crawshaw 3d088411e2 libwg-go: use conn.Bind for socketfd peek
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-06-22 02:24:55 -06:00
Jason A. Donenfeld 39e0c861e2 tools: bump versions
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-06-07 01:58:02 -06:00
Harsh Shandilya d60efcd7c7 strings: sync translations
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-06-02 17:52:01 +05:30
Harsh Shandilya 4dff537d21 ui: address new databinding requirements
Layouts in differing configurations must agree on their root tags, so we give
both copies of main_activity the same root ID, and adjust the check for
two-pane layout to simply test for nullability. This also changes the inset
dispatch code to use ViewCompat and WindowInsetsCompat since they will adjust
insets based on the SDK level allowing us to abstract away that concern.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-05-29 12:25:52 +05:30
Harsh Shandilya 761c2ade4a build: bump to AGP 4.0.0
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-05-29 12:05:24 +05:30
Harsh Shandilya 115a87af32 ui: remove unneeded actionBarPopupTheme attribute
This caused inconsistencies between our overflow icon and actionbar title's tint

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-05-09 15:49:43 +05:30
Harsh Shandilya c7ecaeb145 build: upgrade to gradle 6.4 and track checksum
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-05-09 13:15:57 +05:30
Jason A. Donenfeld 8e2d63db75 tunnel: add windows-style killswitch semantics for GoBackend
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-05-04 02:30:44 -06:00
Harsh Shandilya 3208bac987 build: upgrade to mdc-android 1.2.0-alpha06
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-30 18:21:31 +05:30
Harsh Shandilya fc1f2132fe build: bump gradle wrapper version in task configuration
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-27 14:09:37 +05:30
Harsh Shandilya 09125e1e31 libwg-go: bump go version
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-26 04:33:41 +05:30
Harsh Shandilya 6b1164ad8d ui: switch up dark theme system bars colors
The status bar color had a subtle transparency applied which caused
inconsistencies, and the navigation bar color was the wrong shade of
gray.

Reported-by: Danny Lin <danny@kdrag0n.dev>
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-26 04:30:26 +05:30
Harsh Shandilya c68a5c776b build: bump threetenabp to 1.2.4
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-24 19:32:36 +05:30
Harsh Shandilya f3ac787f5a ToolsInstaller: update Magisk directory
/data/adb/modules has been the location for modules since v18.0 and nobody
should reasonably be on any older version anymore. This has continued to
work for this long because Magisk created symlinks for backwards compat. However,
these symlinks are not created anymore on Android 11, which is where this
problem	first surfaced.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-23 01:40:50 +05:30
Harsh Shandilya 751be2c469 build: don't obfuscate code in minify step
While Play Console correctly manages to deobfuscate stacktraces using the
ProGuard mapping, it makes user recorded logs useless which is rather
undesirable.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-21 21:36:59 +05:30
Harsh Shandilya cfb7f1b016 ui: tweak fab animation duration
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-19 02:17:27 +05:30
Harsh Shandilya 33e07628db build: remove unused cardview dependency
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-19 02:09:07 +05:30
Harsh Shandilya b1b7d1d90c build: upgrade AGP, Kotlin, and preference
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-19 02:08:41 +05:30
Harsh Shandilya 6cb9548f72 tunnel: disable LongLogTag lint
Should have been part of the patch that disabled this for ui/

Fixes: 8d128cf2e9 ("ui: disable LongLogTag lint")
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-19 01:59:51 +05:30
Harsh Shandilya 8207ae1e8d strings: sync translations
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-19 01:48:50 +05:30
Harsh Shandilya 48e0e427eb ui: animate fab scale rather than translation
Suggested-by: Jason A. Donenfeld <Jason@zx2c4.com>
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-16 23:14:35 +05:30
Jason A. Donenfeld 03e95d2dd3 ObservableTunnel: account for race in renulling stats
The stats might become null between these two checks, when a tunnel
flips off, resulting in a null pointer dereference:

at com.wireguard.android.model.ObservableTunnel.getStatisticsAsync (ObservableTunnel.java:103)
at com.wireguard.android.fragment.TunnelDetailFragment.updateStats (TunnelDetailFragment.java:108)
at com.wireguard.android.fragment.TunnelDetailFragment.access$updateStats (TunnelDetailFragment.java:27)
at com.wireguard.android.fragment.TunnelDetailFragment$onResume$1.run (TunnelDetailFragment.java:74)
at java.util.TimerThread.mainLoop (TimerThread.java:562)
at java.util.TimerThread.run (TimerThread.java:512)

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-04-15 01:57:55 -06:00
Harsh Shandilya 58b14cc650 ui: animate fab position in tunnel deletion flow
When tunnel deletion is triggered we don't bother with animation theatrics
because the resulting Snackbar needs this fab to be its anchor, which it can't
do if its outside the screen or busy animating.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-08 19:43:25 +05:30
Harsh Shandilya 3fa8e09545 tunnel: disable BuildConfig generation
We don't (and shouldn't) use BuildConfig values, but the class was
polluting our public API regardless which is undesirable.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-08 17:05:31 +05:30
Jason A. Donenfeld 9d76b354f0 version: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-04-08 00:02:37 -06:00
Harsh Shandilya 1856a50c56 strings: sync translations
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-08 03:37:01 +05:30
Harsh Shandilya ba0f3ece04 ui: tweak FABs to use the correct type
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-08 03:34:07 +05:30
Harsh Shandilya e8ad75d924 ui: codestyle nit
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-07 14:19:54 +05:30
Jason A. Donenfeld b858284b1e KernelModuleDisablerPreference: do not make synchronous calls to getBackend
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-04-07 02:28:34 -06:00
Jason A. Donenfeld 3c6e06f8d5 ui: set proper content description for applications
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-04-06 19:25:05 -06:00
Jason A. Donenfeld 0d33a00e72 version: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-04-06 19:09:25 -06:00
Jason A. Donenfeld 830d0992a7 ui: show excluded/included apps in details view
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-04-06 19:08:57 -06:00
Jason A. Donenfeld e4192ea172 ui: align listen port and mtu in detail editor
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-04-06 18:43:41 -06:00
Jason A. Donenfeld 4bbb1a0fcd ui: add suffix to persistent keepalive in editor
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-04-06 18:24:08 -06:00
Jason A. Donenfeld 77b5937fbb ui: add missing fields to detail view
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-04-06 18:09:03 -06:00
Jason A. Donenfeld 3144d36056 TunnelListFragment: onCreateActionMode is called before adapter is alive
Long press a tunnel item. SIGKILL the app. Reenter it. Boom:

kotlin.KotlinNullPointerException:
  at com.wireguard.android.fragment.TunnelListFragment$ActionModeListener.onCreateActionMode (TunnelListFragment.java:347)
  at androidx.appcompat.app.AppCompatDelegateImpl$ActionModeCallbackWrapperV9.onCreateActionMode (AppCompatDelegateImpl.java:2442)
  at androidx.appcompat.app.WindowDecorActionBar$ActionModeImpl.dispatchOnCreate (WindowDecorActionBar.java:1062)
  at androidx.appcompat.app.WindowDecorActionBar.startActionMode (WindowDecorActionBar.java:530)
  at androidx.appcompat.app.AppCompatDelegateImpl.startSupportActionMode (AppCompatDelegateImpl.java:1055)
  at androidx.appcompat.app.AppCompatActivity.startSupportActionMode (AppCompatActivity.java:316)
  at com.wireguard.android.fragment.TunnelListFragment$ActionModeListener.setItemChecked (TunnelListFragment.java:371)
  at com.wireguard.android.fragment.TunnelListFragment.onActivityCreated (TunnelListFragment.java:174)
  at androidx.fragment.app.Fragment.performActivityCreated (Fragment.java:2717)
  at androidx.fragment.app.FragmentStateManager.activityCreated (FragmentStateManager.java:346)
  at androidx.fragment.app.FragmentManager.moveToState (FragmentManager.java:1188)
  at androidx.fragment.app.FragmentManager.moveToState (FragmentManager.java:161)
  at androidx.fragment.app.FragmentManager.moveToState (FragmentManager.java:1356)
  at androidx.fragment.app.FragmentManager.moveToState (FragmentManager.java:5)
  at androidx.fragment.app.FragmentManager.moveFragmentToExpectedState (FragmentManager.java:1434)
  at androidx.fragment.app.FragmentManager.moveFragmentToExpectedState (FragmentManager.java:5)
  at androidx.fragment.app.FragmentManager.moveToState (FragmentManager.java:1497)
  at androidx.fragment.app.FragmentManager.moveToState (FragmentManager.java:389)
  at androidx.fragment.app.FragmentManager.dispatchStateChange (FragmentManager.java:2625)
  at androidx.fragment.app.FragmentManager.dispatchStateChange (FragmentManager.java:677)
  at androidx.fragment.app.FragmentManager.dispatchActivityCreated (FragmentManager.java:2577)
  at androidx.fragment.app.FragmentManager.dispatchActivityCreated (FragmentManager.java:9)
  at androidx.fragment.app.FragmentController.dispatchActivityCreated (FragmentController.java:247)
  at androidx.fragment.app.FragmentActivity.onStart (FragmentActivity.java:541)
  at androidx.appcompat.app.AppCompatActivity.onStart (AppCompatActivity.java:201)
  at android.app.Instrumentation.callActivityOnStart (Instrumentation.java:1440)
  at android.app.Activity.performStart (Activity.java:8109)
  at android.app.ActivityThread.handleStartActivity (ActivityThread.java:3806)
  at android.app.servertransaction.TransactionExecutor.performLifecycleSequence (TransactionExecutor.java:235)
  at android.app.servertransaction.TransactionExecutor.cycleToPath (TransactionExecutor.java:215)
  at android.app.servertransaction.TransactionExecutor.executeLifecycleState (TransactionExecutor.java:187)
  at android.app.servertransaction.TransactionExecutor.execute (TransactionExecutor.java:105)
  at android.app.ActivityThread$H.handleMessage (ActivityThread.java:2386)
  at android.os.Handler.dispatchMessage (Handler.java:107)
  at android.os.Looper.loop (Looper.java:213)
  at android.app.ActivityThread.main (ActivityThread.java:8178)
  at java.lang.reflect.Method.invoke (Method.java)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:513)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1101)

This commit avoids the crash. But it's not clear to me that this is
really the right solution. However, in testing it appears to work.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-04-06 15:56:07 -06:00
Jason A. Donenfeld 521bbb4b4c AppListDialogFragment: refine singular grammar
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-04-06 19:56:53 +05:30
Jason A. Donenfeld 0a55e10b94 AppListDialogFragment: remove colons from tabs
Harsh's club became ill upon seeing them.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-04-06 19:56:53 +05:30
Harsh Shandilya d6e5fd9301 ui: tweak cards to make them nicer on the eyes
Surprisingly-requested-by: Jason A. Donenfeld <Jason@zx2c4.com>
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-06 19:56:53 +05:30
Harsh Shandilya 2c625f56fd ui: misc cleanups to AppListDialogFragment
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-06 19:56:46 +05:30
Jason A. Donenfeld 7db0fa915e AppListDialogFragment: support both inclusion and exclusion
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-04-06 02:40:49 -06:00
Jason A. Donenfeld e424765a61 tunnel: support IncludedApplications as whitelist
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-04-05 20:04:51 -06:00
Harsh Shandilya 1ca4dbf1a2 tunnel: hold peers in an ArrayList
A set will cause identical peers to be dropped during save

Reported-by: Jason A. Donenfeld <Jason@zx2c4.com>
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-06 01:22:44 +05:30
Harsh Shandilya 065893e31d MultiselectableRelativeLayout: use JvmOverloads constructor
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-06 00:39:22 +05:30
Harsh Shandilya ef70aa88e1 ui: fix scrolling in detail view
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-06 00:17:14 +05:30
Harsh Shandilya 6a11a199ca strings: sync translations
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-04 16:19:21 +05:30
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
Jason A. Donenfeld 07c85ee6f5 version: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-22 13:09:09 -06:00
Jason A. Donenfeld c02dd9e040 Preferences: hide advanced settings
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-22 01:50:58 -06:00
Jason A. Donenfeld 7dae94976d SettingsActivity: remove from parent instead of fixed screen
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-22 01:23:23 -06:00
Jason A. Donenfeld 38c360cb74 libwg-go: bump go version
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-22 01:07:20 -06:00
Jason A. Donenfeld edba640641 TunnelManager: enable Tasker support
Also fix up wording for the permission prompt that Tasker will show when
initially setting this up.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-22 00:51:37 -06:00
Jason A. Donenfeld cf25ae4448 model: begin conversion
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-22 00:19:09 -06:00
Eiji Tanioka e86182af56 strings: Update Japanese translation.
Signed-off-by: Eiji Tanioka <tanioka404@gmail.com>
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-21 19:18:36 +05:30
Jason A. Donenfeld 902a1ce46e viewmodel: port to kotlin
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-21 03:06:56 -06:00
Harsh Shandilya 55849cad65 Replace PermissionRequestCallback with direct methods
These are much cleaner in Kotlin code.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-20 22:22:56 -06:00
Harsh Shandilya edb76af820 build: Load keystore file from rootProject
Since the signing config is also stored in the project root it makes sense to have storeFile
path be relative to it.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-20 22:22:56 -06:00
Harsh Shandilya 04d0b819f6 Convert activity package to Kotlin
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-20 22:22:56 -06:00
Jason A. Donenfeld 85aa5fbd46 AddTunnels: rearrange and relabel
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-20 02:56:10 -06:00
Jason A. Donenfeld 1054e54c89 widget: rewrite in kotlin
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-20 02:46:01 -06:00
Jason A. Donenfeld 2fe5b92035 EdgeToEdge: move into widget
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-20 02:28:49 -06:00
Fiqri Ardyansyah 3a5a161c03 strings: Update Indonesian translation
Signed-off-by: Fiqri Ardyansyah <fiqri15072019@gmail.com>
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-19 18:56:05 -06:00
Jason A. Donenfeld 8451321a79 preferences: rewrite in kotlin
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-19 18:55:45 -06:00
Jason A. Donenfeld 90050a0008 TunnelListFragment: cleanup list type
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-19 17:52:51 -06:00
Jason A. Donenfeld 87c9efce4a TunnelListFragment: catch all parsing exceptions
Otherwise we crash if weird things happen.

Reported-by: Luis Ressel <aranea@aixah.de>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-19 13:05:53 -06:00
Jason A. Donenfeld 240e049e46 InetEndpoint: return proper parser exception
Wrapping this in something foreign doesn't make sense.

Reported-by: Luis Ressel <aranea@aixah.de>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-19 13:04:31 -06:00
Harsh Shandilya 0899b49bb3 Use more Kotlin-esque code where applicable
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-19 14:45:07 +05:30
Harsh Shandilya fc0660ca8d ui: Convert fragment package to Kotlin
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-19 09:24:48 +05:30
Harsh Shandilya b2ed5dbbc8 codestyle: Require atleast 10 references before using star imports
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-19 09:24:48 +05:30
Harsh Shandilya 94c864503e ui: Convert configStore package to Kotlin
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-19 09:24:47 +05:30
Eiji Tanioka c387c6aebf strings: Update Japanese Translation
Signed-off-by: Eiji Tanioka <tanioka404@gmail.com>
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-19 09:16:16 +05:30
Harsh Shandilya 55c9d39c13 strings: Remove now defunct translations
The original strings are removed.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-18 15:18:02 +05:30
Jason A. Donenfeld 6a3b143876 preference: restart application immediately
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-17 23:51:41 -06:00
Shashank Baghel 5f29abfa0d strings: update Hindi translation
Signed-off-by: Shashank Baghel <theradcolor@gmail.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-17 21:46:13 -06:00
Fiqri Ardyansyah ea3364ac12 strings: add Indonesian translations
Signed-off-by: Fiqri Ardyansyah <fiqri15072019@gmail.com>
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-17 21:45:16 -06:00
Harsh Shandilya 2b31eac1af publish.gradle: Use non-deprecated API to prevent eager configuration of tasks
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-14 10:44:52 +05:30
Harsh Shandilya 093139bc91 tunnel: Add an initial set of unit tests
Includes a control set of broken configuration files that we attempt to parse and
verify that the parser fails in a predictable and consistent manner.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-14 10:32:12 +05:30
Harsh Shandilya 6c8a4a6a28 tunnel: Remove MISSING_VALUE from BadConfigException reasons
This is covered under SYNTAX_ERROR which feels like a reasonable choice,
so get rid of this unused field and associated resource string.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-14 10:25:57 +05:30
Jason A. Donenfeld f5d2fd6190 config: show missing section error correctly
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-13 13:20:34 -06:00
LilligantMatsuri 4d77bd8f25 strings: update Simplified Chinese translation
Signed-off-by: LilligantMatsuri <srb12345@vip.qq.com>
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-12 17:26:36 +05:30
Eiji Tanioka 1068adbee3 strings: update Japanese translation
Signed-off-by: Eiji Tanioka <tanioka404@gmail.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-12 01:26:56 -06:00
Jason A. Donenfeld 86fc518585 tunnel: replace CompletableFuture with GhettoCompletableFuture
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-12 01:14:13 -06:00
Jason A. Donenfeld 78377a5c67 tunnel: we return Optional types so mark retrostreams as api
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-12 00:23:03 -06:00
Jason A. Donenfeld 56f2dcc073 tunnel: add javadoc support
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-11 00:29:18 -06:00
Alexey 48739b4141 strings: update Russian translate
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-11 00:29:18 -06:00
Jason A. Donenfeld 704c344213 tunnel: the external API does not expose java9
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-10 11:52:30 -06:00
Jason A. Donenfeld ee263ba68e README: note JCenter addition
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-10 11:49:04 -06:00
Jason A. Donenfeld 692b71af23 tunnel: tools: update to latest go
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-10 22:12:16 +05:30
Jason A. Donenfeld 7e029f1db0 ModuleDownloaderPreference: remove disable_kernel_module after downloading
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-10 22:12:16 +05:30
Jason A. Donenfeld d4875afe31 tunnel: make use of @RestrictTo
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-10 22:12:16 +05:30
Jason A. Donenfeld de0e431d00 build: abstract out groupName
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-10 22:12:12 +05:30
Jason A. Donenfeld 6b304391b5 global: java access control has important semantic meaning
It's not right to blindly follow all of AndroidStudio's suggestions,
especially for things in tunnel/ which comprise useful API.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-10 08:44:38 -06:00
Harsh Shandilya 8f85e4c88f tunnel: Codestyle cleanups
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-10 13:20:16 +05:30
Harsh Shandilya a3b9c3b884 ui: Codestyle cleanups
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-10 13:08:28 +05:30
Harsh Shandilya 021e16959f Upgrade to Gradle 6.2.2
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-10 12:45:06 +05:30
Harsh Shandilya 37adab76e1 BaseFragment: Always anchor Snackbars to FAB
This makes tunnel list fragment correctly render them above the FAB.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-10 12:32:23 +05:30
Jason A. Donenfeld 5d342ee1ab tunnel: upload aar to bintray
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-10 00:29:38 -06:00
Jason A. Donenfeld 697d131397 strings: remove stale translations
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 22:32:56 -06:00
Jason A. Donenfeld 89d9e30025 build: apply version to both modules
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 21:54:27 -06:00
Jason A. Donenfeld dfde86df76 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 17:31:52 -06:00
Jason A. Donenfeld d55fb25a40 KernelModuleDisablerPreference: turn off tunnels before switching backend
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 16:18:55 -06:00
Jason A. Donenfeld 492fcce053 KernelModuleDisabler: allow disabling the kernel module backend
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 15:21:34 -06:00
Jason A. Donenfeld d61f17dbd3 Preferences: give dual state summary for restore on boot
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 14:54:20 -06:00
Jason A. Donenfeld 8996979dc2 Preferences: reorder new checkbox
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 14:49:31 -06:00
Jason A. Donenfeld 31ddb242a6 VersionPreference: reverse go and wgquick titles
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 14:48:02 -06:00
Jason A. Donenfeld b7b5d96b3b TunnelDetailFragment: avoid integer overflow
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 12:38:38 -06:00
Jason A. Donenfeld 2e55e5fd05 global: format code
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 12:36:47 -06:00
Jason A. Donenfeld 40ebf8006e global: optimize imports
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 12:21:35 -06:00
Jason A. Donenfeld 8e8643122e global: get rid of nonnull gradle hack
Hacking things up via gradle is not right, and package-info.java poses
problems with two modules, so instead we just apply it manually to every
class.

Remember to add this to new classes!

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 12:19:15 -06:00
Jason A. Donenfeld c00a0b12e4 Application: simplify static block
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 12:16:02 -06:00
Jason A. Donenfeld 0e21520fd1 gradle: use optimized proguard rules
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 11:29:17 -06:00
Harsh Shandilya 0db233e5c7 Use dark system bars on API 21
Light system bars didn't come around until API 23.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-09 22:12:14 +05:30
Jason A. Donenfeld b9948085a4 ThemeChangeAwareActivity: recreate activity after changing theme
API 21 had issues without this.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 10:39:04 -06:00
Jason A. Donenfeld d62526fde6 WgQuickBackend: by default use single-tunnel mode like GoBackend, but add option
Note that this currently doesn't play well with people activating
wg-quick tunnels from outside the app. Those tunnels won't be
deactivated. But presumably that's desired behavior anyway, considering
people are mucking around at the command line.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 10:02:00 -06:00
Jason A. Donenfeld 134f9c014e ObservableTunnel: pass right argument to state transition
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 09:55:05 -06:00
Jason A. Donenfeld 056cf472d9 ModuleLoader: move to right project
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 09:07:48 -06:00
Jason A. Donenfeld 0c161cc0c2 AsyncWorker: move back to original location
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 08:59:37 -06:00
Harsh Shandilya 7894894610 Use AndroidX Nullable annotation everywhere
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-09 19:24:27 +05:30
Harsh Shandilya 7d48bef70a Rename app module to ui
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-09 19:24:27 +05:30
Harsh Shandilya 6bc3e257f8 Enable nonnull generation for tunnel module
Also cleanup nonnull.gradle while we're at it

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-09 19:24:26 +05:30
Harsh Shandilya adc613d801 Migrate tunnel related classes to tunnel/ Gradle module
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-09 19:24:26 +05:30
Jason A. Donenfeld fd573f6c1c ToolsInstaller: restrict to only main app usage
We don't want lots of different packages fighting over who gets to
install the tools.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 05:07:27 -06:00
Jason A. Donenfeld 1235e966d2 ToolsInstaller: remove versioning
Not ideal, but allows us to get rid of the BuildConfig requirement.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 05:07:02 -06:00
Jason A. Donenfeld e6a2f049ea RootShell: remove need for BuildConfig
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 04:48:58 -06:00
Jason A. Donenfeld d9e9dd04af Tunnel: move state change into interface
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 04:46:17 -06:00
Harsh Shandilya 3c2fa15dc2 Fix tablet mode crashes and UI inconsistencies
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-09 10:50:36 +05:30
Harsh Shandilya e8c9c20570 MainActivity: Dispatch insets to all fragments
Fixes tunnel list fragment rendering behind statusbar when other fragments are showing

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-09 10:32:48 +05:30
Harsh Shandilya 0db2578ca0 Switch to CoordinatorLayout for activity container
Snackbars use it to infer the right margins

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-09 10:32:03 +05:30
Harsh Shandilya e71d6157f1 Add coordinatorlayout dependency and sort
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-09 10:20:14 +05:30
Harsh Shandilya 7ca2a0df18 global: Use requireContext rather than getContext in fragments
requireContext provides helpful error messages when it's null as opposed to getContext
which simply throws a NullPointerException.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-09 10:12:50 +05:30
Jason A. Donenfeld 7a4af834c2 Backend: do not use singletons
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-08 20:23:25 -06:00
Jason A. Donenfeld 314a0d124d GoBackend: setConfigureIntent does nothing
It's only used from the ManageDialog in VpnDialogs, which in turn is
only instantiated in the legacy VPN path.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-08 19:52:57 -06:00
Jason A. Donenfeld 6f1e86e8a7 ToolsInstaller: do not use R
This is horible! But ToolsInstaller uses lots of other error strings
nakedly, as does ModuleLoader. These both need to be fixed up the proper
way (like the last two commits). This commit here is just to make the
initial porting a bit easier.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 01:42:38 +08:00
Jason A. Donenfeld 840c7ea560 RootShell: properly use errormessages
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 01:36:01 +08:00
Jason A. Donenfeld 1839730663 ModuleDownloaderPreference: properly use errormessages
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 01:28:41 +08:00
Jason A. Donenfeld afd75cc4cf ErrorMessages: do not use R from backend
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 00:58:32 +08:00
Jason A. Donenfeld 453a1aaa65 ErrorMessages: do not traverse down into remote exceptions
Otherwise we miss the actual error message.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 00:19:56 +08:00
Jason A. Donenfeld 4905185e61 backend: do not depend on anything except config
This is likely broken but should make for a good starting point.

It also should hopefully handle stopping tunnels before starting new
ones, in the case of the GoBackend. Again, untested.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-09 00:19:42 +08:00
Harsh Shandilya 0990430513 Upgrade AGP and Kotlin
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-08 14:05:45 +08:00
Shashank Baghel 7df13a044f Add Hindi translation
Signed-off-by: Shashank Baghel <theradcolor@gmail.com>
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-08 14:05:36 +08:00
Harsh Shandilya 6135a1f60a MainActivity: Remove unused imports
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-02-28 13:25:28 +05:30
Harsh Shandilya 5e94adc73a ModuleLoader: Staticize isModuleLoaded
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-02-28 13:16:31 +05:30
Harsh Shandilya 44fc0228a9 Upgrade to build tools 29.0.3
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-02-28 12:28:02 +05:30
Harsh Shandilya 4d2bfe3ef3 Resolve some lint and build warnings
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-02-26 20:26:49 +05:30
LilligantMatsuri 7f37ff032f Add Simplified Chinese translation
Signed-off-by: LilligantMatsuri <srb12345@vip.qq.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-02-25 22:04:01 -06:00
Eiji Tanioka 30d508464f remove <item quantity=one> tag due to no plural form in Japanese.
Signed-off-by: Eiji Tanioka <tanioka404@gmail.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-02-25 22:00:15 -06:00
Harsh Shandilya 75b0fed00d ThemeChangeAwareActivity: Remove drawable cache buster
Shouldn't be needed anymore.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-02-25 17:14:52 +05:30
Harsh Shandilya 927cc1fcf5 Make exclusions dialog fit more items in viewport
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-02-25 14:23:28 +05:30
Harsh Shandilya b3090e277a MainActivity: Fix backstack bug exposed by fragment 1.2.2
We've been relying on implicit backstack changes to handle removing the detail fragment for a while which is now gone so let's do this properly like we should

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-02-25 14:23:28 +05:30
Harsh Shandilya 0b45151a3d Resolve deprecation warnings
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-02-25 09:53:01 +05:30
Harsh Shandilya eb5bfa9b5d Cleanup inset dispatch code
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-02-25 09:53:00 +05:30
Harsh Shandilya 4267e94dcd Replace FrameLayouts with recommended FragmentContainerView
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-02-25 08:57:29 +05:30
Harsh Shandilya fac9e7612f Add fragment dependency
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-02-25 08:57:28 +05:30
Harsh Shandilya 01e8e535f4 Ensure insets are dispatched to all fragments
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-02-25 08:57:28 +05:30
Harsh Shandilya e5e2e7571f Setup EdgeToEdge
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-02-25 08:57:27 +05:30
Harsh Shandilya c889a8c8de Add EdgeToEdge and coreKtx dependency
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-02-25 08:57:26 +05:30
Harsh Shandilya cbf2ea7b48 Target JVM 1.8 from Kotlin as well
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-02-25 08:57:26 +05:30
Harsh Shandilya 8078347880 Upgrade AGP and Gradle
If anybody has a problem with missing or mismatched NDK, run this command to install the right version

$ANDROID_SDK_ROOT/tools/bin/sdkmanager 'ndk;20.0.5594570'

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-02-25 08:29:53 +05:30
Harsh Shandilya dd8a802bec Resolve format string warnings
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-02-23 18:55:09 +05:30
Harsh Shandilya 6f6602ddd1 AppListDialogFragment: Revamp toggle logic
Rather than always toggle all elements, elect to unselect all if any are selected. This allows
returning to a clean state in at most two clicks.

Suggested-by: Jason A. Donenfeld <Jason@zx2c4.com>
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-02-23 15:07:39 +05:30
Harsh Shandilya 33e69db436 AppListDialogFragment: Code cleanup
- Using the require_() methods provides helpful error messages when things are null
compared to the get_() methods which throw NPEs.
- Ensure currentlyExcludedApps is empty but never null
- Rename inner variable to silence name shadowing lint
- Make setExclusionsAndDismiss private

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-02-23 15:07:39 +05:30
Eiji Tanioka 13cbec28d6 Japanese translation: change "Deselect All" to "Toggle All"
Fixes: 1eb4ce7 ("AppListDialogFragment: change "Deselect All" to "Toggle All"")
Signed-off-by: Eiji Tanioka <tanioka404@gmail.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-02-23 10:11:14 +01:00
Sébastien LEBEAU 826083adf6 AppListDialogFragment: change "Deselect All" to "Toggle All"
Change functionality in excluded apps dialog for better user experience
when user wants only one or few apps to use WireGuard.

Signed-off-by: Sébastien LEBEAU <sebcbi1@gmail.com>
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-02-23 10:11:12 +01:00
Harsh Shandilya a62bd28e1f SharedLibraryLoader: Fix leaked Closeable warning
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-02-16 15:07:21 +05:30
Harsh Shandilya da1188c6ee Record StrictMode failures to logcat
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-02-16 15:07:20 +05:30
Eiji Tanioka e22cefbfe3 Add japanese translation.
Signed-off-by: Eiji Tanioka <tanioka404@gmail.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-02-15 00:06:18 -06:00
Harsh Shandilya 02ea696070 Port tunnel creation UI from Viscerion
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-02-14 15:27:17 +05:30
Harsh Shandilya d25702d99d Implement custom theming to match Google's AOSP design
Closes: #4
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-02-12 15:23:53 +01:00
Jason A. Donenfeld c554413327 Attribute: remove need for android TextUtils
If this is to be JRE-only, then it doesn't make sense to rely on the
android class, especially since this is so trivial to inline.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-02-10 17:04:34 +01:00
Jason A. Donenfeld f8c5f238ea InetAddresses: don't have global android import
This allows a proper fallback on normal JRE.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-02-10 16:55:34 +01:00
Alexey 66b46c8618 Update Russian translation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-02-09 11:27:30 +01:00
Jason A. Donenfeld 04689d37b7 version: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-02-06 22:01:03 +01:00
Jason A. Donenfeld e041cacf4b wg: bump to latest snapshot
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-02-06 22:01:03 +01:00
Alexey f54fc92b14 Add Russian translation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-02-06 22:01:03 +01:00
Jason A. Donenfeld 6e7b6bcc35 wg-go: bump go requirement
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-02-03 17:21:34 +01:00
Jason A. Donenfeld e70d5be535 wg: remove mnl dependency and bump dep
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-02-03 17:09:45 +01:00
269 changed files with 13457 additions and 8437 deletions
+1 -1
View File
@@ -15,4 +15,4 @@ build/
*.iml
*.jks
keystore.properties
package-info.java
gradlew.bat
+2 -5
View File
@@ -1,6 +1,3 @@
[submodule "app/tools/libmnl"]
path = app/tools/libmnl
url = https://git.netfilter.org/libmnl/
[submodule "app/tools/wireguard-tools"]
path = app/tools/wireguard-tools
[submodule "tunnel/tools/wireguard-tools"]
path = tunnel/tools/wireguard-tools
url = https://git.zx2c4.com/wireguard-tools
+18 -3
View File
@@ -29,9 +29,6 @@
<emptyLine />
</value>
</option>
<AndroidXmlCodeStyleSettings>
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
</AndroidXmlCodeStyleSettings>
<JavaCodeStyleSettings>
<option name="GENERATE_FINAL_LOCALS" value="true" />
<option name="GENERATE_FINAL_PARAMETERS" value="true" />
@@ -59,6 +56,24 @@
</value>
</option>
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="10" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="10" />
</JetCodeStyleSettings>
<XML>
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
</XML>
+14 -13
View File
@@ -9,7 +9,6 @@
</option>
<option name="nonThreadSafeTypes" value="" />
</inspection_tool>
<inspection_tool class="AndroidLintGoogleAppIndexingWarning" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="AndroidLintIconExpectedSize" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AndroidLintNegativeMargin" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AndroidLintTypographyQuotes" enabled="true" level="WARNING" enabled_by_default="true" />
@@ -40,7 +39,9 @@
<inspection_tool class="AssignmentToLambdaParameter" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AssignmentToSuperclassField" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AssignmentUsedAsCondition" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true">
<option name="METHOD_MATCHER_CONFIG" value="java.util.Formatter,format,java.io.Writer,append,com.google.common.base.Preconditions,checkNotNull,org.hibernate.Session,close,java.io.PrintWriter,printf,java.util.HashMap,put,java.util.Map,put" />
</inspection_tool>
<inspection_tool class="BadExceptionCaught" enabled="true" level="WARNING" enabled_by_default="true">
<option name="exceptionsString" value="" />
<option name="exceptions">
@@ -59,6 +60,9 @@
<inspection_tool class="CallToStringConcatCanBeReplacedByOperator" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CannotResolve" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="CastConflictsWithInstanceof" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CatchMayIgnoreException" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreCatchBlocksWithComments" value="false" />
</inspection_tool>
<inspection_tool class="ChainedEquality" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ClassInitializer" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ClassNameDiffersFromFileName" enabled="true" level="WARNING" enabled_by_default="true" />
@@ -103,9 +107,6 @@
<inspection_tool class="DoubleLiteralMayBeFloatLiteral" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DuplicateAlternationBranch" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DuplicateBooleanBranch" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DuplicateCondition" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreSideEffectConditions" value="true" />
</inspection_tool>
<inspection_tool class="DuplicateDeclarations" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="DynamicRegexReplaceableByCompiledPattern" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ElementOnlyUsedFromTestCode" enabled="false" level="WARNING" enabled_by_default="false" />
@@ -196,7 +197,6 @@
<inspection_tool class="ImplicitSubclassInspection" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="IncompatibleTypes" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="InitializerIssues" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="InnerClassMayBeStatic" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="InnerClassReferencedViaSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="InnerClassVariableHidesOuterClassVariable" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreInvisibleFields" value="true" />
@@ -215,7 +215,6 @@
<inspection_tool class="InstanceofThis" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="InstantiationOfUtilityClass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="IntLiteralMayBeLongLiteral" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="IntegerDivisionInFloatingPointContext" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="IntegerTypeRequired" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="InterfaceMayBeAnnotatedFunctional" enabled="true" level="INFORMATION" enabled_by_default="true" />
<inspection_tool class="InterfaceNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
@@ -294,6 +293,7 @@
<option name="ignoreForLoopDeclarations" value="true" />
</inspection_tool>
<inspection_tool class="MultipleTopLevelClassesInFile" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="MultipleVariablesInDeclaration" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NamedResource" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="NativeMethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="NegatedConditional" enabled="true" level="WARNING" enabled_by_default="true">
@@ -347,7 +347,6 @@
<inspection_tool class="OverriddenMethodCallDuringObjectConstruction" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PackageInfoWithoutPackage" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PointerTypeRequired" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="PointlessNullCheck" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ProblematicVarargsMethodOverride" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ProtectedField" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ProtectedInnerClass" enabled="true" level="WARNING" enabled_by_default="true">
@@ -394,7 +393,13 @@
<inspection_tool class="SizeReplaceableByIsEmpty" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StaticCallOnSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StaticFieldReferenceOnSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StaticImport" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StaticImport" enabled="true" level="WARNING" enabled_by_default="true">
<option name="allowedClasses">
<set>
<option value="org.junit.Assert" />
</set>
</option>
</inspection_tool>
<inspection_tool class="StaticInheritance" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StaticMethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
<option name="m_regex" value="[a-z][A-Za-z\d]*" />
@@ -432,7 +437,6 @@
<inspection_tool class="TemplateArgumentsIssues" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="TestMethodWithoutAssertion" enabled="false" level="WARNING" enabled_by_default="false">
<option name="assertionMethods" value="org.junit.Assert,assert.*|fail.*,junit.framework.Assert,assert.*|fail.*,org.junit.jupiter.api.Assertions,assert.*|fail.*,org.mockito.Mockito,verify.*,org.mockito.InOrder,verify,org.junit.rules.ExpectedException,expect.*,org.hamcrest.MatcherAssert,assertThat" />
<option name="assertKeywordIsAssertion" value="false" />
</inspection_tool>
<inspection_tool class="TestNGMethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ThisEscapedInConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
@@ -440,7 +444,6 @@
<option name="ignoreRethrownExceptions" value="true" />
</inspection_tool>
<inspection_tool class="ThrowsRuntimeException" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ToArrayCallWithZeroLengthArrayArgument" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="TooBroadScope" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_allowConstructorAsInitializer" value="false" />
<option name="m_onlyLookAtBlocks" value="true" />
@@ -466,10 +469,8 @@
<inspection_tool class="UnnecessaryBlockStatement" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreSwitchBranches" value="false" />
</inspection_tool>
<inspection_tool class="UnnecessaryCallToStringValueOf" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryConstantArrayCreationExpression" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryDefault" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryExplicitNumericCast" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryFullyQualifiedName" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreJavadoc" value="false" />
+23
View File
@@ -11,3 +11,26 @@ $ git clone --recurse-submodules https://git.zx2c4.com/wireguard-android
$ 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), alongside [extensive class library documentation](https://javadoc.io/doc/com.wireguard.android/tunnel).
```
implementation 'com.wireguard.android:tunnel:$wireguardTunnelVersion'
```
The library makes use of Java 8 features, so be sure to support those in your gradle configuration:
```
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
```
## Translating
Please help us translate the app into several languages on [our translation platform](https://crowdin.com/project/WireGuard).
-104
View File
@@ -1,104 +0,0 @@
apply plugin: 'com.android.application'
apply from: 'nonnull.gradle'
// Create a variable called keystorePropertiesFile, and initialize it to your
// keystore.properties file, in the rootProject folder.
final def keystorePropertiesFile = rootProject.file("keystore.properties")
android {
buildToolsVersion '29.0.2'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
compileSdkVersion 29
dataBinding.enabled true
defaultConfig {
applicationId 'com.wireguard.android'
minSdkVersion 21
targetSdkVersion 29
versionCode 463
versionName '0.0.20200124'
buildConfigField 'int', 'MIN_SDK_VERSION', "$minSdkVersion.apiLevel"
}
// If the keystore file exists
if (keystorePropertiesFile.exists()) {
// Initialize a new Properties() object called keystoreProperties.
final def keystoreProperties = new Properties()
// Load your keystore.properties file into the keystoreProperties object.
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}
}
buildTypes {
release {
if (keystorePropertiesFile.exists()) signingConfig signingConfigs.release
externalNativeBuild {
cmake {
arguments "-DANDROID_PACKAGE_NAME=${android.defaultConfig.applicationId}"
}
}
minifyEnabled true
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
debug {
applicationIdSuffix ".debug"
versionNameSuffix "-debug"
externalNativeBuild {
cmake {
arguments "-DANDROID_PACKAGE_NAME=${android.defaultConfig.applicationId}${applicationIdSuffix}"
}
}
}
}
externalNativeBuild {
cmake {
path 'tools/CMakeLists.txt'
}
}
}
ext {
annotationsVersion = '1.1.0'
appcompatVersion = '1.1.0'
cardviewVersion = '1.0.0'
databindingVersion = '3.5.3'
materialComponentsVersion = '1.0.0'
jsr305Version = '3.0.2'
preferenceVersion = '1.1.0'
streamsupportVersion = '1.7.1'
threetenabpVersion = '1.2.1'
// 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'
eddsaVersion = '0.3.0'
}
dependencies {
implementation "androidx.annotation:annotation:$annotationsVersion"
implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation "androidx.cardview:cardview:$cardviewVersion"
implementation "androidx.databinding:databinding-runtime:$databindingVersion"
implementation "androidx.preference:preference:$preferenceVersion"
implementation "com.google.android.material:material:$materialComponentsVersion"
implementation "com.google.code.findbugs:jsr305:$jsr305Version"
implementation "com.jakewharton.threetenabp:threetenabp:$threetenabpVersion"
implementation "com.journeyapps:zxing-android-embedded:$zxingEmbeddedVersion"
implementation "net.sourceforge.streamsupport:android-retrofuture:$streamsupportVersion"
implementation "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion"
implementation "net.i2p.crypto:eddsa:$eddsaVersion"
}
tasks.withType(JavaCompile) {
options.compilerArgs << '-Xlint:unchecked'
options.deprecation = true
}
-87
View File
@@ -1,87 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
task generateNonNullJavaFiles(dependsOn: "assembleDebug", type: Copy) {
group = "Copying"
description = "Generate package-info.java classes"
def basePackage = "com" + File.separatorChar + "wireguard"
def mainSrcPhrase = "src" + File.separatorChar + "main" + File.separatorChar +
"java" + File.separatorChar
def mainTestSrcPhrase = "src" + File.separatorChar + "test" + File.separatorChar +
"java" + File.separatorChar
def mainAndroidTestSrcPhrase = "src" + File.separatorChar + "androidTest" + File.separatorChar +
"java" + File.separatorChar
def sourceDir = file( "${projectDir}" + File.separatorChar + "src" + File.separatorChar +
"main" + File.separatorChar + "java" + File.separatorChar +
basePackage )
def testSourceDir = file( "${projectDir}" + File.separatorChar + "src" + File.separatorChar +
"test" + File.separatorChar + "java" + File.separatorChar +
basePackage)
def androidTestSourceDir = file( "${projectDir}" + File.separatorChar + "src" + File
.separatorChar +
"androidTest" + File.separatorChar + "java" + File.separatorChar +
basePackage )
generateInfoFiles(sourceDir, mainSrcPhrase);
sourceDir.eachDirRecurse { dir ->
generateInfoFiles(dir, mainSrcPhrase)
}
if (file(testSourceDir).exists()) {
generateInfoFiles(testSourceDir, mainTestSrcPhrase);
testSourceDir.eachDirRecurse { dir ->
generateInfoFiles(dir, mainTestSrcPhrase)
}
}
if (file(androidTestSourceDir).exists()) {
generateInfoFiles(androidTestSourceDir, mainAndroidTestSrcPhrase);
androidTestSourceDir.eachDirRecurse { dir ->
generateInfoFiles(dir, mainAndroidTestSrcPhrase)
}
}
println "[SUCCESS] NonNull generator: package-info.java files checked"
}
private void generateInfoFiles(File dir, String mainSrcPhrase) {
def infoFileContentHeader = getFileContentHeader();
def infoFileContentFooter = getFileContentFooter();
def infoFilePath = dir.getAbsolutePath() + File.separatorChar + "package-info.java"
//file(infoFilePath).delete(); //do not use in production code
if (!file(infoFilePath).exists()) {
def infoFileContentPackage = getFileContentPackage(dir.getAbsolutePath(), mainSrcPhrase);
new File(infoFilePath).write(infoFileContentHeader +
infoFileContentPackage + infoFileContentFooter)
println "[dir] " + infoFilePath + " created";
}
}
def getFileContentPackage(String path, String mainSrcPhrase) {
def mainSrcPhraseIndex = path.indexOf(mainSrcPhrase)
def output = path.substring(mainSrcPhraseIndex)
// Win hotfix
if (System.properties['os.name'].toLowerCase().contains('windows')) {
output = output.replace("\\", "/")
mainSrcPhrase = mainSrcPhrase.replace("\\", "/")
}
return "package " + output.replaceAll(mainSrcPhrase, "").replaceAll(
"/", ".") + ";\n"
}
def getFileContentHeader() {
return "/**\n" +
" * Make all method parameters @NonNull by default.\n" +
" */\n" +
"@NonNullForAll\n"
}
def getFileContentFooter() {
return "\n" +
"import com.wireguard.util.NonNullForAll;\n"
}
-3
View File
@@ -1,3 +0,0 @@
# Squelch all warnings, they're harmless but ProGuard
# escalates them as errors.
-dontwarn sun.misc.Unsafe
@@ -1,160 +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.util.Log;
import androidx.preference.PreferenceManager;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDelegate;
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.ModuleLoader;
import com.wireguard.android.util.RootShell;
import com.wireguard.android.util.ToolsInstaller;
import java.lang.ref.WeakReference;
import java.util.Locale;
import java9.util.concurrent.CompletableFuture;
public class Application extends android.app.Application {
private static final String TAG = "WireGuard/" + Application.class.getSimpleName();
public static final String USER_AGENT;
static {
String preferredAbi = "unknown ABI";
if (Build.SUPPORTED_ABIS.length > 0)
preferredAbi = Build.SUPPORTED_ABIS[0];
USER_AGENT = String.format(Locale.ENGLISH, "WireGuard/%s (Android %d; %s; %s; %s %s; %s)", BuildConfig.VERSION_NAME, Build.VERSION.SDK_INT, preferredAbi, Build.BOARD, Build.MANUFACTURER, Build.MODEL, Build.FINGERPRINT);
}
@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 RootShell rootShell;
@SuppressWarnings("NullableProblems") private SharedPreferences sharedPreferences;
@SuppressWarnings("NullableProblems") private ToolsInstaller toolsInstaller;
@SuppressWarnings("NullableProblems") private ModuleLoader moduleLoader;
@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 (!app.moduleLoader.isModuleLoaded() && app.moduleLoader.moduleMightExist()) {
try {
app.rootShell.start();
didStartRootShell = true;
app.moduleLoader.loadModule();
} catch (final Exception ignored) {
}
}
if (app.moduleLoader.isModuleLoaded()) {
try {
if (!didStartRootShell)
app.rootShell.start();
backend = new WgQuickBackend(app.getApplicationContext());
} catch (final Exception ignored) {
}
}
if (backend == null)
backend = new GoBackend(app.getApplicationContext());
app.backend = backend;
}
return app.backend;
}
}
public static CompletableFuture<Backend> getBackendAsync() {
return get().futureBackend;
}
public static RootShell getRootShell() {
return get().rootShell;
}
public static SharedPreferences getSharedPreferences() {
return get().sharedPreferences;
}
public static ToolsInstaller getToolsInstaller() {
return get().toolsInstaller;
}
public static ModuleLoader getModuleLoader() {
return get().moduleLoader;
}
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);
}
}
@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());
moduleLoader = new ModuleLoader(getApplicationContext());
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);
}
}
@@ -1,38 +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;
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();
}
});
}
}
@@ -1,174 +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 androidx.databinding.Observable;
import androidx.databinding.Observable.OnPropertyChangedCallback;
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 androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import android.util.Log;
import com.wireguard.android.activity.MainActivity;
import com.wireguard.android.activity.TunnelToggleActivity;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.widget.SlashDrawable;
import java.util.Objects;
/**
* 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)
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 Tunnel 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 Tunnel 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() == Tunnel.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();
}
}
}
@@ -1,99 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity;
import androidx.databinding.CallbackRegistry;
import androidx.databinding.CallbackRegistry.NotifierCallback;
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.wireguard.android.Application;
import com.wireguard.android.model.Tunnel;
import java.util.Objects;
/**
* Base class for activities that need to remember the currently-selected tunnel.
*/
public abstract class BaseActivity extends ThemeChangeAwareActivity {
private static final String KEY_SELECTED_TUNNEL = "selected_tunnel";
private final SelectionChangeRegistry selectionChangeRegistry = new SelectionChangeRegistry();
@Nullable private Tunnel selectedTunnel;
public void addOnSelectedTunnelChangedListener(final OnSelectedTunnelChangedListener listener) {
selectionChangeRegistry.add(listener);
}
@Nullable
public Tunnel getSelectedTunnel() {
return selectedTunnel;
}
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
// Restore the saved tunnel if there is one; otherwise grab it from the arguments.
final String savedTunnelName;
if (savedInstanceState != null)
savedTunnelName = savedInstanceState.getString(KEY_SELECTED_TUNNEL);
else if (getIntent() != null)
savedTunnelName = getIntent().getStringExtra(KEY_SELECTED_TUNNEL);
else
savedTunnelName = null;
if (savedTunnelName != null)
Application.getTunnelManager().getTunnels()
.thenAccept(tunnels -> setSelectedTunnel(tunnels.get(savedTunnelName)));
// The selected tunnel must be set before the superclass method recreates fragments.
super.onCreate(savedInstanceState);
}
@Override
protected void onSaveInstanceState(final Bundle outState) {
if (selectedTunnel != null)
outState.putString(KEY_SELECTED_TUNNEL, selectedTunnel.getName());
super.onSaveInstanceState(outState);
}
protected abstract void onSelectedTunnelChanged(@Nullable Tunnel oldTunnel, @Nullable Tunnel newTunnel);
public void removeOnSelectedTunnelChangedListener(
final OnSelectedTunnelChangedListener listener) {
selectionChangeRegistry.remove(listener);
}
public void setSelectedTunnel(@Nullable final Tunnel tunnel) {
final Tunnel oldTunnel = selectedTunnel;
if (Objects.equals(oldTunnel, tunnel))
return;
selectedTunnel = tunnel;
onSelectedTunnelChanged(oldTunnel, tunnel);
selectionChangeRegistry.notifyCallbacks(oldTunnel, 0, tunnel);
}
public interface OnSelectedTunnelChangedListener {
void onSelectedTunnelChanged(@Nullable Tunnel oldTunnel, @Nullable Tunnel newTunnel);
}
private static final class SelectionChangeNotifier
extends NotifierCallback<OnSelectedTunnelChangedListener, Tunnel, Tunnel> {
@Override
public void onNotifyCallback(final OnSelectedTunnelChangedListener listener,
final Tunnel oldTunnel, final int ignored,
final Tunnel newTunnel) {
listener.onSelectedTunnelChanged(oldTunnel, newTunnel);
}
}
private static final class SelectionChangeRegistry
extends CallbackRegistry<OnSelectedTunnelChangedListener, Tunnel, Tunnel> {
private SelectionChangeRegistry() {
super(new SelectionChangeNotifier());
}
}
}
@@ -1,139 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.appcompat.app.ActionBar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.LinearLayout;
import com.wireguard.android.R;
import com.wireguard.android.fragment.TunnelDetailFragment;
import com.wireguard.android.fragment.TunnelEditorFragment;
import com.wireguard.android.fragment.TunnelListFragment;
import com.wireguard.android.model.Tunnel;
/**
* CRUD interface for WireGuard tunnels. This activity serves as the main entry point to the
* WireGuard application, and contains several fragments for listing, viewing details of, and
* editing the configuration and interface state of WireGuard tunnels.
*/
public class MainActivity extends BaseActivity
implements FragmentManager.OnBackStackChangedListener {
@Nullable private ActionBar actionBar;
private boolean isTwoPaneLayout;
@Nullable private TunnelListFragment listFragment;
@Override
public void onBackPressed() {
final int backStackEntries = getSupportFragmentManager().getBackStackEntryCount();
// If the action menu is visible and expanded, collapse it instead of navigating back.
if (isTwoPaneLayout || backStackEntries == 0) {
if (listFragment != null && listFragment.collapseActionMenu())
return;
}
// If the two-pane layout does not have an editor open, going back should exit the app.
if (isTwoPaneLayout && backStackEntries <= 1) {
finish();
return;
}
// Deselect the current tunnel on navigating back from the detail pane to the one-pane list.
if (!isTwoPaneLayout && backStackEntries == 1) {
setSelectedTunnel(null);
return;
}
super.onBackPressed();
}
@Override public void onBackStackChanged() {
if (actionBar == null)
return;
// Do not show the home menu when the two-pane layout is at the detail view (see above).
final int backStackEntries = getSupportFragmentManager().getBackStackEntryCount();
final int minBackStackEntries = isTwoPaneLayout ? 2 : 1;
actionBar.setDisplayHomeAsUpEnabled(backStackEntries >= minBackStackEntries);
}
// We use onTouchListener here to avoid the UI click sound, hence
// calling View#performClick defeats the purpose of it.
@SuppressLint("ClickableViewAccessibility")
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
actionBar = getSupportActionBar();
isTwoPaneLayout = findViewById(R.id.master_detail_wrapper) instanceof LinearLayout;
listFragment = (TunnelListFragment) getSupportFragmentManager().findFragmentByTag("LIST");
getSupportFragmentManager().addOnBackStackChangedListener(this);
onBackStackChanged();
final View actionBarView = findViewById(R.id.action_bar);
if (actionBarView != null)
actionBarView.setOnTouchListener((v, e) -> listFragment != null && listFragment.collapseActionMenu());
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.main_activity, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
// The back arrow in the action bar should act the same as the back button.
onBackPressed();
return true;
case R.id.menu_action_edit:
getSupportFragmentManager().beginTransaction()
.replace(R.id.detail_container, new TunnelEditorFragment())
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.addToBackStack(null)
.commit();
return true;
case R.id.menu_action_save:
// This menu item is handled by the editor fragment.
return false;
case R.id.menu_settings:
startActivity(new Intent(this, SettingsActivity.class));
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
protected void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel,
@Nullable final Tunnel newTunnel) {
final FragmentManager fragmentManager = getSupportFragmentManager();
final int backStackEntries = fragmentManager.getBackStackEntryCount();
if (newTunnel == null) {
// Clear everything off the back stack (all editors and detail fragments).
fragmentManager.popBackStackImmediate(0, FragmentManager.POP_BACK_STACK_INCLUSIVE);
return;
}
if (backStackEntries == 2) {
// Pop the editor off the back stack to reveal the detail fragment. Use the immediate
// method to avoid the editor picking up the new tunnel while it is still visible.
fragmentManager.popBackStackImmediate();
} else if (backStackEntries == 0) {
// Create and show a new detail fragment.
fragmentManager.beginTransaction()
.add(R.id.detail_container, new TunnelDetailFragment())
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.addToBackStack(null)
.commit();
}
}
}
@@ -1,128 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceScreen;
import android.util.SparseArray;
import android.view.MenuItem;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.backend.WgQuickBackend;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Interface for changing application-global persistent settings.
*/
public class SettingsActivity extends ThemeChangeAwareActivity {
private final SparseArray<PermissionRequestCallback> permissionRequestCallbacks = new SparseArray<>();
private int permissionRequestCounter;
public void ensurePermissions(final String[] permissions, final PermissionRequestCallback cb) {
final List<String> needPermissions = new ArrayList<>(permissions.length);
for (final String permission : permissions) {
if (ContextCompat.checkSelfPermission(this, permission)
!= PackageManager.PERMISSION_GRANTED)
needPermissions.add(permission);
}
if (needPermissions.isEmpty()) {
final int[] granted = new int[permissions.length];
Arrays.fill(granted, PackageManager.PERMISSION_GRANTED);
cb.done(permissions, granted);
return;
}
final int idx = permissionRequestCounter++;
permissionRequestCallbacks.put(idx, cb);
ActivityCompat.requestPermissions(this,
needPermissions.toArray(new String[needPermissions.size()]), idx);
}
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getSupportFragmentManager().findFragmentById(android.R.id.content) == null) {
getSupportFragmentManager().beginTransaction()
.add(android.R.id.content, new SettingsFragment())
.commit();
}
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onRequestPermissionsResult(final int requestCode,
final String[] permissions,
final int[] grantResults) {
final PermissionRequestCallback f = permissionRequestCallbacks.get(requestCode);
if (f != null) {
permissionRequestCallbacks.remove(requestCode);
f.done(permissions, grantResults);
}
}
public interface PermissionRequestCallback {
void done(String[] permissions, int[] grantResults);
}
public static class SettingsFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String key) {
addPreferencesFromResource(R.xml.preferences);
final PreferenceScreen screen = getPreferenceScreen();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
screen.removePreference(getPreferenceManager().findPreference("dark_theme"));
final Preference wgQuickOnlyPrefs[] = {
getPreferenceManager().findPreference("tools_installer"),
getPreferenceManager().findPreference("restore_on_boot")
};
for (final Preference pref : wgQuickOnlyPrefs)
pref.setVisible(false);
Application.getBackendAsync().thenAccept(backend -> {
for (final Preference pref : wgQuickOnlyPrefs) {
if (backend instanceof WgQuickBackend)
pref.setVisible(true);
else
screen.removePreference(pref);
}
});
final Preference moduleInstaller = getPreferenceManager().findPreference("module_downloader");
moduleInstaller.setVisible(false);
if (Application.getModuleLoader().isModuleLoaded()) {
screen.removePreference(moduleInstaller);
} else {
Application.getAsyncWorker().runAsync(Application.getRootShell()::start).whenComplete((v, e) -> {
if (e == null)
moduleInstaller.setVisible(true);
else
screen.removePreference(moduleInstaller);
});
}
}
}
}
@@ -1,82 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
import android.util.Log;
import com.wireguard.android.Application;
import java.lang.reflect.Field;
public abstract class ThemeChangeAwareActivity extends AppCompatActivity implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = "WireGuard/" + ThemeChangeAwareActivity.class.getSimpleName();
private static boolean lastDarkMode;
@Nullable private static Resources lastResources;
private static synchronized void invalidateDrawableCache(final Resources resources, final boolean darkMode) {
if (resources == lastResources && darkMode == lastDarkMode)
return;
try {
Field f;
Object o = resources;
try {
f = o.getClass().getDeclaredField("mResourcesImpl");
f.setAccessible(true);
o = f.get(o);
} catch (final Exception ignored) {
}
f = o.getClass().getDeclaredField("mDrawableCache");
f.setAccessible(true);
o = f.get(o);
try {
o.getClass().getMethod("onConfigurationChange", int.class).invoke(o, -1);
} catch (final Exception ignored) {
o.getClass().getMethod("clear").invoke(o);
}
} catch (final Exception e) {
Log.e(TAG, "Failed to flush drawable cache", e);
}
lastResources = resources;
lastDarkMode = darkMode;
}
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
Application.getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
}
@Override
protected void onDestroy() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
Application.getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
super.onDestroy();
}
@Override
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) {
if ("dark_theme".equals(key)) {
final boolean darkMode = sharedPreferences.getBoolean(key, false);
AppCompatDelegate.setDefaultNightMode(
sharedPreferences.getBoolean(key, false) ?
AppCompatDelegate.MODE_NIGHT_YES :
AppCompatDelegate.MODE_NIGHT_NO);
invalidateDrawableCache(getResources(), darkMode);
recreate();
}
}
}
@@ -1,34 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity;
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.wireguard.android.fragment.TunnelEditorFragment;
import com.wireguard.android.model.Tunnel;
/**
* Standalone activity for creating tunnels.
*/
public class TunnelCreatorActivity extends BaseActivity {
@Override
@SuppressWarnings("UnnecessaryFullyQualifiedName")
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getSupportFragmentManager().findFragmentById(android.R.id.content) == null) {
getSupportFragmentManager().beginTransaction()
.add(android.R.id.content, new TunnelEditorFragment())
.commit();
}
}
@Override
protected void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
finish();
}
}
@@ -1,51 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import android.content.ComponentName;
import android.os.Bundle;
import android.os.Build;
import android.service.quicksettings.TileService;
import android.util.Log;
import android.widget.Toast;
import com.wireguard.android.Application;
import com.wireguard.android.QuickTileService;
import com.wireguard.android.R;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.util.ErrorMessages;
@RequiresApi(Build.VERSION_CODES.N)
public class TunnelToggleActivity extends AppCompatActivity {
private static final String TAG = "WireGuard/" + TunnelToggleActivity.class.getSimpleName();
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Tunnel tunnel = Application.getTunnelManager().getLastUsedTunnel();
if (tunnel == null)
return;
tunnel.setState(State.TOGGLE).whenComplete((v, t) -> {
TileService.requestListeningState(this, new ComponentName(this, QuickTileService.class));
onToggleFinished(t);
finishAffinity();
});
}
private void onToggleFinished(@Nullable final Throwable throwable) {
if (throwable == null)
return;
final String error = ErrorMessages.get(throwable);
final String message = getString(R.string.toggle_error, error);
Log.e(TAG, message, throwable);
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
}
@@ -1,152 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.backend;
import android.content.Context;
import androidx.annotation.Nullable;
import android.util.Log;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.model.Tunnel.Statistics;
import com.wireguard.config.Config;
import com.wireguard.crypto.Key;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java9.util.stream.Collectors;
import java9.util.stream.Stream;
/**
* WireGuard backend that uses {@code wg-quick} to implement tunnel configuration.
*/
public final class WgQuickBackend implements Backend {
private static final String TAG = "WireGuard/" + WgQuickBackend.class.getSimpleName();
private final File localTemporaryDir;
private final Context context;
public WgQuickBackend(final Context context) {
localTemporaryDir = new File(context.getCacheDir(), "tmp");
this.context = context;
}
@Override
public Config applyConfig(final Tunnel tunnel, final Config config) throws Exception {
if (tunnel.getState() == State.UP) {
// Restart the tunnel to apply the new config.
setStateInternal(tunnel, tunnel.getConfig(), State.DOWN);
try {
setStateInternal(tunnel, config, State.UP);
} catch (final Exception e) {
// The new configuration didn't work, so try to go back to the old one.
setStateInternal(tunnel, tunnel.getConfig(), State.UP);
throw e;
}
}
return config;
}
@Override
public Set<String> enumerate() {
final List<String> output = new ArrayList<>();
// Don't throw an exception here or nothing will show up in the UI.
try {
Application.getToolsInstaller().ensureToolsAvailable();
if (Application.getRootShell().run(output, "wg show interfaces") != 0 || output.isEmpty())
return Collections.emptySet();
} catch (final Exception e) {
Log.w(TAG, "Unable to enumerate running tunnels", e);
return Collections.emptySet();
}
// wg puts all interface names on the same line. Split them into separate elements.
return Stream.of(output.get(0).split(" ")).collect(Collectors.toUnmodifiableSet());
}
@Override
public State getState(final Tunnel tunnel) {
return enumerate().contains(tunnel.getName()) ? State.UP : State.DOWN;
}
@Override
public Statistics getStatistics(final Tunnel tunnel) {
final Statistics stats = new Statistics();
final Collection<String> output = new ArrayList<>();
try {
if (Application.getRootShell().run(output, String.format("wg show '%s' transfer", tunnel.getName())) != 0)
return stats;
} catch (final Exception ignored) {
return stats;
}
for (final String line : output) {
final String[] parts = line.split("\\t");
if (parts.length != 3)
continue;
try {
stats.add(Key.fromBase64(parts[0]), Long.parseLong(parts[1]), Long.parseLong(parts[2]));
} catch (final Exception ignored) {
}
}
return stats;
}
@Override
public String getTypePrettyName() {
return context.getString(R.string.type_name_kernel_module);
}
@Override
public String getVersion() throws Exception {
final List<String> output = new ArrayList<>();
if (Application.getRootShell()
.run(output, "cat /sys/module/wireguard/version") != 0 || output.isEmpty())
throw new Exception(context.getString(R.string.module_version_error));
return output.get(0);
}
@Override
public State setState(final Tunnel tunnel, State state) throws Exception {
final State originalState = getState(tunnel);
if (state == State.TOGGLE)
state = originalState == State.UP ? State.DOWN : State.UP;
if (state == originalState)
return originalState;
Log.d(TAG, "Changing tunnel " + tunnel.getName() + " to state " + state);
Application.getToolsInstaller().ensureToolsAvailable();
setStateInternal(tunnel, tunnel.getConfig(), state);
return getState(tunnel);
}
private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state) throws Exception {
Objects.requireNonNull(config, "Trying to set state with a null config");
final File tempFile = new File(localTemporaryDir, tunnel.getName() + ".conf");
try (final FileOutputStream stream = new FileOutputStream(tempFile, false)) {
stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8));
}
String command = String.format("wg-quick %s '%s'",
state.toString().toLowerCase(Locale.ENGLISH), tempFile.getAbsolutePath());
if (state == State.UP)
command = "cat /sys/module/wireguard/version && " + command;
final int result = Application.getRootShell().run(null, command);
// noinspection ResultOfMethodCallIgnored
tempFile.delete();
if (result != 0)
throw new Exception(context.getString(R.string.tunnel_config_error, result));
}
}
@@ -1,103 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.configStore;
import android.content.Context;
import android.util.Log;
import com.wireguard.android.R;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.Config;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java9.util.stream.Collectors;
import java9.util.stream.Stream;
/**
* Configuration store that uses a {@code wg-quick}-style file for each configured tunnel.
*/
public final class FileConfigStore implements ConfigStore {
private static final String TAG = "WireGuard/" + FileConfigStore.class.getSimpleName();
private final Context context;
public FileConfigStore(final Context context) {
this.context = context;
}
@Override
public Config create(final String name, final Config config) throws IOException {
Log.d(TAG, "Creating configuration for tunnel " + name);
final File file = fileFor(name);
if (!file.createNewFile())
throw new IOException(context.getString(R.string.config_file_exists_error, file.getName()));
try (final FileOutputStream stream = new FileOutputStream(file, false)) {
stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8));
}
return config;
}
@Override
public void delete(final String name) throws IOException {
Log.d(TAG, "Deleting configuration for tunnel " + name);
final File file = fileFor(name);
if (!file.delete())
throw new IOException(context.getString(R.string.config_delete_error, file.getName()));
}
@Override
public Set<String> enumerate() {
return Stream.of(context.fileList())
.filter(name -> name.endsWith(".conf"))
.map(name -> name.substring(0, name.length() - ".conf".length()))
.collect(Collectors.toUnmodifiableSet());
}
private File fileFor(final String name) {
return new File(context.getFilesDir(), name + ".conf");
}
@Override
public Config load(final String name) throws BadConfigException, IOException {
try (final FileInputStream stream = new FileInputStream(fileFor(name))) {
return Config.parse(stream);
}
}
@Override
public void rename(final String name, final String replacement) throws IOException {
Log.d(TAG, "Renaming configuration for tunnel " + name + " to " + replacement);
final File file = fileFor(name);
final File replacementFile = fileFor(replacement);
if (!replacementFile.createNewFile())
throw new IOException(context.getString(R.string.config_exists_error, replacement));
if (!file.renameTo(replacementFile)) {
if (!replacementFile.delete())
Log.w(TAG, "Couldn't delete marker file for new name " + replacement);
throw new IOException(context.getString(R.string.config_rename_error, file.getName()));
}
}
@Override
public Config save(final String name, final Config config) throws IOException {
Log.d(TAG, "Saving configuration for tunnel " + name);
final File file = fileFor(name);
if (!file.isFile())
throw new FileNotFoundException(context.getString(R.string.config_not_found_error, file.getName()));
try (final FileOutputStream stream = new FileOutputStream(file, false)) {
stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8));
}
return config;
}
}
@@ -1,148 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.databinding;
import androidx.databinding.BindingAdapter;
import androidx.databinding.DataBindingUtil;
import androidx.databinding.ObservableList;
import androidx.databinding.ViewDataBinding;
import androidx.databinding.adapters.ListenerUtil;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
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 java9.util.Optional;
/**
* Static methods for use by generated code in the Android data binding library.
*/
@SuppressWarnings("unused")
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) : "");
}
}
@@ -1,140 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.databinding;
import androidx.databinding.DataBindingUtil;
import androidx.databinding.ObservableList;
import androidx.databinding.ViewDataBinding;
import androidx.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.wireguard.android.BR;
import java.lang.ref.WeakReference;
import java.util.Objects;
/**
* Helper class for binding an ObservableList to the children of a ViewGroup.
*/
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);
}
}
}
}
@@ -1,159 +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 androidx.databinding.DataBindingUtil;
import androidx.databinding.ObservableList;
import androidx.databinding.ViewDataBinding;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.Adapter;
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 java.lang.ref.WeakReference;
/**
* A generic {@code RecyclerView.Adapter} backed by a {@code ObservableKeyedList}.
*/
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;
}
}
}
@@ -1,133 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.appcompat.app.AlertDialog;
import android.widget.Toast;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.databinding.AppListDialogFragmentBinding;
import com.wireguard.android.model.ApplicationData;
import com.wireguard.android.util.ErrorMessages;
import com.wireguard.android.util.ObservableKeyedArrayList;
import com.wireguard.android.util.ObservableKeyedList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java9.util.Comparators;
public class AppListDialogFragment extends DialogFragment {
private static final String KEY_EXCLUDED_APPS = "excludedApps";
private final ObservableKeyedList<String, ApplicationData> appData = new ObservableKeyedArrayList<>();
@Nullable private List<String> currentlyExcludedApps;
public static <T extends Fragment & AppExclusionListener>
AppListDialogFragment newInstance(final ArrayList<String> excludedApps, final T target) {
final Bundle extras = new Bundle();
extras.putStringArrayList(KEY_EXCLUDED_APPS, excludedApps);
final AppListDialogFragment fragment = new AppListDialogFragment();
fragment.setTargetFragment(target, 0);
fragment.setArguments(extras);
return fragment;
}
private void loadData() {
final Activity activity = getActivity();
if (activity == null) {
return;
}
final PackageManager pm = activity.getPackageManager();
Application.getAsyncWorker().supplyAsync(() -> {
final Intent launcherIntent = new Intent(Intent.ACTION_MAIN, null);
launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER);
final List<ResolveInfo> resolveInfos = pm.queryIntentActivities(launcherIntent, 0);
final List<ApplicationData> appData = new ArrayList<>();
for (ResolveInfo resolveInfo : resolveInfos) {
String packageName = resolveInfo.activityInfo.packageName;
appData.add(new ApplicationData(resolveInfo.loadIcon(pm), resolveInfo.loadLabel(pm).toString(), packageName, currentlyExcludedApps.contains(packageName)));
}
Collections.sort(appData, Comparators.comparing(ApplicationData::getName, String.CASE_INSENSITIVE_ORDER));
return appData;
}).whenComplete(((data, throwable) -> {
if (data != null) {
appData.clear();
appData.addAll(data);
} else {
final String error = ErrorMessages.get(throwable);
final String message = activity.getString(R.string.error_fetching_apps, error);
Toast.makeText(activity, message, Toast.LENGTH_LONG).show();
dismissAllowingStateLoss();
}
}));
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
currentlyExcludedApps = getArguments().getStringArrayList(KEY_EXCLUDED_APPS);
}
@Override
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity());
alertDialogBuilder.setTitle(R.string.excluded_applications);
final AppListDialogFragmentBinding binding = AppListDialogFragmentBinding.inflate(getActivity().getLayoutInflater(), null, false);
binding.executePendingBindings();
alertDialogBuilder.setView(binding.getRoot());
alertDialogBuilder.setPositiveButton(R.string.set_exclusions, (dialog, which) -> setExclusionsAndDismiss());
alertDialogBuilder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
alertDialogBuilder.setNeutralButton(R.string.deselect_all, (dialog, which) -> {
});
binding.setFragment(this);
binding.setAppData(appData);
loadData();
final AlertDialog dialog = alertDialogBuilder.create();
dialog.setOnShowListener(d -> dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener(view -> {
for (final ApplicationData app : appData)
app.setExcludedFromTunnel(false);
}));
return dialog;
}
void setExclusionsAndDismiss() {
final List<String> excludedApps = new ArrayList<>();
for (final ApplicationData data : appData) {
if (data.isExcludedFromTunnel()) {
excludedApps.add(data.getPackageName());
}
}
((AppExclusionListener) getTargetFragment()).onExcludedAppsSelected(excludedApps);
dismiss();
}
public interface AppExclusionListener {
void onExcludedAppsSelected(List<String> excludedApps);
}
}
@@ -1,126 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment;
import android.content.Context;
import android.content.Intent;
import androidx.databinding.DataBindingUtil;
import androidx.databinding.ViewDataBinding;
import androidx.annotation.Nullable;
import com.google.android.material.snackbar.Snackbar;
import androidx.fragment.app.Fragment;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.activity.BaseActivity;
import com.wireguard.android.activity.BaseActivity.OnSelectedTunnelChangedListener;
import com.wireguard.android.backend.GoBackend;
import com.wireguard.android.databinding.TunnelDetailFragmentBinding;
import com.wireguard.android.databinding.TunnelListItemBinding;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.util.ErrorMessages;
/**
* Base class for fragments that need to know the currently-selected tunnel. Only does anything when
* attached to a {@code BaseActivity}.
*/
public abstract class BaseFragment extends Fragment implements OnSelectedTunnelChangedListener {
private static final int REQUEST_CODE_VPN_PERMISSION = 23491;
private static final String TAG = "WireGuard/" + BaseFragment.class.getSimpleName();
@Nullable private BaseActivity activity;
@Nullable private Tunnel pendingTunnel;
@Nullable private Boolean pendingTunnelUp;
@Nullable
protected Tunnel getSelectedTunnel() {
return activity != null ? activity.getSelectedTunnel() : null;
}
@Override
public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_VPN_PERMISSION) {
if (pendingTunnel != null && pendingTunnelUp != null)
setTunnelStateWithPermissionsResult(pendingTunnel, pendingTunnelUp);
pendingTunnel = null;
pendingTunnelUp = null;
}
}
@Override
public void onAttach(final Context context) {
super.onAttach(context);
if (context instanceof BaseActivity) {
activity = (BaseActivity) context;
activity.addOnSelectedTunnelChangedListener(this);
} else {
activity = null;
}
}
@Override
public void onDetach() {
if (activity != null)
activity.removeOnSelectedTunnelChangedListener(this);
activity = null;
super.onDetach();
}
protected void setSelectedTunnel(@Nullable final Tunnel tunnel) {
if (activity != null)
activity.setSelectedTunnel(tunnel);
}
public void setTunnelState(final View view, final boolean checked) {
final ViewDataBinding binding = DataBindingUtil.findBinding(view);
final Tunnel tunnel;
if (binding instanceof TunnelDetailFragmentBinding)
tunnel = ((TunnelDetailFragmentBinding) binding).getTunnel();
else if (binding instanceof TunnelListItemBinding)
tunnel = ((TunnelListItemBinding) binding).getItem();
else
return;
if (tunnel == null)
return;
Application.getBackendAsync().thenAccept(backend -> {
if (backend instanceof GoBackend) {
final Intent intent = GoBackend.VpnService.prepare(view.getContext());
if (intent != null) {
pendingTunnel = tunnel;
pendingTunnelUp = checked;
startActivityForResult(intent, REQUEST_CODE_VPN_PERMISSION);
return;
}
}
setTunnelStateWithPermissionsResult(tunnel, checked);
});
}
private void setTunnelStateWithPermissionsResult(final Tunnel tunnel, final boolean checked) {
tunnel.setState(State.of(checked)).whenComplete((state, throwable) -> {
if (throwable == null)
return;
final String error = ErrorMessages.get(throwable);
final int messageResId = checked ? R.string.error_up : R.string.error_down;
final String message = getContext().getString(messageResId, error);
final View view = getView();
if (view != null)
Snackbar.make(view, message, Snackbar.LENGTH_LONG).show();
else
Toast.makeText(getContext(), message, Toast.LENGTH_LONG).show();
Log.e(TAG, message, throwable);
});
}
}
@@ -1,118 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.appcompat.app.AlertDialog;
import android.view.inputmethod.InputMethodManager;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.databinding.ConfigNamingDialogFragmentBinding;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.Config;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
public class ConfigNamingDialogFragment extends DialogFragment {
private static final String KEY_CONFIG_TEXT = "config_text";
@Nullable private ConfigNamingDialogFragmentBinding binding;
@Nullable private Config config;
@Nullable private InputMethodManager imm;
public static ConfigNamingDialogFragment newInstance(final String configText) {
final Bundle extras = new Bundle();
extras.putString(KEY_CONFIG_TEXT, configText);
final ConfigNamingDialogFragment fragment = new ConfigNamingDialogFragment();
fragment.setArguments(extras);
return fragment;
}
private void createTunnelAndDismiss() {
if (binding != null) {
final String name = binding.tunnelNameText.getText().toString();
Application.getTunnelManager().create(name, config).whenComplete((tunnel, throwable) -> {
if (tunnel != null) {
dismiss();
} else {
binding.tunnelNameTextLayout.setError(throwable.getMessage());
}
});
}
}
@Override
public void dismiss() {
setKeyboardVisible(false);
super.dismiss();
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Bundle arguments = getArguments();
final String configText = arguments.getString(KEY_CONFIG_TEXT);
final byte[] configBytes = configText.getBytes(StandardCharsets.UTF_8);
try {
config = Config.parse(new ByteArrayInputStream(configBytes));
} catch (final BadConfigException | IOException e) {
throw new IllegalArgumentException("Invalid config passed to " + getClass().getSimpleName(), e);
}
}
@Override
public Dialog onCreateDialog(final Bundle savedInstanceState) {
final Activity activity = Objects.requireNonNull(getActivity());
imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(activity);
alertDialogBuilder.setTitle(R.string.import_from_qr_code);
binding = ConfigNamingDialogFragmentBinding.inflate(activity.getLayoutInflater(), null, false);
binding.executePendingBindings();
alertDialogBuilder.setView(binding.getRoot());
alertDialogBuilder.setPositiveButton(R.string.create_tunnel, null);
alertDialogBuilder.setNegativeButton(R.string.cancel, (dialog, which) -> dismiss());
return alertDialogBuilder.create();
}
@Override public void onResume() {
super.onResume();
final AlertDialog dialog = (AlertDialog) getDialog();
if (dialog != null) {
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> createTunnelAndDismiss());
setKeyboardVisible(true);
}
}
private void setKeyboardVisible(final boolean visible) {
Objects.requireNonNull(imm);
if (visible) {
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
} else if (binding != null) {
imm.hideSoftInputFromWindow(binding.tunnelNameText.getWindowToken(), 0);
}
}
}
@@ -1,159 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.databinding.DataBindingUtil;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import com.wireguard.android.R;
import com.wireguard.android.databinding.TunnelDetailFragmentBinding;
import com.wireguard.android.databinding.TunnelDetailPeerBinding;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.crypto.Key;
import java.util.Timer;
import java.util.TimerTask;
/**
* Fragment that shows details about a specific tunnel.
*/
public class TunnelDetailFragment extends BaseFragment {
@Nullable private TunnelDetailFragmentBinding binding;
@Nullable private Timer timer;
@Nullable private State lastState = State.TOGGLE;
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
inflater.inflate(R.menu.tunnel_detail, menu);
}
@Override
public void onStop() {
super.onStop();
if (timer != null) {
timer.cancel();
timer = null;
}
}
@Override
public void onResume() {
super.onResume();
timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
updateStats();
}
}, 0, 1000);
}
@Override
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
binding = TunnelDetailFragmentBinding.inflate(inflater, container, false);
binding.executePendingBindings();
return binding.getRoot();
}
@Override
public void onDestroyView() {
binding = null;
super.onDestroyView();
}
@Override
public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
if (binding == null)
return;
binding.setTunnel(newTunnel);
if (newTunnel == null)
binding.setConfig(null);
else
newTunnel.getConfigAsync().thenAccept(binding::setConfig);
lastState = State.TOGGLE;
updateStats();
}
@Override
public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
if (binding == null) {
return;
}
binding.setFragment(this);
onSelectedTunnelChanged(null, getSelectedTunnel());
super.onViewStateRestored(savedInstanceState);
}
private String formatBytes(final long bytes) {
if (bytes < 1024)
return getContext().getString(R.string.transfer_bytes, bytes);
else if (bytes < 1024*1024)
return getContext().getString(R.string.transfer_kibibytes, bytes/1024.0);
else if (bytes < 1024*1024*1024)
return getContext().getString(R.string.transfer_mibibytes, bytes/(1024.0*1024.0));
else if (bytes < 1024*1024*1024*1024)
return getContext().getString(R.string.transfer_gibibytes, bytes/(1024.0*1024.0*1024.0));
return getContext().getString(R.string.transfer_tibibytes, bytes/(1024.0*1024.0*1024.0)/1024.0);
}
private void updateStats() {
if (binding == null || !isResumed())
return;
final Tunnel tunnel = binding.getTunnel();
if (tunnel == null)
return;
final State state = tunnel.getState();
if (state != State.UP && lastState == state)
return;
lastState = state;
tunnel.getStatisticsAsync().whenComplete((statistics, throwable) -> {
if (throwable != null) {
for (int i = 0; i < binding.peersLayout.getChildCount(); ++i) {
final TunnelDetailPeerBinding peer = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i));
if (peer == null)
continue;
peer.transferLabel.setVisibility(View.GONE);
peer.transferText.setVisibility(View.GONE);
}
return;
}
for (int i = 0; i < binding.peersLayout.getChildCount(); ++i) {
final TunnelDetailPeerBinding peer = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i));
if (peer == null)
continue;
final Key publicKey = peer.getItem().getPublicKey();
final long rx = statistics.peerRx(publicKey);
final long tx = statistics.peerTx(publicKey);
if (rx == 0 && tx == 0) {
peer.transferLabel.setVisibility(View.GONE);
peer.transferText.setVisibility(View.GONE);
continue;
}
peer.transferText.setText(getContext().getString(R.string.transfer_rx_tx, formatBytes(rx), formatBytes(tx)));
peer.transferLabel.setVisibility(View.VISIBLE);
peer.transferText.setVisibility(View.VISIBLE);
}
});
}
}
@@ -1,262 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment;
import android.app.Activity;
import android.content.Context;
import androidx.databinding.ObservableList;
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.google.android.material.snackbar.Snackbar;
import androidx.fragment.app.FragmentManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.databinding.TunnelEditorFragmentBinding;
import com.wireguard.android.fragment.AppListDialogFragment.AppExclusionListener;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.TunnelManager;
import com.wireguard.android.util.ErrorMessages;
import com.wireguard.android.viewmodel.ConfigProxy;
import com.wireguard.config.Config;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Fragment for editing a WireGuard configuration.
*/
public class TunnelEditorFragment extends BaseFragment implements AppExclusionListener {
private static final String KEY_LOCAL_CONFIG = "local_config";
private static final String KEY_ORIGINAL_NAME = "original_name";
private static final String TAG = "WireGuard/" + TunnelEditorFragment.class.getSimpleName();
@Nullable private TunnelEditorFragmentBinding binding;
@Nullable private Tunnel tunnel;
private void onConfigLoaded(final Config config) {
if (binding != null) {
binding.setConfig(new ConfigProxy(config));
}
}
private void onConfigSaved(final Tunnel savedTunnel,
@Nullable final Throwable throwable) {
final String message;
if (throwable == null) {
message = getString(R.string.config_save_success, savedTunnel.getName());
Log.d(TAG, message);
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
onFinished();
} else {
final String error = ErrorMessages.get(throwable);
message = getString(R.string.config_save_error, savedTunnel.getName(), error);
Log.e(TAG, message, throwable);
if (binding != null) {
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
}
}
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
inflater.inflate(R.menu.config_editor, menu);
}
@Override
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
binding = TunnelEditorFragmentBinding.inflate(inflater, container, false);
binding.executePendingBindings();
return binding.getRoot();
}
@Override
public void onDestroyView() {
binding = null;
super.onDestroyView();
}
@Override
public void onExcludedAppsSelected(final List<String> excludedApps) {
Objects.requireNonNull(binding, "Tried to set excluded apps while no view was loaded");
final ObservableList<String> excludedApplications =
binding.getConfig().getInterface().getExcludedApplications();
excludedApplications.clear();
excludedApplications.addAll(excludedApps);
}
private void onFinished() {
// Hide the keyboard; it rarely goes away on its own.
final Activity activity = getActivity();
if (activity == null) return;
final View focusedView = activity.getCurrentFocus();
if (focusedView != null) {
final Object service = activity.getSystemService(Context.INPUT_METHOD_SERVICE);
final InputMethodManager inputManager = (InputMethodManager) service;
if (inputManager != null)
inputManager.hideSoftInputFromWindow(focusedView.getWindowToken(),
InputMethodManager.HIDE_NOT_ALWAYS);
}
// Tell the activity to finish itself or go back to the detail view.
getActivity().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.
final Tunnel savedTunnel = tunnel;
if (savedTunnel == getSelectedTunnel())
setSelectedTunnel(null);
setSelectedTunnel(savedTunnel);
});
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_action_save:
if (binding == null)
return false;
final Config newConfig;
try {
newConfig = binding.getConfig().resolve();
} catch (final Exception e) {
final String error = ErrorMessages.get(e);
final String tunnelName = tunnel == null ? binding.getName() : tunnel.getName();
final String message = getString(R.string.config_save_error, tunnelName, error);
Log.e(TAG, message, e);
Snackbar.make(binding.mainContainer, error, Snackbar.LENGTH_LONG).show();
return false;
}
if (tunnel == null) {
Log.d(TAG, "Attempting to create new tunnel " + binding.getName());
final TunnelManager manager = Application.getTunnelManager();
manager.create(binding.getName(), newConfig)
.whenComplete(this::onTunnelCreated);
} else if (!tunnel.getName().equals(binding.getName())) {
Log.d(TAG, "Attempting to rename tunnel to " + binding.getName());
tunnel.setName(binding.getName())
.whenComplete((a, b) -> onTunnelRenamed(tunnel, newConfig, b));
} else {
Log.d(TAG, "Attempting to save config of " + tunnel.getName());
tunnel.setConfig(newConfig)
.whenComplete((a, b) -> onConfigSaved(tunnel, b));
}
return true;
default:
return super.onOptionsItemSelected(item);
}
}
public void onRequestSetExcludedApplications(@SuppressWarnings("unused") final View view) {
final FragmentManager fragmentManager = getFragmentManager();
if (fragmentManager != null && binding != null) {
final ArrayList<String> excludedApps = new ArrayList<>(binding.getConfig().getInterface().getExcludedApplications());
final AppListDialogFragment fragment = AppListDialogFragment.newInstance(excludedApps, this);
fragment.show(fragmentManager, null);
}
}
@Override
public void onSaveInstanceState(final Bundle outState) {
if (binding != null)
outState.putParcelable(KEY_LOCAL_CONFIG, binding.getConfig());
outState.putString(KEY_ORIGINAL_NAME, tunnel == null ? null : tunnel.getName());
super.onSaveInstanceState(outState);
}
@Override
public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel,
@Nullable final Tunnel newTunnel) {
tunnel = newTunnel;
if (binding == null)
return;
binding.setConfig(new ConfigProxy());
if (tunnel != null) {
binding.setName(tunnel.getName());
tunnel.getConfigAsync().thenAccept(this::onConfigLoaded);
} else {
binding.setName("");
}
}
private void onTunnelCreated(final Tunnel newTunnel, @Nullable final Throwable throwable) {
final String message;
if (throwable == null) {
tunnel = newTunnel;
message = getString(R.string.tunnel_create_success, tunnel.getName());
Log.d(TAG, message);
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
onFinished();
} else {
final String error = ErrorMessages.get(throwable);
message = getString(R.string.tunnel_create_error, error);
Log.e(TAG, message, throwable);
if (binding != null) {
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
}
}
}
private void onTunnelRenamed(final Tunnel renamedTunnel, final Config newConfig,
@Nullable final Throwable throwable) {
final String message;
if (throwable == null) {
message = getString(R.string.tunnel_rename_success, renamedTunnel.getName());
Log.d(TAG, message);
// Now save the rest of configuration changes.
Log.d(TAG, "Attempting to save config of renamed tunnel " + tunnel.getName());
renamedTunnel.setConfig(newConfig).whenComplete((a, b) -> onConfigSaved(renamedTunnel, b));
} else {
final String error = ErrorMessages.get(throwable);
message = getString(R.string.tunnel_rename_error, error);
Log.e(TAG, message, throwable);
if (binding != null) {
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
}
}
}
@Override
public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
if (binding == null) {
return;
}
binding.setFragment(this);
if (savedInstanceState == null) {
onSelectedTunnelChanged(null, getSelectedTunnel());
} else {
tunnel = getSelectedTunnel();
final ConfigProxy config = savedInstanceState.getParcelable(KEY_LOCAL_CONFIG);
final String originalName = savedInstanceState.getString(KEY_ORIGINAL_NAME);
if (tunnel != null && !tunnel.getName().equals(originalName))
onSelectedTunnelChanged(null, tunnel);
else
binding.setConfig(config);
}
super.onViewStateRestored(savedInstanceState);
}
}
@@ -1,479 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.OpenableColumns;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.snackbar.Snackbar;
import androidx.fragment.app.FragmentManager;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.recyclerview.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.activity.TunnelCreatorActivity;
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter;
import com.wireguard.android.databinding.TunnelListFragmentBinding;
import com.wireguard.android.databinding.TunnelListItemBinding;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.util.ErrorMessages;
import com.wireguard.android.widget.MultiselectableRelativeLayout;
import com.wireguard.android.widget.fab.FloatingActionsMenuRecyclerViewScrollListener;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.Config;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java9.util.concurrent.CompletableFuture;
import java9.util.stream.StreamSupport;
/**
* Fragment containing a list of known WireGuard tunnels. It allows creating and deleting tunnels.
*/
public class TunnelListFragment extends BaseFragment {
private static final int REQUEST_IMPORT = 1;
private static final String TAG = "WireGuard/" + TunnelListFragment.class.getSimpleName();
private final ActionModeListener actionModeListener = new ActionModeListener();
@Nullable private ActionMode actionMode;
@Nullable private TunnelListFragmentBinding binding;
public boolean collapseActionMenu() {
if (binding != null && binding.createMenu.isExpanded()) {
binding.createMenu.collapse();
return true;
}
return false;
}
private void importTunnel(@NonNull final String configText) {
try {
// Ensure the config text is parseable before proceeding…
Config.parse(new ByteArrayInputStream(configText.getBytes(StandardCharsets.UTF_8)));
// Config text is valid, now create the tunnel…
final FragmentManager fragmentManager = getFragmentManager();
if (fragmentManager != null)
ConfigNamingDialogFragment.newInstance(configText).show(fragmentManager, null);
} catch (final BadConfigException | IOException e) {
onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(e));
}
}
private void importTunnel(@Nullable final Uri uri) {
final Activity activity = getActivity();
if (activity == null || uri == null)
return;
final ContentResolver contentResolver = activity.getContentResolver();
final Collection<CompletableFuture<Tunnel>> futureTunnels = new ArrayList<>();
final List<Throwable> throwables = new ArrayList<>();
Application.getAsyncWorker().supplyAsync(() -> {
final String[] columns = {OpenableColumns.DISPLAY_NAME};
String name = null;
try (Cursor cursor = contentResolver.query(uri, columns,
null, null, null)) {
if (cursor != null && cursor.moveToFirst() && !cursor.isNull(0))
name = cursor.getString(0);
}
if (name == null)
name = Uri.decode(uri.getLastPathSegment());
int idx = name.lastIndexOf('/');
if (idx >= 0) {
if (idx >= name.length() - 1)
throw new IllegalArgumentException(getResources().getString(R.string.illegal_filename_error, name));
name = name.substring(idx + 1);
}
boolean isZip = name.toLowerCase(Locale.ENGLISH).endsWith(".zip");
if (name.toLowerCase(Locale.ENGLISH).endsWith(".conf"))
name = name.substring(0, name.length() - ".conf".length());
else if (!isZip)
throw new IllegalArgumentException(getResources().getString(R.string.bad_extension_error));
if (isZip) {
try (ZipInputStream zip = new ZipInputStream(contentResolver.openInputStream(uri));
BufferedReader reader = new BufferedReader(new InputStreamReader(zip))) {
ZipEntry entry;
while ((entry = zip.getNextEntry()) != null) {
if (entry.isDirectory())
continue;
name = entry.getName();
idx = name.lastIndexOf('/');
if (idx >= 0) {
if (idx >= name.length() - 1)
continue;
name = name.substring(name.lastIndexOf('/') + 1);
}
if (name.toLowerCase(Locale.ENGLISH).endsWith(".conf"))
name = name.substring(0, name.length() - ".conf".length());
else
continue;
Config config = null;
try {
config = Config.parse(reader);
} catch (Exception e) {
throwables.add(e);
}
if (config != null)
futureTunnels.add(Application.getTunnelManager().create(name, config).toCompletableFuture());
}
}
} else {
futureTunnels.add(Application.getTunnelManager().create(name,
Config.parse(contentResolver.openInputStream(uri))).toCompletableFuture());
}
if (futureTunnels.isEmpty()) {
if (throwables.size() == 1)
throw throwables.get(0);
else if (throwables.isEmpty())
throw new IllegalArgumentException(getResources().getString(R.string.no_configs_error));
}
return CompletableFuture.allOf(futureTunnels.toArray(new CompletableFuture[futureTunnels.size()]));
}).whenComplete((future, exception) -> {
if (exception != null) {
onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(exception));
} else {
future.whenComplete((ignored1, ignored2) -> {
final List<Tunnel> tunnels = new ArrayList<>(futureTunnels.size());
for (final CompletableFuture<Tunnel> futureTunnel : futureTunnels) {
Tunnel tunnel = null;
try {
tunnel = futureTunnel.getNow(null);
} catch (final Exception e) {
throwables.add(e);
}
if (tunnel != null)
tunnels.add(tunnel);
}
onTunnelImportFinished(tunnels, throwables);
});
}
});
}
@Override
public void onActivityCreated(@Nullable final Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
final Collection<Integer> checkedItems = savedInstanceState.getIntegerArrayList("CHECKED_ITEMS");
if (checkedItems != null) {
for (final Integer i : checkedItems)
actionModeListener.setItemChecked(i, true);
}
}
}
@Override
public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
switch (requestCode) {
case REQUEST_IMPORT:
if (resultCode == Activity.RESULT_OK && data != null)
importTunnel(data.getData());
return;
case IntentIntegrator.REQUEST_CODE:
final IntentResult result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
if (result != null && result.getContents() != null) {
importTunnel(result.getContents());
}
return;
default:
super.onActivityResult(requestCode, resultCode, data);
}
}
@SuppressWarnings("deprecation")
@SuppressLint("ClickableViewAccessibility")
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
binding = TunnelListFragmentBinding.inflate(inflater, container, false);
binding.tunnelList.setOnTouchListener((view, motionEvent) -> {
if (binding != null) {
binding.createMenu.collapse();
}
return false;
});
binding.tunnelList.setOnScrollListener(new FloatingActionsMenuRecyclerViewScrollListener(binding.createMenu));
binding.executePendingBindings();
return binding.getRoot();
}
@Override
public void onDestroyView() {
binding = null;
super.onDestroyView();
}
@Override
public void onPause() {
if (binding != null) {
binding.createMenu.collapse();
}
super.onPause();
}
public void onRequestCreateConfig(@SuppressWarnings("unused") final View view) {
startActivity(new Intent(getActivity(), TunnelCreatorActivity.class));
if (binding != null)
binding.createMenu.collapse();
}
public void onRequestImportConfig(@SuppressWarnings("unused") final View view) {
final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
startActivityForResult(intent, REQUEST_IMPORT);
if (binding != null)
binding.createMenu.collapse();
}
public void onRequestScanQRCode(@SuppressWarnings("unused") final View view) {
final IntentIntegrator intentIntegrator = IntentIntegrator.forSupportFragment(this);
intentIntegrator.setOrientationLocked(false);
intentIntegrator.setBeepEnabled(false);
intentIntegrator.setPrompt(getString(R.string.qr_code_hint));
intentIntegrator.initiateScan(Collections.singletonList(IntentIntegrator.QR_CODE));
if (binding != null)
binding.createMenu.collapse();
}
@Override
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putIntegerArrayList("CHECKED_ITEMS", actionModeListener.getCheckedItems());
}
@Override
public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
if (binding == null)
return;
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
if (newTunnel != null)
viewForTunnel(newTunnel, tunnels).setSingleSelected(true);
if (oldTunnel != null)
viewForTunnel(oldTunnel, tunnels).setSingleSelected(false);
});
}
private void onTunnelDeletionFinished(final Integer count, @Nullable final Throwable throwable) {
final String message;
if (throwable == null) {
message = getResources().getQuantityString(R.plurals.delete_success, count, count);
} else {
final String error = ErrorMessages.get(throwable);
message = getResources().getQuantityString(R.plurals.delete_error, count, count, error);
Log.e(TAG, message, throwable);
}
if (binding != null) {
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
}
}
private void onTunnelImportFinished(final List<Tunnel> tunnels, final Collection<Throwable> throwables) {
String message = null;
for (final Throwable throwable : throwables) {
final String error = ErrorMessages.get(throwable);
message = getString(R.string.import_error, error);
Log.e(TAG, message, throwable);
}
if (tunnels.size() == 1 && throwables.isEmpty())
message = getString(R.string.import_success, tunnels.get(0).getName());
else if (tunnels.isEmpty() && throwables.size() == 1)
/* Use the exception message from above. */ ;
else if (throwables.isEmpty())
message = getResources().getQuantityString(R.plurals.import_total_success,
tunnels.size(), tunnels.size());
else if (!throwables.isEmpty())
message = getResources().getQuantityString(R.plurals.import_partial_success,
tunnels.size() + throwables.size(),
tunnels.size(), tunnels.size() + throwables.size());
if (binding != null)
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
}
@Override
public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if (binding == null) {
return;
}
binding.setFragment(this);
Application.getTunnelManager().getTunnels().thenAccept(binding::setTunnels);
binding.setRowConfigurationHandler((ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TunnelListItemBinding, Tunnel>) (binding, tunnel, position) -> {
binding.setFragment(this);
binding.getRoot().setOnClickListener(clicked -> {
if (actionMode == null) {
setSelectedTunnel(tunnel);
} else {
actionModeListener.toggleItemChecked(position);
}
});
binding.getRoot().setOnLongClickListener(clicked -> {
actionModeListener.toggleItemChecked(position);
return true;
});
if (actionMode != null)
((MultiselectableRelativeLayout) binding.getRoot()).setMultiSelected(actionModeListener.checkedItems.contains(position));
else
((MultiselectableRelativeLayout) binding.getRoot()).setSingleSelected(getSelectedTunnel() == tunnel);
});
}
private MultiselectableRelativeLayout viewForTunnel(final Tunnel tunnel, final List tunnels) {
return (MultiselectableRelativeLayout) binding.tunnelList.findViewHolderForAdapterPosition(tunnels.indexOf(tunnel)).itemView;
}
private final class ActionModeListener implements ActionMode.Callback {
private final Collection<Integer> checkedItems = new HashSet<>();
@Nullable private Resources resources;
public ArrayList<Integer> getCheckedItems() {
return new ArrayList<>(checkedItems);
}
@Override
public boolean onActionItemClicked(final ActionMode mode, final MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_action_delete:
final Iterable<Integer> copyCheckedItems = new HashSet<>(checkedItems);
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
final Collection<Tunnel> tunnelsToDelete = new ArrayList<>();
for (final Integer position : copyCheckedItems)
tunnelsToDelete.add(tunnels.get(position));
final CompletableFuture[] futures = StreamSupport.stream(tunnelsToDelete)
.map(Tunnel::delete)
.toArray(CompletableFuture[]::new);
CompletableFuture.allOf(futures)
.thenApply(x -> futures.length)
.whenComplete(TunnelListFragment.this::onTunnelDeletionFinished);
});
checkedItems.clear();
mode.finish();
return true;
case R.id.menu_action_select_all:
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
for (int i = 0; i < tunnels.size(); ++i) {
setItemChecked(i, true);
}
});
return true;
default:
return false;
}
}
@Override
public boolean onCreateActionMode(final ActionMode mode, final Menu menu) {
actionMode = mode;
if (getActivity() != null) {
resources = getActivity().getResources();
}
mode.getMenuInflater().inflate(R.menu.tunnel_list_action_mode, menu);
binding.tunnelList.getAdapter().notifyDataSetChanged();
return true;
}
@Override
public void onDestroyActionMode(final ActionMode mode) {
actionMode = null;
resources = null;
checkedItems.clear();
binding.tunnelList.getAdapter().notifyDataSetChanged();
}
@Override
public boolean onPrepareActionMode(final ActionMode mode, final Menu menu) {
updateTitle(mode);
return false;
}
void setItemChecked(final int position, final boolean checked) {
if (checked) {
checkedItems.add(position);
} else {
checkedItems.remove(position);
}
final RecyclerView.Adapter adapter = binding == null ? null : binding.tunnelList.getAdapter();
if (actionMode == null && !checkedItems.isEmpty() && getActivity() != null) {
((AppCompatActivity) getActivity()).startSupportActionMode(this);
} else if (actionMode != null && checkedItems.isEmpty()) {
actionMode.finish();
}
if (adapter != null)
adapter.notifyItemChanged(position);
updateTitle(actionMode);
}
void toggleItemChecked(final int position) {
setItemChecked(position, !checkedItems.contains(position));
}
private void updateTitle(@Nullable final ActionMode mode) {
if (mode == null) {
return;
}
final int count = checkedItems.size();
if (count == 0) {
mode.setTitle("");
} else {
mode.setTitle(resources.getQuantityString(R.plurals.delete_title, count, count));
}
}
}
}
@@ -1,54 +0,0 @@
/*
* 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 android.graphics.drawable.Drawable;
import com.wireguard.android.BR;
import com.wireguard.util.Keyed;
public class ApplicationData extends BaseObservable implements Keyed<String> {
private final Drawable icon;
private final String name;
private final String packageName;
private boolean excludedFromTunnel;
public ApplicationData(final Drawable icon, final String name, final String packageName, final boolean excludedFromTunnel) {
this.icon = icon;
this.name = name;
this.packageName = packageName;
this.excludedFromTunnel = excludedFromTunnel;
}
public Drawable getIcon() {
return icon;
}
@Override
public String getKey() {
return name;
}
public String getName() {
return name;
}
public String getPackageName() {
return packageName;
}
@Bindable
public boolean isExcludedFromTunnel() {
return excludedFromTunnel;
}
public void setExcludedFromTunnel(final boolean excludedFromTunnel) {
this.excludedFromTunnel = excludedFromTunnel;
notifyPropertyChanged(BR.excludedFromTunnel);
}
}
@@ -1,205 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.model;
import android.os.SystemClock;
import android.util.Pair;
import androidx.databinding.BaseObservable;
import androidx.databinding.Bindable;
import androidx.annotation.Nullable;
import com.wireguard.android.BR;
import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.config.Config;
import com.wireguard.crypto.Key;
import com.wireguard.util.Keyed;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
import java9.util.concurrent.CompletableFuture;
import java9.util.concurrent.CompletionStage;
/**
* Encapsulates the volatile and nonvolatile state of a WireGuard tunnel.
*/
public class Tunnel extends BaseObservable implements Keyed<String> {
public static final int NAME_MAX_LENGTH = 15;
private static final Pattern NAME_PATTERN = Pattern.compile("[a-zA-Z0-9_=+.-]{1,15}");
private final TunnelManager manager;
@Nullable private Config config;
private String name;
private State state;
@Nullable private Statistics statistics;
Tunnel(final TunnelManager manager, final String name,
@Nullable final Config config, final State state) {
this.manager = manager;
this.name = name;
this.config = config;
this.state = state;
}
public static boolean isNameInvalid(final CharSequence name) {
return !NAME_PATTERN.matcher(name).matches();
}
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;
}
@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;
}
public String onNameChanged(final String name) {
this.name = name;
notifyPropertyChanged(BR.name);
return name;
}
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);
}
public enum State {
DOWN,
TOGGLE,
UP;
public static State of(final boolean running) {
return running ? UP : DOWN;
}
}
public static class Statistics extends BaseObservable {
private long lastTouched = SystemClock.elapsedRealtime();
private final Map<Key, Pair<Long, Long>> peerBytes = new HashMap<>();
public void add(final Key key, final long rx, final long tx) {
peerBytes.put(key, Pair.create(rx, tx));
lastTouched = SystemClock.elapsedRealtime();
}
private boolean isStale() {
return SystemClock.elapsedRealtime() - lastTouched > 900;
}
public Key[] peers() {
return peerBytes.keySet().toArray(new Key[0]);
}
public long peerRx(final Key peer) {
if (!peerBytes.containsKey(peer))
return 0;
return peerBytes.get(peer).first;
}
public long peerTx(final Key peer) {
if (!peerBytes.containsKey(peer))
return 0;
return peerBytes.get(peer).second;
}
public long totalRx() {
long rx = 0;
for (final Pair<Long, Long> val : peerBytes.values()) {
rx += val.first;
}
return rx;
}
public long totalTx() {
long tx = 0;
for (final Pair<Long, Long> val : peerBytes.values()) {
tx += val.second;
}
return tx;
}
}
}
@@ -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 androidx.databinding.BaseObservable;
import androidx.databinding.Bindable;
import androidx.annotation.Nullable;
import com.wireguard.android.Application;
import com.wireguard.android.BR;
import com.wireguard.android.R;
import com.wireguard.android.configStore.ConfigStore;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.model.Tunnel.Statistics;
import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.android.util.ObservableSortedKeyedArrayList;
import com.wireguard.android.util.ObservableSortedKeyedList;
import com.wireguard.config.Config;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Set;
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,
*/
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, Tunnel>> 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, Tunnel> tunnels = new ObservableSortedKeyedArrayList<>(COMPARATOR);
private boolean haveLoaded;
@Nullable private Tunnel lastUsedTunnel;
public TunnelManager(final ConfigStore configStore) {
this.configStore = configStore;
}
static CompletionStage<State> getTunnelState(final Tunnel tunnel) {
return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getState(tunnel))
.thenApply(tunnel::onStateChanged);
}
static CompletionStage<Statistics> getTunnelStatistics(final Tunnel tunnel) {
return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getStatistics(tunnel))
.thenApply(tunnel::onStatisticsChanged);
}
private Tunnel addToList(final String name, @Nullable final Config config, final State state) {
final Tunnel tunnel = new Tunnel(this, name, config, state);
tunnels.add(tunnel);
return tunnel;
}
public CompletionStage<Tunnel> 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 Tunnel 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);
try {
configStore.delete(tunnel.getName());
} catch (final Exception e) {
if (originalState == State.UP)
Application.getBackend().setState(tunnel, State.UP);
// 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 Tunnel getLastUsedTunnel() {
return lastUsedTunnel;
}
CompletionStage<Config> getTunnelConfig(final Tunnel tunnel) {
return Application.getAsyncWorker().supplyAsync(() -> configStore.load(tunnel.getName()))
.thenApply(tunnel::onConfigChanged);
}
public CompletableFuture<ObservableSortedKeyedList<String, Tunnel>> getTunnels() {
return completableTunnels;
}
public void onCreate() {
Application.getAsyncWorker().supplyAsync(configStore::enumerate)
.thenAcceptBoth(Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().enumerate()), 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().enumerate())
.thenAccept(running -> {
for (final Tunnel 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(Tunnel::getName)
.collect(Collectors.toUnmodifiableSet());
Application.getSharedPreferences().edit().putStringSet(KEY_RUNNING_TUNNELS, runningTunnels).apply();
}
private void setLastUsedTunnel(@Nullable final Tunnel 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 Tunnel tunnel, final Config config) {
return Application.getAsyncWorker().supplyAsync(() -> {
final Config appliedConfig = Application.getBackend().applyConfig(tunnel, config);
return configStore.save(tunnel.getName(), appliedConfig);
}).thenApply(tunnel::onConfigChanged);
}
CompletionStage<String> setTunnelName(final Tunnel 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);
configStore.rename(tunnel.getName(), name);
final String newName = tunnel.onNameChanged(name);
if (originalState == State.UP)
Application.getBackend().setState(tunnel, State.UP);
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);
});
}
CompletionStage<State> setTunnelState(final Tunnel tunnel, final State state) {
// Ensure the configuration is loaded before trying to use it.
return tunnel.getConfigAsync().thenCompose(x ->
Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().setState(tunnel, state))
).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;
}
/* We disable the below, for now, as the security model of allowing this
* might take a bit more consideration.
*/
if (true)
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 Tunnel tunnel = tunnels.get(tunnelName);
if (tunnel == null)
return;
manager.setTunnelState(tunnel, state);
});
}
}
}
@@ -1,112 +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 androidx.annotation.Nullable;
import com.google.android.material.snackbar.Snackbar;
import androidx.preference.Preference;
import android.util.AttributeSet;
import android.util.Log;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.util.DownloadsFileSaver;
import com.wireguard.android.util.DownloadsFileSaver.DownloadsFile;
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.
*/
public class LogExporterPreference extends Preference {
private static final String TAG = "WireGuard/" + LogExporterPreference.class.getSimpleName();
@Nullable private String exportedFilePath;
public LogExporterPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
private void exportLog() {
Application.getAsyncWorker().supplyAsync(() -> {
DownloadsFile outputFile = DownloadsFileSaver.save(getContext(), "wireguard-log.txt", "text/plain", true);
try {
final Process process = Runtime.getRuntime().exec(new String[]{
"logcat", "-b", "all", "-d", "-v", "threadtime", "*:V"});
try (final BufferedReader stdout = new BufferedReader(new InputStreamReader(process.getInputStream()));
final BufferedReader stderr = new BufferedReader(new InputStreamReader(process.getErrorStream())))
{
String line;
while ((line = stdout.readLine()) != null) {
outputFile.getOutputStream().write(line.getBytes());
outputFile.getOutputStream().write('\n');
}
outputFile.getOutputStream().close();
stdout.close();
if (process.waitFor() != 0) {
final StringBuilder errors = new StringBuilder();
errors.append(R.string.logcat_error);
while ((line = stderr.readLine()) != null)
errors.append(line);
throw new Exception(errors.toString());
}
}
} catch (final Exception e) {
outputFile.delete();
throw e;
}
return outputFile.getFileName();
}).whenComplete(this::exportLogComplete);
}
private void exportLogComplete(final String filePath, @Nullable final Throwable throwable) {
if (throwable != null) {
final String error = ErrorMessages.get(throwable);
final String message = getContext().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();
setEnabled(true);
} else {
exportedFilePath = filePath;
notifyChanged();
}
}
@Override
public CharSequence getSummary() {
return exportedFilePath == null ?
getContext().getString(R.string.log_export_summary) :
getContext().getString(R.string.log_export_success, exportedFilePath);
}
@Override
public CharSequence getTitle() {
return getContext().getString(R.string.log_export_title);
}
@Override
protected void onClick() {
FragmentUtils.getPrefActivity(this).ensurePermissions(
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
(permissions, granted) -> {
if (granted.length > 0 && granted[0] == PackageManager.PERMISSION_GRANTED) {
setEnabled(false);
exportLog();
}
});
}
}
@@ -1,91 +0,0 @@
/*
* Copyright © 2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference;
import android.content.Context;
import android.content.Intent;
import android.system.OsConstants;
import android.util.AttributeSet;
import android.widget.Toast;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.util.ModuleLoader;
import com.wireguard.android.util.ToolsInstaller;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
public class ModuleDownloaderPreference extends Preference {
private State state = State.INITIAL;
public ModuleDownloaderPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
@Override
public CharSequence getSummary() {
return getContext().getString(state.messageResourceId);
}
@Override
public CharSequence getTitle() {
return getContext().getString(R.string.module_installer_title);
}
@Override
protected void onClick() {
setState(State.WORKING);
Application.getAsyncWorker().supplyAsync(Application.getModuleLoader()::download).whenComplete(this::onDownloadResult);
}
private void onDownloadResult(final Integer result, @Nullable final Throwable throwable) {
if (throwable != null) {
setState(State.FAILURE);
Toast.makeText(getContext(), throwable.getMessage(), Toast.LENGTH_LONG).show();
} else if (result == OsConstants.ENOENT)
setState(State.NOTFOUND);
else if (result == OsConstants.EXIT_SUCCESS) {
setState(State.SUCCESS);
Application.getAsyncWorker().runAsync(() -> {
Thread.sleep(1000 * 5);
Intent i = getContext().getPackageManager().getLaunchIntentForPackage(getContext().getPackageName());
if (i == null)
return;
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Application.get().startActivity(i);
System.exit(0);
});
} else
setState(State.FAILURE);
}
private void setState(final State state) {
if (this.state == state)
return;
this.state = state;
if (isEnabled() != state.shouldEnableView)
setEnabled(state.shouldEnableView);
notifyChanged();
}
private enum State {
INITIAL(R.string.module_installer_initial, true),
FAILURE(R.string.module_installer_error, true),
WORKING(R.string.module_installer_working, false),
SUCCESS(R.string.module_installer_success, false),
NOTFOUND(R.string.module_installer_not_found, false);
private final int messageResourceId;
private final boolean shouldEnableView;
State(final int messageResourceId, final boolean shouldEnableView) {
this.messageResourceId = messageResourceId;
this.shouldEnableView = shouldEnableView;
}
}
}
@@ -1,102 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference;
import android.content.Context;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import android.util.AttributeSet;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.util.ToolsInstaller;
/**
* Preference implementing a button that asynchronously runs {@code ToolsInstaller} and displays the
* result as the preference summary.
*/
public class ToolsInstallerPreference extends Preference {
private State state = State.INITIAL;
public ToolsInstallerPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
@Override
public CharSequence getSummary() {
return getContext().getString(state.messageResourceId);
}
@Override
public CharSequence getTitle() {
return getContext().getString(R.string.tools_installer_title);
}
@Override
public void onAttached() {
super.onAttached();
Application.getAsyncWorker().supplyAsync(Application.getToolsInstaller()::areInstalled).whenComplete(this::onCheckResult);
}
private void onCheckResult(final int state, @Nullable final Throwable throwable) {
if (throwable != null || state == ToolsInstaller.ERROR)
setState(State.INITIAL);
else if ((state & ToolsInstaller.YES) == ToolsInstaller.YES)
setState(State.ALREADY);
else if ((state & (ToolsInstaller.MAGISK | ToolsInstaller.NO)) == (ToolsInstaller.MAGISK | ToolsInstaller.NO))
setState(State.INITIAL_MAGISK);
else if ((state & (ToolsInstaller.SYSTEM | ToolsInstaller.NO)) == (ToolsInstaller.SYSTEM | ToolsInstaller.NO))
setState(State.INITIAL_SYSTEM);
else
setState(State.INITIAL);
}
@Override
protected void onClick() {
setState(State.WORKING);
Application.getAsyncWorker().supplyAsync(Application.getToolsInstaller()::install).whenComplete(this::onInstallResult);
}
private void onInstallResult(final Integer result, @Nullable final Throwable throwable) {
if (throwable != null)
setState(State.FAILURE);
else if ((result & (ToolsInstaller.YES | ToolsInstaller.MAGISK)) == (ToolsInstaller.YES | ToolsInstaller.MAGISK))
setState(State.SUCCESS_MAGISK);
else if ((result & (ToolsInstaller.YES | ToolsInstaller.SYSTEM)) == (ToolsInstaller.YES | ToolsInstaller.SYSTEM))
setState(State.SUCCESS_SYSTEM);
else
setState(State.FAILURE);
}
private void setState(final State state) {
if (this.state == state)
return;
this.state = state;
if (isEnabled() != state.shouldEnableView)
setEnabled(state.shouldEnableView);
notifyChanged();
}
private enum State {
INITIAL(R.string.tools_installer_initial, true),
ALREADY(R.string.tools_installer_already, false),
FAILURE(R.string.tools_installer_failure, true),
WORKING(R.string.tools_installer_working, false),
INITIAL_SYSTEM(R.string.tools_installer_initial_system, true),
SUCCESS_SYSTEM(R.string.tools_installer_success_system, false),
INITIAL_MAGISK(R.string.tools_installer_initial_magisk, true),
SUCCESS_MAGISK(R.string.tools_installer_success_magisk, false);
private final int messageResourceId;
private final boolean shouldEnableView;
State(final int messageResourceId, final boolean shouldEnableView) {
this.messageResourceId = messageResourceId;
this.shouldEnableView = shouldEnableView;
}
}
}
@@ -1,60 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import android.util.AttributeSet;
import com.wireguard.android.Application;
import com.wireguard.android.BuildConfig;
import com.wireguard.android.R;
import java.util.Locale;
public class VersionPreference extends Preference {
@Nullable private String versionSummary;
public VersionPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
Application.getBackendAsync().thenAccept(backend -> {
versionSummary = getContext().getString(R.string.version_summary_checking, backend.getTypePrettyName().toLowerCase(Locale.ENGLISH));
Application.getAsyncWorker().supplyAsync(backend::getVersion).whenComplete((version, exception) -> {
versionSummary = exception == null
? getContext().getString(R.string.version_summary, backend.getTypePrettyName(), version)
: getContext().getString(R.string.version_summary_unknown, backend.getTypePrettyName().toLowerCase(Locale.ENGLISH));
notifyChanged();
});
});
}
@Nullable
@Override
public CharSequence getSummary() {
return versionSummary;
}
@Override
public CharSequence getTitle() {
return getContext().getString(R.string.version_title, BuildConfig.VERSION_NAME);
}
@Override
protected void onClick() {
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://www.wireguard.com/"));
try {
getContext().startActivity(intent);
} catch (final ActivityNotFoundException ignored) {
}
}
}
@@ -1,119 +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 androidx.annotation.Nullable;
import com.google.android.material.snackbar.Snackbar;
import androidx.preference.Preference;
import android.util.AttributeSet;
import android.util.Log;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.util.DownloadsFileSaver;
import com.wireguard.android.util.DownloadsFileSaver.DownloadsFile;
import com.wireguard.android.util.ErrorMessages;
import com.wireguard.android.util.FragmentUtils;
import com.wireguard.config.Config;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import java9.util.concurrent.CompletableFuture;
/**
* Preference implementing a button that asynchronously exports config zips.
*/
public class ZipExporterPreference extends Preference {
private static final String TAG = "WireGuard/" + ZipExporterPreference.class.getSimpleName();
@Nullable private String exportedFilePath;
public ZipExporterPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
private void exportZip() {
Application.getTunnelManager().getTunnels().thenAccept(this::exportZip);
}
private void exportZip(final List<Tunnel> tunnels) {
final List<CompletableFuture<Config>> futureConfigs = new ArrayList<>(tunnels.size());
for (final Tunnel tunnel : tunnels)
futureConfigs.add(tunnel.getConfigAsync().toCompletableFuture());
if (futureConfigs.isEmpty()) {
exportZipComplete(null, new IllegalArgumentException(
getContext().getString(R.string.no_tunnels_error)));
return;
}
CompletableFuture.allOf(futureConfigs.toArray(new CompletableFuture[futureConfigs.size()]))
.whenComplete((ignored1, exception) -> Application.getAsyncWorker().supplyAsync(() -> {
if (exception != null)
throw exception;
DownloadsFile outputFile = DownloadsFileSaver.save(getContext(), "wireguard-export.zip", "application/zip", true);
try (ZipOutputStream zip = new ZipOutputStream(outputFile.getOutputStream())) {
for (int i = 0; i < futureConfigs.size(); ++i) {
zip.putNextEntry(new ZipEntry(tunnels.get(i).getName() + ".conf"));
zip.write(futureConfigs.get(i).getNow(null).
toWgQuickString().getBytes(StandardCharsets.UTF_8));
}
zip.closeEntry();
} catch (final Exception e) {
outputFile.delete();
throw e;
}
return outputFile.getFileName();
}).whenComplete(this::exportZipComplete));
}
private void exportZipComplete(@Nullable final String filePath, @Nullable final Throwable throwable) {
if (throwable != null) {
final String error = ErrorMessages.get(throwable);
final String message = getContext().getString(R.string.zip_export_error, error);
Log.e(TAG, message, throwable);
Snackbar.make(
FragmentUtils.getPrefActivity(this).findViewById(android.R.id.content),
message, Snackbar.LENGTH_LONG).show();
setEnabled(true);
} else {
exportedFilePath = filePath;
notifyChanged();
}
}
@Override
public CharSequence getSummary() {
return exportedFilePath == null ?
getContext().getString(R.string.zip_export_summary) :
getContext().getString(R.string.zip_export_success, exportedFilePath);
}
@Override
public CharSequence getTitle() {
return getContext().getString(R.string.zip_export_title);
}
@Override
protected void onClick() {
FragmentUtils.getPrefActivity(this).ensurePermissions(
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
(permissions, granted) -> {
if (granted.length > 0 && granted[0] == PackageManager.PERMISSION_GRANTED) {
setEnabled(false);
exportZip();
}
});
}
}
@@ -1,63 +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 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.
*/
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;
}
}
@@ -1,37 +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 com.google.android.material.snackbar.Snackbar;
import android.view.View;
import android.widget.TextView;
/**
* Standalone utilities for interacting with the system clipboard.
*/
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();
}
}
@@ -1,98 +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 java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class DownloadsFileSaver {
public static class DownloadsFile {
private Context context;
private OutputStream outputStream;
private String fileName;
private 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 OutputStream getOutputStream() { return outputStream; }
public String getFileName() { return fileName; }
public void delete() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
context.getContentResolver().delete(uri, null, null);
else
new File(fileName).delete();
}
}
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);
}
}
}
@@ -1,130 +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 androidx.annotation.Nullable;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.BadConfigException.Location;
import com.wireguard.config.BadConfigException.Reason;
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 java.net.InetAddress;
import java.util.EnumMap;
import java.util.Map;
import java9.util.Maps;
public final class ErrorMessages {
private static final Map<Reason, Integer> BCE_REASON_MAP = new EnumMap<>(Maps.of(
Reason.INVALID_KEY, R.string.bad_config_reason_invalid_key,
Reason.INVALID_NUMBER, R.string.bad_config_reason_invalid_number,
Reason.INVALID_VALUE, R.string.bad_config_reason_invalid_value,
Reason.MISSING_ATTRIBUTE, R.string.bad_config_reason_missing_attribute,
Reason.MISSING_SECTION, R.string.bad_config_reason_missing_section,
Reason.MISSING_VALUE, R.string.bad_config_reason_missing_value,
Reason.SYNTAX_ERROR, R.string.bad_config_reason_syntax_error,
Reason.UNKNOWN_ATTRIBUTE, R.string.bad_config_reason_unknown_attribute,
Reason.UNKNOWN_SECTION, R.string.bad_config_reason_unknown_section
));
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 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.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)
break;
cause = cause.getCause();
}
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 androidx.annotation.Nullable;
import android.util.Log;
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");
}
}
@@ -1,27 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.content.Context;
import androidx.preference.Preference;
import android.view.ContextThemeWrapper;
import com.wireguard.android.activity.SettingsActivity;
public final class FragmentUtils {
private FragmentUtils() {
// Prevent instantiation
}
public static SettingsActivity getPrefActivity(final Preference preference) {
final Context context = preference.getContext();
if (context instanceof ContextThemeWrapper) {
if (context instanceof SettingsActivity) {
return ((SettingsActivity) context);
}
}
return null;
}
}
@@ -1,109 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import androidx.databinding.ObservableArrayList;
import androidx.annotation.Nullable;
import com.wireguard.util.Keyed;
import java.util.Collection;
import java.util.ListIterator;
import java.util.Objects;
/**
* 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.
*/
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,19 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import androidx.databinding.ObservableList;
import com.wireguard.util.Keyed;
import com.wireguard.util.KeyedList;
/**
* A list that is both keyed and observable.
*/
public interface ObservableKeyedList<K, E extends Keyed<? extends K>>
extends KeyedList<K, E>, ObservableList<E> {
}
@@ -1,198 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import androidx.annotation.Nullable;
import com.wireguard.util.Keyed;
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;
/**
* 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.
*/
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,17 +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.SortedKeyedList;
/**
* A list that is both sorted/keyed and observable.
*/
public interface ObservableSortedKeyedList<K, E extends Keyed<? extends K>>
extends ObservableKeyedList<K, E>, SortedKeyedList<K, E> {
}
@@ -1,93 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.viewmodel;
import androidx.databinding.ObservableArrayList;
import androidx.databinding.ObservableList;
import android.os.Parcel;
import android.os.Parcelable;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.Config;
import com.wireguard.config.Peer;
import java.util.ArrayList;
import java.util.Collection;
public class ConfigProxy implements Parcelable {
public static final Parcelable.Creator<ConfigProxy> CREATOR = new ConfigProxyCreator();
private final InterfaceProxy interfaze;
private final ObservableList<PeerProxy> peers = new ObservableArrayList<>();
private ConfigProxy(final Parcel in) {
interfaze = in.readParcelable(InterfaceProxy.class.getClassLoader());
in.readTypedList(peers, PeerProxy.CREATOR);
for (final PeerProxy proxy : peers)
proxy.bind(this);
}
public ConfigProxy(final Config other) {
interfaze = new InterfaceProxy(other.getInterface());
for (final Peer peer : other.getPeers()) {
final PeerProxy proxy = new PeerProxy(peer);
peers.add(proxy);
proxy.bind(this);
}
}
public ConfigProxy() {
interfaze = new InterfaceProxy();
}
public PeerProxy addPeer() {
final PeerProxy proxy = new PeerProxy();
peers.add(proxy);
proxy.bind(this);
return proxy;
}
@Override
public int describeContents() {
return 0;
}
public InterfaceProxy getInterface() {
return interfaze;
}
public ObservableList<PeerProxy> getPeers() {
return peers;
}
public Config resolve() throws BadConfigException {
final Collection<Peer> resolvedPeers = new ArrayList<>();
for (final PeerProxy proxy : peers)
resolvedPeers.add(proxy.resolve());
return new Config.Builder()
.setInterface(interfaze.resolve())
.addPeers(resolvedPeers)
.build();
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeParcelable(interfaze, flags);
dest.writeTypedList(peers);
}
private static class ConfigProxyCreator implements Parcelable.Creator<ConfigProxy> {
@Override
public ConfigProxy createFromParcel(final Parcel in) {
return new ConfigProxy(in);
}
@Override
public ConfigProxy[] newArray(final int size) {
return new ConfigProxy[size];
}
}
}
@@ -1,190 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.viewmodel;
import androidx.databinding.BaseObservable;
import androidx.databinding.Bindable;
import androidx.databinding.ObservableArrayList;
import androidx.databinding.ObservableList;
import android.os.Parcel;
import android.os.Parcelable;
import com.wireguard.android.BR;
import com.wireguard.config.Attribute;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.Interface;
import com.wireguard.crypto.Key;
import com.wireguard.crypto.KeyFormatException;
import com.wireguard.crypto.KeyPair;
import java.net.InetAddress;
import java.util.List;
import java9.util.stream.Collectors;
import java9.util.stream.StreamSupport;
public class InterfaceProxy extends BaseObservable implements Parcelable {
public static final Parcelable.Creator<InterfaceProxy> CREATOR = new InterfaceProxyCreator();
private final ObservableList<String> excludedApplications = new ObservableArrayList<>();
private String addresses;
private String dnsServers;
private String listenPort;
private String mtu;
private String privateKey;
private String publicKey;
private InterfaceProxy(final Parcel in) {
addresses = in.readString();
dnsServers = in.readString();
in.readStringList(excludedApplications);
listenPort = in.readString();
mtu = in.readString();
privateKey = in.readString();
publicKey = in.readString();
}
public InterfaceProxy(final Interface other) {
addresses = Attribute.join(other.getAddresses());
final List<String> dnsServerStrings = StreamSupport.stream(other.getDnsServers())
.map(InetAddress::getHostAddress)
.collect(Collectors.toUnmodifiableList());
dnsServers = Attribute.join(dnsServerStrings);
excludedApplications.addAll(other.getExcludedApplications());
listenPort = other.getListenPort().map(String::valueOf).orElse("");
mtu = other.getMtu().map(String::valueOf).orElse("");
final KeyPair keyPair = other.getKeyPair();
privateKey = keyPair.getPrivateKey().toBase64();
publicKey = keyPair.getPublicKey().toBase64();
}
public InterfaceProxy() {
addresses = "";
dnsServers = "";
listenPort = "";
mtu = "";
privateKey = "";
publicKey = "";
}
@Override
public int describeContents() {
return 0;
}
public void generateKeyPair() {
final KeyPair keyPair = new KeyPair();
privateKey = keyPair.getPrivateKey().toBase64();
publicKey = keyPair.getPublicKey().toBase64();
notifyPropertyChanged(BR.privateKey);
notifyPropertyChanged(BR.publicKey);
}
@Bindable
public String getAddresses() {
return addresses;
}
@Bindable
public String getDnsServers() {
return dnsServers;
}
public ObservableList<String> getExcludedApplications() {
return excludedApplications;
}
@Bindable
public String getListenPort() {
return listenPort;
}
@Bindable
public String getMtu() {
return mtu;
}
@Bindable
public String getPrivateKey() {
return privateKey;
}
@Bindable
public String getPublicKey() {
return publicKey;
}
public Interface resolve() throws BadConfigException {
final Interface.Builder builder = new Interface.Builder();
if (!addresses.isEmpty())
builder.parseAddresses(addresses);
if (!dnsServers.isEmpty())
builder.parseDnsServers(dnsServers);
if (!excludedApplications.isEmpty())
builder.excludeApplications(excludedApplications);
if (!listenPort.isEmpty())
builder.parseListenPort(listenPort);
if (!mtu.isEmpty())
builder.parseMtu(mtu);
if (!privateKey.isEmpty())
builder.parsePrivateKey(privateKey);
return builder.build();
}
public void setAddresses(final String addresses) {
this.addresses = addresses;
notifyPropertyChanged(BR.addresses);
}
public void setDnsServers(final String dnsServers) {
this.dnsServers = dnsServers;
notifyPropertyChanged(BR.dnsServers);
}
public void setListenPort(final String listenPort) {
this.listenPort = listenPort;
notifyPropertyChanged(BR.listenPort);
}
public void setMtu(final String mtu) {
this.mtu = mtu;
notifyPropertyChanged(BR.mtu);
}
public void setPrivateKey(final String privateKey) {
this.privateKey = privateKey;
try {
publicKey = new KeyPair(Key.fromBase64(privateKey)).getPublicKey().toBase64();
} catch (final KeyFormatException ignored) {
publicKey = "";
}
notifyPropertyChanged(BR.privateKey);
notifyPropertyChanged(BR.publicKey);
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeString(addresses);
dest.writeString(dnsServers);
dest.writeStringList(excludedApplications);
dest.writeString(listenPort);
dest.writeString(mtu);
dest.writeString(privateKey);
dest.writeString(publicKey);
}
private static class InterfaceProxyCreator implements Parcelable.Creator<InterfaceProxy> {
@Override
public InterfaceProxy createFromParcel(final Parcel in) {
return new InterfaceProxy(in);
}
@Override
public InterfaceProxy[] newArray(final int size) {
return new InterfaceProxy[size];
}
}
}
@@ -1,380 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.viewmodel;
import androidx.databinding.BaseObservable;
import androidx.databinding.Bindable;
import androidx.databinding.Observable;
import androidx.databinding.ObservableList;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import com.wireguard.android.BR;
import com.wireguard.config.Attribute;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.InetEndpoint;
import com.wireguard.config.Peer;
import com.wireguard.crypto.Key;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java9.util.Lists;
import java9.util.Sets;
import java9.util.stream.Collectors;
import java9.util.stream.Stream;
public class PeerProxy extends BaseObservable implements Parcelable {
public static final Parcelable.Creator<PeerProxy> CREATOR = new PeerProxyCreator();
private static final Set<String> IPV4_PUBLIC_NETWORKS = new LinkedHashSet<>(Lists.of(
"0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
"64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
"172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
"176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
"192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
"193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
));
private static final Set<String> IPV4_WILDCARD = Sets.of("0.0.0.0/0");
private final List<String> dnsRoutes = new ArrayList<>();
private String allowedIps;
private AllowedIpsState allowedIpsState = AllowedIpsState.INVALID;
private String endpoint;
@Nullable private InterfaceDnsListener interfaceDnsListener;
@Nullable private ConfigProxy owner;
@Nullable private PeerListListener peerListListener;
private String persistentKeepalive;
private String preSharedKey;
private String publicKey;
private int totalPeers;
private PeerProxy(final Parcel in) {
allowedIps = in.readString();
endpoint = in.readString();
persistentKeepalive = in.readString();
preSharedKey = in.readString();
publicKey = in.readString();
}
public PeerProxy(final Peer other) {
allowedIps = Attribute.join(other.getAllowedIps());
endpoint = other.getEndpoint().map(InetEndpoint::toString).orElse("");
persistentKeepalive = other.getPersistentKeepalive().map(String::valueOf).orElse("");
preSharedKey = other.getPreSharedKey().map(Key::toBase64).orElse("");
publicKey = other.getPublicKey().toBase64();
}
public PeerProxy() {
allowedIps = "";
endpoint = "";
persistentKeepalive = "";
preSharedKey = "";
publicKey = "";
}
public void bind(final ConfigProxy owner) {
final InterfaceProxy interfaze = owner.getInterface();
final ObservableList<PeerProxy> peers = owner.getPeers();
if (interfaceDnsListener == null)
interfaceDnsListener = new InterfaceDnsListener(this);
interfaze.addOnPropertyChangedCallback(interfaceDnsListener);
setInterfaceDns(interfaze.getDnsServers());
if (peerListListener == null)
peerListListener = new PeerListListener(this);
peers.addOnListChangedCallback(peerListListener);
setTotalPeers(peers.size());
this.owner = owner;
}
private void calculateAllowedIpsState() {
final AllowedIpsState newState;
if (totalPeers == 1) {
// String comparison works because we only care if allowedIps is a superset of one of
// the above sets of (valid) *networks*. We are not checking for a superset based on
// the individual addresses in each set.
final Collection<String> networkStrings = getAllowedIpsSet();
// If allowedIps contains both the wildcard and the public networks, then private
// networks aren't excluded!
if (networkStrings.containsAll(IPV4_WILDCARD))
newState = AllowedIpsState.CONTAINS_IPV4_WILDCARD;
else if (networkStrings.containsAll(IPV4_PUBLIC_NETWORKS))
newState = AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS;
else
newState = AllowedIpsState.OTHER;
} else {
newState = AllowedIpsState.INVALID;
}
if (newState != allowedIpsState) {
allowedIpsState = newState;
notifyPropertyChanged(BR.ableToExcludePrivateIps);
notifyPropertyChanged(BR.excludingPrivateIps);
}
}
@Override
public int describeContents() {
return 0;
}
@Bindable
public String getAllowedIps() {
return allowedIps;
}
private Set<String> getAllowedIpsSet() {
return new LinkedHashSet<>(Lists.of(Attribute.split(allowedIps)));
}
@Bindable
public String getEndpoint() {
return endpoint;
}
@Bindable
public String getPersistentKeepalive() {
return persistentKeepalive;
}
@Bindable
public String getPreSharedKey() {
return preSharedKey;
}
@Bindable
public String getPublicKey() {
return publicKey;
}
@Bindable
public boolean isAbleToExcludePrivateIps() {
return allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS
|| allowedIpsState == AllowedIpsState.CONTAINS_IPV4_WILDCARD;
}
@Bindable
public boolean isExcludingPrivateIps() {
return allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS;
}
public Peer resolve() throws BadConfigException {
final Peer.Builder builder = new Peer.Builder();
if (!allowedIps.isEmpty())
builder.parseAllowedIPs(allowedIps);
if (!endpoint.isEmpty())
builder.parseEndpoint(endpoint);
if (!persistentKeepalive.isEmpty())
builder.parsePersistentKeepalive(persistentKeepalive);
if (!preSharedKey.isEmpty())
builder.parsePreSharedKey(preSharedKey);
if (!publicKey.isEmpty())
builder.parsePublicKey(publicKey);
return builder.build();
}
public void setAllowedIps(final String allowedIps) {
this.allowedIps = allowedIps;
notifyPropertyChanged(BR.allowedIps);
calculateAllowedIpsState();
}
public void setEndpoint(final String endpoint) {
this.endpoint = endpoint;
notifyPropertyChanged(BR.endpoint);
}
public void setExcludingPrivateIps(final boolean excludingPrivateIps) {
if (!isAbleToExcludePrivateIps() || isExcludingPrivateIps() == excludingPrivateIps)
return;
final Set<String> oldNetworks = excludingPrivateIps ? IPV4_WILDCARD : IPV4_PUBLIC_NETWORKS;
final Set<String> newNetworks = excludingPrivateIps ? IPV4_PUBLIC_NETWORKS : IPV4_WILDCARD;
final Collection<String> input = getAllowedIpsSet();
final int outputSize = input.size() - oldNetworks.size() + newNetworks.size();
final Collection<String> output = new LinkedHashSet<>(outputSize);
boolean replaced = false;
// Replace the first instance of the wildcard with the public network list, or vice versa.
for (final String network : input) {
if (oldNetworks.contains(network)) {
if (!replaced) {
for (final String replacement : newNetworks)
if (!output.contains(replacement))
output.add(replacement);
replaced = true;
}
} else if (!output.contains(network)) {
output.add(network);
}
}
// DNS servers only need to handled specially when we're excluding private IPs.
if (excludingPrivateIps)
output.addAll(dnsRoutes);
else
output.removeAll(dnsRoutes);
allowedIps = Attribute.join(output);
allowedIpsState = excludingPrivateIps ?
AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS : AllowedIpsState.CONTAINS_IPV4_WILDCARD;
notifyPropertyChanged(BR.allowedIps);
notifyPropertyChanged(BR.excludingPrivateIps);
}
private void setInterfaceDns(final CharSequence dnsServers) {
final List<String> newDnsRoutes = Stream.of(Attribute.split(dnsServers))
.filter(server -> !server.contains(":"))
.map(server -> server + "/32")
.collect(Collectors.toUnmodifiableList());
if (allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS) {
final Collection<String> input = getAllowedIpsSet();
final Collection<String> output = new LinkedHashSet<>(input.size() + 1);
// Yes, this is quadratic in the number of DNS servers, but most users have 1 or 2.
for (final String network : input)
if (!dnsRoutes.contains(network) || newDnsRoutes.contains(network))
output.add(network);
// Since output is a Set, this does the Right Thing™ (it does not duplicate networks).
output.addAll(newDnsRoutes);
// None of the public networks are /32s, so this cannot change the AllowedIPs state.
allowedIps = Attribute.join(output);
notifyPropertyChanged(BR.allowedIps);
}
dnsRoutes.clear();
dnsRoutes.addAll(newDnsRoutes);
}
public void setPersistentKeepalive(final String persistentKeepalive) {
this.persistentKeepalive = persistentKeepalive;
notifyPropertyChanged(BR.persistentKeepalive);
}
public void setPreSharedKey(final String preSharedKey) {
this.preSharedKey = preSharedKey;
notifyPropertyChanged(BR.preSharedKey);
}
public void setPublicKey(final String publicKey) {
this.publicKey = publicKey;
notifyPropertyChanged(BR.publicKey);
}
private void setTotalPeers(final int totalPeers) {
if (this.totalPeers == totalPeers)
return;
this.totalPeers = totalPeers;
calculateAllowedIpsState();
}
public void unbind() {
if (owner == null)
return;
final InterfaceProxy interfaze = owner.getInterface();
final ObservableList<PeerProxy> peers = owner.getPeers();
if (interfaceDnsListener != null)
interfaze.removeOnPropertyChangedCallback(interfaceDnsListener);
if (peerListListener != null)
peers.removeOnListChangedCallback(peerListListener);
peers.remove(this);
setInterfaceDns("");
setTotalPeers(0);
owner = null;
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeString(allowedIps);
dest.writeString(endpoint);
dest.writeString(persistentKeepalive);
dest.writeString(preSharedKey);
dest.writeString(publicKey);
}
private enum AllowedIpsState {
CONTAINS_IPV4_PUBLIC_NETWORKS,
CONTAINS_IPV4_WILDCARD,
INVALID,
OTHER
}
private static final class InterfaceDnsListener extends Observable.OnPropertyChangedCallback {
private final WeakReference<PeerProxy> weakPeerProxy;
private InterfaceDnsListener(final PeerProxy peerProxy) {
weakPeerProxy = new WeakReference<>(peerProxy);
}
@Override
public void onPropertyChanged(final Observable sender, final int propertyId) {
@Nullable final PeerProxy peerProxy = weakPeerProxy.get();
if (peerProxy == null) {
sender.removeOnPropertyChangedCallback(this);
return;
}
// This shouldn't be possible, but try to avoid a ClassCastException anyway.
if (!(sender instanceof InterfaceProxy))
return;
if (!(propertyId == BR._all || propertyId == BR.dnsServers))
return;
peerProxy.setInterfaceDns(((InterfaceProxy) sender).getDnsServers());
}
}
private static final class PeerListListener
extends ObservableList.OnListChangedCallback<ObservableList<PeerProxy>> {
private final WeakReference<PeerProxy> weakPeerProxy;
private PeerListListener(final PeerProxy peerProxy) {
weakPeerProxy = new WeakReference<>(peerProxy);
}
@Override
public void onChanged(final ObservableList<PeerProxy> sender) {
@Nullable final PeerProxy peerProxy = weakPeerProxy.get();
if (peerProxy == null) {
sender.removeOnListChangedCallback(this);
return;
}
peerProxy.setTotalPeers(sender.size());
}
@Override
public void onItemRangeChanged(final ObservableList<PeerProxy> sender,
final int positionStart, final int itemCount) {
// Do nothing.
}
@Override
public void onItemRangeInserted(final ObservableList<PeerProxy> sender,
final int positionStart, final int itemCount) {
onChanged(sender);
}
@Override
public void onItemRangeMoved(final ObservableList<PeerProxy> sender,
final int fromPosition, final int toPosition,
final int itemCount) {
// Do nothing.
}
@Override
public void onItemRangeRemoved(final ObservableList<PeerProxy> sender,
final int positionStart, final int itemCount) {
onChanged(sender);
}
}
private static class PeerProxyCreator implements Parcelable.Creator<PeerProxy> {
@Override
public PeerProxy createFromParcel(final Parcel in) {
return new PeerProxy(in);
}
@Override
public PeerProxy[] newArray(final int size) {
return new PeerProxy[size];
}
}
}
@@ -1,54 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget;
import androidx.annotation.Nullable;
import android.text.InputFilter;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import com.wireguard.crypto.Key;
/**
* InputFilter for entering WireGuard private/public keys encoded with base64.
*/
public class KeyInputFilter implements InputFilter {
private static boolean isAllowed(final char c) {
return Character.isLetterOrDigit(c) || c == '+' || c == '/';
}
public static InputFilter newInstance() {
return new KeyInputFilter();
}
@Nullable
@Override
public CharSequence filter(final CharSequence source,
final int sStart, final int sEnd,
final Spanned dest,
final int dStart, final int dEnd) {
SpannableStringBuilder replacement = null;
int rIndex = 0;
final int dLength = dest.length();
for (int sIndex = sStart; sIndex < sEnd; ++sIndex) {
final char c = source.charAt(sIndex);
final int dIndex = dStart + (sIndex - sStart);
// Restrict characters to the base64 character set.
// Ensure adding this character does not push the length over the limit.
if (((dIndex + 1 < Key.Format.BASE64.getLength() && isAllowed(c)) ||
(dIndex + 1 == Key.Format.BASE64.getLength() && c == '=')) &&
dLength + (sIndex - sStart) < Key.Format.BASE64.getLength()) {
++rIndex;
} else {
if (replacement == null)
replacement = new SpannableStringBuilder(source, sStart, sEnd);
replacement.delete(rIndex, rIndex + 1);
}
}
return replacement;
}
}
@@ -1,59 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.RelativeLayout;
import com.wireguard.android.R;
public class MultiselectableRelativeLayout extends RelativeLayout {
private static final int[] STATE_MULTISELECTED = {R.attr.state_multiselected};
private boolean multiselected;
public MultiselectableRelativeLayout(final Context context) {
super(context);
}
public MultiselectableRelativeLayout(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
public MultiselectableRelativeLayout(final Context context, final AttributeSet attrs, final int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public MultiselectableRelativeLayout(final Context context, final AttributeSet attrs, final int defStyleAttr, final int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected int[] onCreateDrawableState(final int extraSpace) {
if (multiselected) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
mergeDrawableStates(drawableState, STATE_MULTISELECTED);
return drawableState;
}
return super.onCreateDrawableState(extraSpace);
}
public void setMultiSelected(final boolean on) {
if (!multiselected) {
multiselected = true;
refreshDrawableState();
}
setActivated(on);
}
public void setSingleSelected(final boolean on) {
if (multiselected) {
multiselected = false;
refreshDrawableState();
}
setActivated(on);
}
}
@@ -1,53 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget;
import androidx.annotation.Nullable;
import android.text.InputFilter;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import com.wireguard.android.model.Tunnel;
/**
* InputFilter for entering WireGuard configuration names (Linux interface names).
*/
public class NameInputFilter implements InputFilter {
private static boolean isAllowed(final char c) {
return Character.isLetterOrDigit(c) || "_=+.-".indexOf(c) >= 0;
}
public static InputFilter newInstance() {
return new NameInputFilter();
}
@Nullable
@Override
public CharSequence filter(final CharSequence source,
final int sStart, final int sEnd,
final Spanned dest,
final int dStart, final int dEnd) {
SpannableStringBuilder replacement = null;
int rIndex = 0;
final int dLength = dest.length();
for (int sIndex = sStart; sIndex < sEnd; ++sIndex) {
final char c = source.charAt(sIndex);
final int dIndex = dStart + (sIndex - sStart);
// Restrict characters to those valid in interfaces.
// Ensure adding this character does not push the length over the limit.
if ((dIndex < Tunnel.NAME_MAX_LENGTH && isAllowed(c)) &&
dLength + (sIndex - sStart) < Tunnel.NAME_MAX_LENGTH) {
++rIndex;
} else {
if (replacement == null)
replacement = new SpannableStringBuilder(source, sStart, sEnd);
replacement.delete(rIndex, rIndex + 1);
}
}
return replacement;
}
}
@@ -1,217 +0,0 @@
/*
* Copyright © 2018 The Android Open Source Project
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.res.ColorStateList;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Path.Direction;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff.Mode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.drawable.Drawable;
import android.os.Build;
import androidx.annotation.ColorInt;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import android.util.FloatProperty;
@RequiresApi(Build.VERSION_CODES.N)
public class SlashDrawable extends Drawable {
private static final float CENTER_X = 10.65f;
private static final float CENTER_Y = 11.869239f;
private static final float CORNER_RADIUS = Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? 0f : 1f;
// Draw the slash washington-monument style; rotate to no-u-turn style
private static final float DEFAULT_ROTATION = -45f;
private static final long QS_ANIM_LENGTH = 350;
private static final float SCALE = 24f;
private static final float SLASH_HEIGHT = 28f;
// These values are derived in un-rotated (vertical) orientation
private static final float SLASH_WIDTH = 1.8384776f;
// Bottom is derived during animation
private static final float LEFT = (CENTER_X - (SLASH_WIDTH / 2)) / SCALE;
private static final float RIGHT = (CENTER_X + (SLASH_WIDTH / 2)) / SCALE;
private static final float TOP = (CENTER_Y - (SLASH_HEIGHT / 2)) / SCALE;
private static final FloatProperty mSlashLengthProp = new FloatProperty<SlashDrawable>("slashLength") {
@Override
public Float get(final SlashDrawable object) {
return object.mCurrentSlashLength;
}
@Override
public void setValue(final SlashDrawable object, final float value) {
object.mCurrentSlashLength = value;
}
};
private final Drawable mDrawable;
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Path mPath = new Path();
private final RectF mSlashRect = new RectF(0, 0, 0, 0);
private boolean mAnimationEnabled = true;
// Animate this value on change
private float mCurrentSlashLength;
private float mRotation;
private boolean mSlashed;
public SlashDrawable(final Drawable d) {
mDrawable = d;
}
@SuppressWarnings("deprecation")
@Override
public void draw(final Canvas canvas) {
canvas.save();
final Matrix m = new Matrix();
final int width = getBounds().width();
final int height = getBounds().height();
final float radiusX = scale(CORNER_RADIUS, width);
final float radiusY = scale(CORNER_RADIUS, height);
updateRect(
scale(LEFT, width),
scale(TOP, height),
scale(RIGHT, width),
scale(TOP + mCurrentSlashLength, height)
);
mPath.reset();
// Draw the slash vertically
mPath.addRoundRect(mSlashRect, radiusX, radiusY, Direction.CW);
// Rotate -45 + desired rotation
m.setRotate(mRotation + DEFAULT_ROTATION, width / 2, height / 2);
mPath.transform(m);
canvas.drawPath(mPath, mPaint);
// Rotate back to vertical
m.setRotate(-mRotation - DEFAULT_ROTATION, width / 2, height / 2);
mPath.transform(m);
// Draw another rect right next to the first, for clipping
m.setTranslate(mSlashRect.width(), 0);
mPath.transform(m);
mPath.addRoundRect(mSlashRect, 1.0f * width, 1.0f * height, Direction.CW);
m.setRotate(mRotation + DEFAULT_ROTATION, width / 2, height / 2);
mPath.transform(m);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
canvas.clipPath(mPath, Region.Op.DIFFERENCE);
else
canvas.clipOutPath(mPath);
mDrawable.draw(canvas);
canvas.restore();
}
@Override
public int getIntrinsicHeight() {
return mDrawable.getIntrinsicHeight();
}
@Override
public int getIntrinsicWidth() {
return mDrawable.getIntrinsicWidth();
}
@SuppressWarnings("deprecation")
@Override
public int getOpacity() {
return PixelFormat.OPAQUE;
}
@Override
protected void onBoundsChange(final Rect bounds) {
super.onBoundsChange(bounds);
mDrawable.setBounds(bounds);
}
private float scale(final float frac, final int width) {
return frac * width;
}
@Override
public void setAlpha(@IntRange(from = 0, to = 255) final int alpha) {
mDrawable.setAlpha(alpha);
mPaint.setAlpha(alpha);
}
public void setAnimationEnabled(final boolean enabled) {
mAnimationEnabled = enabled;
}
@Override
public void setColorFilter(@Nullable final ColorFilter colorFilter) {
mDrawable.setColorFilter(colorFilter);
mPaint.setColorFilter(colorFilter);
}
private void setDrawableTintList(@Nullable final ColorStateList tint) {
mDrawable.setTintList(tint);
}
public void setRotation(final float rotation) {
if (mRotation == rotation)
return;
mRotation = rotation;
invalidateSelf();
}
@SuppressWarnings("unchecked")
public void setSlashed(final boolean slashed) {
if (mSlashed == slashed) return;
mSlashed = slashed;
final float end = mSlashed ? SLASH_HEIGHT / SCALE : 0f;
final float start = mSlashed ? 0f : SLASH_HEIGHT / SCALE;
if (mAnimationEnabled) {
final ObjectAnimator anim = ObjectAnimator.ofFloat(this, mSlashLengthProp, start, end);
anim.addUpdateListener((ValueAnimator valueAnimator) -> invalidateSelf());
anim.setDuration(QS_ANIM_LENGTH);
anim.start();
} else {
mCurrentSlashLength = end;
invalidateSelf();
}
}
@Override
public void setTint(@ColorInt final int tintColor) {
super.setTint(tintColor);
mDrawable.setTint(tintColor);
mPaint.setColor(tintColor);
}
@Override
public void setTintList(@Nullable final ColorStateList tint) {
super.setTintList(tint);
setDrawableTintList(tint);
mPaint.setColor(tint == null ? 0 : tint.getDefaultColor());
invalidateSelf();
}
@Override
public void setTintMode(final Mode tintMode) {
super.setTintMode(tintMode);
mDrawable.setTintMode(tintMode);
}
private void updateRect(final float left, final float top, final float right, final float bottom) {
mSlashRect.left = left;
mSlashRect.top = top;
mSlashRect.right = right;
mSlashRect.bottom = bottom;
}
}
@@ -1,59 +0,0 @@
/*
* Copyright © 2013 The Android Open Source Project
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget;
import android.content.Context;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import android.util.AttributeSet;
import android.widget.Switch;
public class ToggleSwitch extends Switch {
private boolean isRestoringState;
@Nullable private OnBeforeCheckedChangeListener listener;
public ToggleSwitch(final Context context) {
this(context, null);
}
@SuppressWarnings({"SameParameterValue", "WeakerAccess"})
public ToggleSwitch(final Context context, @Nullable final AttributeSet attrs) {
super(context, attrs);
}
@Override
public void onRestoreInstanceState(final Parcelable state) {
isRestoringState = true;
super.onRestoreInstanceState(state);
isRestoringState = false;
}
@Override
public void setChecked(final boolean checked) {
if (checked == isChecked())
return;
if (isRestoringState || listener == null) {
super.setChecked(checked);
return;
}
setEnabled(false);
listener.onBeforeCheckedChanged(this, checked);
}
public void setCheckedInternal(final boolean checked) {
super.setChecked(checked);
setEnabled(true);
}
public void setOnBeforeCheckedChangeListener(final OnBeforeCheckedChangeListener listener) {
this.listener = listener;
}
public interface OnBeforeCheckedChangeListener {
void onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked);
}
}
@@ -1,66 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget.fab;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.content.Context;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import com.google.android.material.snackbar.Snackbar;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import android.util.AttributeSet;
import android.view.View;
public class FloatingActionButtonBehavior extends CoordinatorLayout.Behavior<FloatingActionsMenu> {
private static final long ANIMATION_DURATION = 250;
private static final TimeInterpolator FAST_OUT_SLOW_IN_INTERPOLATOR = new FastOutSlowInInterpolator();
public FloatingActionButtonBehavior(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
private static void animateChange(final FloatingActionsMenu child, final float destination, final float fullSpan) {
final float origin = child.getBehaviorYTranslation();
if (Math.abs(destination - origin) < fullSpan / 2) {
child.setBehaviorYTranslation(destination);
return;
}
final ValueAnimator animator = new ValueAnimator();
animator.setFloatValues(origin, destination);
animator.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
animator.setDuration((long) (ANIMATION_DURATION * (Math.abs(destination - origin) / fullSpan)));
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(final Animator a) {
child.setBehaviorYTranslation(destination);
}
});
animator.addUpdateListener(a -> child.setBehaviorYTranslation((float) a.getAnimatedValue()));
animator.start();
}
@Override
public boolean layoutDependsOn(final CoordinatorLayout parent, final FloatingActionsMenu child,
final View dependency) {
return dependency instanceof Snackbar.SnackbarLayout;
}
@Override
public boolean onDependentViewChanged(final CoordinatorLayout parent, final FloatingActionsMenu child,
final View dependency) {
animateChange(child, Math.min(0, dependency.getTranslationY() - dependency.getMeasuredHeight()), dependency.getMeasuredHeight());
return true;
}
@Override
public void onDependentViewRemoved(final CoordinatorLayout parent, final FloatingActionsMenu child,
final View dependency) {
animateChange(child, 0, dependency.getMeasuredHeight());
}
}
@@ -1,629 +0,0 @@
/*
* Copyright © 2014 Jerzy Chalupski
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget.fab;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Keep;
import androidx.annotation.Nullable;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import androidx.core.content.res.ResourcesCompat;
import androidx.appcompat.widget.AppCompatTextView;
import android.util.AttributeSet;
import android.view.ContextThemeWrapper;
import android.view.TouchDelegate;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.OvershootInterpolator;
import android.widget.TextView;
import com.wireguard.android.R;
public class FloatingActionsMenu extends ViewGroup {
public static final int EXPAND_DOWN = 1;
public static final int EXPAND_LEFT = 2;
public static final int EXPAND_RIGHT = 3;
public static final int EXPAND_UP = 0;
public static final int LABELS_ON_LEFT_SIDE = 0;
public static final int LABELS_ON_RIGHT_SIDE = 1;
private static final TimeInterpolator ALPHA_EXPAND_INTERPOLATOR = new DecelerateInterpolator();
private static final int ANIMATION_DURATION = 300;
private static final boolean BROKEN_LABEL_STYLE = Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1 && Build.BRAND.equalsIgnoreCase("ASUS");
private static final float COLLAPSED_PLUS_ROTATION = 0f;
private static final TimeInterpolator COLLAPSE_INTERPOLATOR = new DecelerateInterpolator(3f);
private static final float EXPANDED_PLUS_ROTATION = 90f + 45f;
private static final TimeInterpolator EXPAND_INTERPOLATOR = new OvershootInterpolator();
private final AnimatorSet mCollapseAnimation = new AnimatorSet().setDuration(ANIMATION_DURATION);
private final AnimatorSet mExpandAnimation = new AnimatorSet().setDuration(ANIMATION_DURATION);
private final Rect touchArea = new Rect(0, 0, 0, 0);
private float behaviorYTranslation;
@Nullable private FloatingActionButton mAddButton;
private int mButtonSpacing;
private int mButtonsCount;
private int mExpandDirection;
private boolean mExpanded;
private int mLabelsMargin;
private int mLabelsPosition;
private int mLabelsStyle;
private int mLabelsVerticalOffset;
@Nullable private OnFloatingActionsMenuUpdateListener mListener;
private int mMaxButtonHeight;
private int mMaxButtonWidth;
@Nullable private RotatingDrawable mRotatingDrawable;
@Nullable private TouchDelegateGroup mTouchDelegateGroup;
private float scrollYTranslation;
public FloatingActionsMenu(final Context context) {
this(context, null);
}
public FloatingActionsMenu(final Context context, @Nullable final AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public FloatingActionsMenu(final Context context, @Nullable final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
private static int adjustForOvershoot(final int dimension) {
return dimension * 12 / 10;
}
public void addButton(final LabeledFloatingActionButton button) {
addView(button, mButtonsCount - 1);
mButtonsCount++;
if (mLabelsStyle != 0) {
createLabels();
}
}
public void collapse() {
collapse(false);
}
private void collapse(final boolean immediately) {
if (mExpanded) {
mExpanded = false;
mTouchDelegateGroup.setEnabled(false);
mCollapseAnimation.setDuration(immediately ? 0 : ANIMATION_DURATION);
mCollapseAnimation.start();
mExpandAnimation.cancel();
if (mListener != null) {
mListener.onMenuCollapsed();
}
}
}
public void collapseImmediately() {
collapse(true);
}
private void createAddButton(final Context context) {
final RotatingDrawable rotatingDrawable = new RotatingDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_action_add_white, context.getTheme()));
mRotatingDrawable = rotatingDrawable;
final TimeInterpolator interpolator = new OvershootInterpolator();
final ObjectAnimator collapseAnimator = ObjectAnimator.ofFloat(rotatingDrawable, "rotation", EXPANDED_PLUS_ROTATION, COLLAPSED_PLUS_ROTATION);
final ObjectAnimator expandAnimator = ObjectAnimator.ofFloat(rotatingDrawable, "rotation", COLLAPSED_PLUS_ROTATION, EXPANDED_PLUS_ROTATION);
collapseAnimator.setInterpolator(interpolator);
expandAnimator.setInterpolator(interpolator);
mExpandAnimation.play(expandAnimator);
mCollapseAnimation.play(collapseAnimator);
mAddButton = new FloatingActionButton(context);
mAddButton.setImageDrawable(rotatingDrawable);
mAddButton.setId(R.id.fab_expand_menu_button);
mAddButton.setOnClickListener(v -> toggle());
addView(mAddButton, super.generateDefaultLayoutParams());
mButtonsCount++;
}
private void createLabels() {
final Context context = BROKEN_LABEL_STYLE ? getContext() : new ContextThemeWrapper(getContext(), mLabelsStyle);
for (int i = 0; i < mButtonsCount; i++) {
final FloatingActionButton button = (FloatingActionButton) getChildAt(i);
if (button instanceof LabeledFloatingActionButton) {
final String title = ((LabeledFloatingActionButton) button).getTitle();
final AppCompatTextView label = new AppCompatTextView(context);
if (!BROKEN_LABEL_STYLE)
label.setTextAppearance(context, mLabelsStyle);
label.setText(title);
addView(label);
button.setTag(R.id.fab_label, label);
}
}
}
public void expand() {
if (!mExpanded) {
mExpanded = true;
mTouchDelegateGroup.setEnabled(true);
mCollapseAnimation.cancel();
mExpandAnimation.start();
if (mListener != null) {
mListener.onMenuExpanded();
}
}
}
private boolean expandsHorizontally() {
return mExpandDirection == EXPAND_LEFT || mExpandDirection == EXPAND_RIGHT;
}
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(super.generateDefaultLayoutParams());
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(final AttributeSet attrs) {
return new LayoutParams(super.generateLayoutParams(attrs));
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(final ViewGroup.LayoutParams p) {
return new LayoutParams(super.generateLayoutParams(p));
}
public float getBehaviorYTranslation() {
return behaviorYTranslation;
}
public float getScrollYTranslation() {
return scrollYTranslation;
}
private void init(final Context context, @Nullable final AttributeSet attributeSet) {
mButtonSpacing = (int) (getResources().getDimension(R.dimen.fab_actions_spacing));
mLabelsMargin = getResources().getDimensionPixelSize(R.dimen.fab_labels_margin);
mLabelsVerticalOffset = getResources().getDimensionPixelSize(R.dimen.fab_shadow_offset);
mTouchDelegateGroup = new TouchDelegateGroup(this);
setTouchDelegate(mTouchDelegateGroup);
final TypedArray attr = context.obtainStyledAttributes(attributeSet, R.styleable.FloatingActionsMenu, 0, 0);
mExpandDirection = attr.getInt(R.styleable.FloatingActionsMenu_fab_expandDirection, EXPAND_UP);
mLabelsStyle = attr.getResourceId(R.styleable.FloatingActionsMenu_fab_labelStyle, 0);
mLabelsPosition = attr.getInt(R.styleable.FloatingActionsMenu_fab_labelsPosition, LABELS_ON_LEFT_SIDE);
attr.recycle();
if (mLabelsStyle != 0 && expandsHorizontally()) {
throw new IllegalStateException("Action labels in horizontal expand orientation are not supported");
}
createAddButton(context);
}
public boolean isExpanded() {
return mExpanded;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
bringChildToFront(mAddButton);
mButtonsCount = getChildCount();
if (mLabelsStyle != 0) {
createLabels();
}
}
@Override
protected void onLayout(final boolean changed, final int l, final int t, final int r, final int b) {
switch (mExpandDirection) {
case EXPAND_UP:
case EXPAND_DOWN:
final boolean expandUp = mExpandDirection == EXPAND_UP;
if (changed) {
mTouchDelegateGroup.clearTouchDelegates();
}
final int addButtonY = expandUp ? b - t - mAddButton.getMeasuredHeight() : 0;
// Ensure mAddButton is centered on the line where the buttons should be
final int buttonsHorizontalCenter = (mLabelsPosition == LABELS_ON_LEFT_SIDE
? r - l - mMaxButtonWidth / 2
: mMaxButtonWidth / 2);
final int addButtonLeft = buttonsHorizontalCenter - mAddButton.getMeasuredWidth() / 2;
mAddButton.layout(addButtonLeft, addButtonY, addButtonLeft + mAddButton.getMeasuredWidth(), addButtonY + mAddButton.getMeasuredHeight());
final int labelsOffset = mMaxButtonWidth / 2 + mLabelsMargin;
final int labelsXNearButton = mLabelsPosition == LABELS_ON_LEFT_SIDE
? buttonsHorizontalCenter - labelsOffset
: buttonsHorizontalCenter + labelsOffset;
int nextY = expandUp ?
addButtonY - mButtonSpacing :
addButtonY + mAddButton.getMeasuredHeight() + mButtonSpacing;
for (int i = mButtonsCount - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child == mAddButton || child.getVisibility() == GONE) continue;
final int childX = buttonsHorizontalCenter - child.getMeasuredWidth() / 2;
final int childY = expandUp ? nextY - child.getMeasuredHeight() : nextY;
child.layout(childX, childY, childX + child.getMeasuredWidth(), childY + child.getMeasuredHeight());
final float collapsedTranslation = addButtonY - childY;
final float expandedTranslation = 0f;
child.setTranslationY(mExpanded ? expandedTranslation : collapsedTranslation);
child.setAlpha(mExpanded ? 1f : 0f);
final LayoutParams params = (LayoutParams) child.getLayoutParams();
params.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation);
params.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation);
params.setAnimationsTarget(child);
final View label = (View) child.getTag(R.id.fab_label);
if (label != null) {
final int labelXAwayFromButton = mLabelsPosition == LABELS_ON_LEFT_SIDE
? labelsXNearButton - label.getMeasuredWidth()
: labelsXNearButton + label.getMeasuredWidth();
final int labelLeft = mLabelsPosition == LABELS_ON_LEFT_SIDE
? labelXAwayFromButton
: labelsXNearButton;
final int labelRight = mLabelsPosition == LABELS_ON_LEFT_SIDE
? labelsXNearButton
: labelXAwayFromButton;
final int labelTop = childY - mLabelsVerticalOffset + (child.getMeasuredHeight() - label.getMeasuredHeight()) / 2;
label.layout(labelLeft, labelTop, labelRight, labelTop + label.getMeasuredHeight());
touchArea.set(Math.min(childX, labelLeft),
childY - mButtonSpacing / 2,
Math.max(childX + child.getMeasuredWidth(), labelRight),
childY + child.getMeasuredHeight() + mButtonSpacing / 2);
mTouchDelegateGroup.addTouchDelegate(new TouchDelegate(new Rect(touchArea), child));
label.setTranslationY(mExpanded ? expandedTranslation : collapsedTranslation);
label.setAlpha(mExpanded ? 1f : 0f);
final LayoutParams labelParams = (LayoutParams) label.getLayoutParams();
labelParams.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation);
labelParams.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation);
labelParams.setAnimationsTarget(label);
}
nextY = expandUp ?
childY - mButtonSpacing :
childY + child.getMeasuredHeight() + mButtonSpacing;
}
break;
case EXPAND_LEFT:
case EXPAND_RIGHT:
final boolean expandLeft = mExpandDirection == EXPAND_LEFT;
final int addButtonX = expandLeft ? r - l - mAddButton.getMeasuredWidth() : 0;
// Ensure mAddButton is centered on the line where the buttons should be
final int addButtonTop = b - t - mMaxButtonHeight + (mMaxButtonHeight - mAddButton.getMeasuredHeight()) / 2;
mAddButton.layout(addButtonX, addButtonTop, addButtonX + mAddButton.getMeasuredWidth(), addButtonTop + mAddButton.getMeasuredHeight());
int nextX = expandLeft ?
addButtonX - mButtonSpacing :
addButtonX + mAddButton.getMeasuredWidth() + mButtonSpacing;
for (int i = mButtonsCount - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child == mAddButton || child.getVisibility() == GONE) continue;
final int childX = expandLeft ? nextX - child.getMeasuredWidth() : nextX;
final int childY = addButtonTop + (mAddButton.getMeasuredHeight() - child.getMeasuredHeight()) / 2;
child.layout(childX, childY, childX + child.getMeasuredWidth(), childY + child.getMeasuredHeight());
final float collapsedTranslation = addButtonX - childX;
final float expandedTranslation = 0f;
child.setTranslationX(mExpanded ? expandedTranslation : collapsedTranslation);
child.setAlpha(mExpanded ? 1f : 0f);
final LayoutParams params = (LayoutParams) child.getLayoutParams();
params.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation);
params.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation);
params.setAnimationsTarget(child);
nextX = expandLeft ?
childX - mButtonSpacing :
childX + child.getMeasuredWidth() + mButtonSpacing;
}
break;
}
}
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
int width = 0;
int height = 0;
mMaxButtonWidth = 0;
mMaxButtonHeight = 0;
int maxLabelWidth = 0;
for (int i = 0; i < mButtonsCount; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
switch (mExpandDirection) {
case EXPAND_UP:
case EXPAND_DOWN:
mMaxButtonWidth = Math.max(mMaxButtonWidth, child.getMeasuredWidth());
height += child.getMeasuredHeight();
break;
case EXPAND_LEFT:
case EXPAND_RIGHT:
width += child.getMeasuredWidth();
mMaxButtonHeight = Math.max(mMaxButtonHeight, child.getMeasuredHeight());
break;
}
if (!expandsHorizontally()) {
final TextView label = (TextView) child.getTag(R.id.fab_label);
if (label != null) {
maxLabelWidth = Math.max(maxLabelWidth, label.getMeasuredWidth());
}
}
}
if (expandsHorizontally()) {
height = mMaxButtonHeight;
} else {
width = mMaxButtonWidth + (maxLabelWidth > 0 ? maxLabelWidth + mLabelsMargin : 0);
}
switch (mExpandDirection) {
case EXPAND_UP:
case EXPAND_DOWN:
height += mButtonSpacing * (mButtonsCount - 1);
height = adjustForOvershoot(height);
break;
case EXPAND_LEFT:
case EXPAND_RIGHT:
width += mButtonSpacing * (mButtonsCount - 1);
width = adjustForOvershoot(width);
break;
}
setMeasuredDimension(width, height);
}
@Override
public void onRestoreInstanceState(final Parcelable state) {
if (state instanceof SavedState) {
final SavedState savedState = (SavedState) state;
mExpanded = savedState.mExpanded;
mTouchDelegateGroup.setEnabled(mExpanded);
if (mRotatingDrawable != null) {
mRotatingDrawable.setRotation(mExpanded ? EXPANDED_PLUS_ROTATION : COLLAPSED_PLUS_ROTATION);
}
super.onRestoreInstanceState(savedState.getSuperState());
} else {
super.onRestoreInstanceState(state);
}
}
@Override
public Parcelable onSaveInstanceState() {
final Parcelable superState = super.onSaveInstanceState();
final SavedState savedState = new SavedState(superState);
savedState.mExpanded = mExpanded;
return savedState;
}
public void removeButton(final LabeledFloatingActionButton button) {
removeView(button.getLabelView());
removeView(button);
button.setTag(R.id.fab_label, null);
mButtonsCount--;
}
public void setBehaviorYTranslation(final float behaviorYTranslation) {
this.behaviorYTranslation = behaviorYTranslation;
setTranslationY(behaviorYTranslation + scrollYTranslation);
}
@Override
public void setEnabled(final boolean enabled) {
super.setEnabled(enabled);
mAddButton.setEnabled(enabled);
}
public void setOnFloatingActionsMenuUpdateListener(final OnFloatingActionsMenuUpdateListener listener) {
mListener = listener;
}
public void setScrollYTranslation(final float scrollYTranslation) {
this.scrollYTranslation = scrollYTranslation;
setTranslationY(behaviorYTranslation + scrollYTranslation);
}
public void toggle() {
if (mExpanded) {
collapse();
} else {
expand();
}
}
public interface OnFloatingActionsMenuUpdateListener {
void onMenuCollapsed();
void onMenuExpanded();
}
private static class RotatingDrawable extends LayerDrawable {
private float mRotation;
RotatingDrawable(final Drawable drawable) {
super(new Drawable[]{drawable});
}
@Override
public void draw(final Canvas canvas) {
canvas.save();
canvas.rotate(mRotation, getBounds().centerX(), getBounds().centerY());
super.draw(canvas);
canvas.restore();
}
@SuppressWarnings("UnusedDeclaration")
public float getRotation() {
return mRotation;
}
@Keep
@SuppressWarnings("UnusedDeclaration")
public void setRotation(final float rotation) {
mRotation = rotation;
invalidateSelf();
}
}
public static class SavedState extends BaseSavedState {
public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
@Override
public SavedState createFromParcel(final Parcel in) {
return new SavedState(in);
}
@Override
public SavedState[] newArray(final int size) {
return new SavedState[size];
}
};
private boolean mExpanded;
public SavedState(final Parcelable parcel) {
super(parcel);
}
private SavedState(final Parcel in) {
super(in);
mExpanded = in.readInt() == 1;
}
@Override
public void writeToParcel(final Parcel out, final int flags) {
super.writeToParcel(out, flags);
out.writeInt(mExpanded ? 1 : 0);
}
}
private class LayoutParams extends ViewGroup.LayoutParams {
private final ObjectAnimator mCollapseAlpha = new ObjectAnimator();
private final ObjectAnimator mCollapseDir = new ObjectAnimator();
private final ObjectAnimator mExpandAlpha = new ObjectAnimator();
private final ObjectAnimator mExpandDir = new ObjectAnimator();
private boolean animationsSetToPlay;
LayoutParams(final ViewGroup.LayoutParams source) {
super(source);
mExpandDir.setInterpolator(EXPAND_INTERPOLATOR);
mExpandAlpha.setInterpolator(ALPHA_EXPAND_INTERPOLATOR);
mCollapseDir.setInterpolator(COLLAPSE_INTERPOLATOR);
mCollapseAlpha.setInterpolator(COLLAPSE_INTERPOLATOR);
mCollapseAlpha.setProperty(View.ALPHA);
mCollapseAlpha.setFloatValues(1f, 0f);
mExpandAlpha.setProperty(View.ALPHA);
mExpandAlpha.setFloatValues(0f, 1f);
switch (mExpandDirection) {
case EXPAND_UP:
case EXPAND_DOWN:
mCollapseDir.setProperty(View.TRANSLATION_Y);
mExpandDir.setProperty(View.TRANSLATION_Y);
break;
case EXPAND_LEFT:
case EXPAND_RIGHT:
mCollapseDir.setProperty(View.TRANSLATION_X);
mExpandDir.setProperty(View.TRANSLATION_X);
break;
}
}
private void addLayerTypeListener(final Animator animator, final View view) {
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(final Animator animation) {
view.setLayerType(LAYER_TYPE_NONE, null);
}
@Override
public void onAnimationStart(final Animator animation) {
view.setLayerType(LAYER_TYPE_HARDWARE, null);
}
});
}
public void setAnimationsTarget(final View view) {
mCollapseAlpha.setTarget(view);
mCollapseDir.setTarget(view);
mExpandAlpha.setTarget(view);
mExpandDir.setTarget(view);
// Now that the animations have targets, set them to be played
if (!animationsSetToPlay) {
addLayerTypeListener(mExpandDir, view);
addLayerTypeListener(mCollapseDir, view);
mCollapseAnimation.play(mCollapseAlpha);
mCollapseAnimation.play(mCollapseDir);
mExpandAnimation.play(mExpandAlpha);
mExpandAnimation.play(mExpandDir);
animationsSetToPlay = true;
}
}
}
}
@@ -1,27 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget.fab;
import androidx.recyclerview.widget.RecyclerView;
public class FloatingActionsMenuRecyclerViewScrollListener extends RecyclerView.OnScrollListener {
private static final float SCALE_FACTOR = 1.5f;
private final FloatingActionsMenu menu;
public FloatingActionsMenuRecyclerViewScrollListener(final FloatingActionsMenu menu) {
this.menu = menu;
}
private static float bound(final float min, final float proposal, final float max) {
return Math.min(max, Math.max(min, proposal));
}
@Override
public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) {
super.onScrolled(recyclerView, dx, dy);
menu.setScrollYTranslation(bound(0, menu.getScrollYTranslation() + dy * SCALE_FACTOR, menu.getMeasuredHeight() - menu.getTranslationY()));
}
}
@@ -1,58 +0,0 @@
/*
* Copyright © 2014 Jerzy Chalupski
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget.fab;
import android.content.Context;
import android.content.res.TypedArray;
import androidx.annotation.Nullable;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import android.util.AttributeSet;
import android.widget.TextView;
import com.wireguard.android.R;
public class LabeledFloatingActionButton extends FloatingActionButton {
@Nullable private final String title;
public LabeledFloatingActionButton(final Context context) {
this(context, null);
}
public LabeledFloatingActionButton(final Context context, @Nullable final AttributeSet attrs) {
this(context, attrs, 0);
}
public LabeledFloatingActionButton(final Context context, @Nullable final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
final TypedArray attr = context.obtainStyledAttributes(attrs, R.styleable.LabeledFloatingActionButton, 0, 0);
title = attr.getString(R.styleable.LabeledFloatingActionButton_fab_title);
attr.recycle();
}
@Nullable
TextView getLabelView() {
return (TextView) getTag(R.id.fab_label);
}
@Nullable
public String getTitle() {
return title;
}
@Override
public void setVisibility(final int visibility) {
final TextView label = getLabelView();
if (label != null) {
label.setVisibility(visibility);
}
super.setVisibility(visibility);
}
}
@@ -1,78 +0,0 @@
/*
* Copyright © 2014 Jerzy Chalupski
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget.fab;
import android.graphics.Rect;
import androidx.annotation.Nullable;
import android.view.MotionEvent;
import android.view.TouchDelegate;
import android.view.View;
import java.util.ArrayList;
import java.util.Collection;
public class TouchDelegateGroup extends TouchDelegate {
private static final Rect USELESS_HACKY_RECT = new Rect();
private final Collection<TouchDelegate> mTouchDelegates = new ArrayList<>();
@Nullable private TouchDelegate mCurrentTouchDelegate;
private boolean mEnabled;
public TouchDelegateGroup(final View uselessHackyView) {
super(USELESS_HACKY_RECT, uselessHackyView);
}
public void addTouchDelegate(final TouchDelegate touchDelegate) {
mTouchDelegates.add(touchDelegate);
}
public void clearTouchDelegates() {
mTouchDelegates.clear();
mCurrentTouchDelegate = null;
}
@Override
public boolean onTouchEvent(final MotionEvent event) {
if (!mEnabled)
return false;
TouchDelegate delegate = null;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
for (final TouchDelegate touchDelegate : mTouchDelegates) {
if (touchDelegate.onTouchEvent(event)) {
mCurrentTouchDelegate = touchDelegate;
return true;
}
}
break;
case MotionEvent.ACTION_MOVE:
delegate = mCurrentTouchDelegate;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
delegate = mCurrentTouchDelegate;
mCurrentTouchDelegate = null;
break;
}
return delegate != null && delegate.onTouchEvent(event);
}
public void removeTouchDelegate(final TouchDelegate touchDelegate) {
mTouchDelegates.remove(touchDelegate);
if (mCurrentTouchDelegate == touchDelegate) {
mCurrentTouchDelegate = null;
}
}
public void setEnabled(final boolean enabled) {
mEnabled = enabled;
}
}
@@ -1,14 +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.
*/
public interface Keyed<K> {
K getKey();
}
@@ -1,32 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.util;
import androidx.annotation.Nullable;
import java.util.Collection;
import java.util.List;
/**
* A list containing elements that can be looked up by key. A {@code KeyedList} cannot contain
* {@code null} elements.
*/
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,31 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.util;
import androidx.annotation.Nullable;
import java.util.Collection;
import java.util.Comparator;
import java.util.Set;
/**
* A keyed list where all elements are sorted by the comparator returned by {@code comparator()}
* applied to their keys.
*/
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,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<padding
android:bottom="4dp"
android:left="8dp"
android:right="8dp"
android:top="4dp" />
<solid android:color="@color/fab_label_background_color" />
</shape>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="?android:attr/colorForeground"
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" />
</vector>
@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/master_detail_wrapper"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:divider="?attr/dividerHorizontal"
android:orientation="horizontal"
android:showDividers="middle"
tools:context=".activity.MainActivity">
<fragment
android:name="com.wireguard.android.fragment.TunnelListFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
android:tag="LIST" />
<FrameLayout
android:id="@+id/detail_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3" />
</LinearLayout>
@@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<import type="com.wireguard.android.model.ApplicationData" />
<variable
name="fragment"
type="com.wireguard.android.fragment.AppListDialogFragment" />
<variable
name="appData"
type="com.wireguard.android.util.ObservableKeyedList&lt;String, ApplicationData&gt;" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="200dp">
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="@{appData.isEmpty() ? View.VISIBLE : View.GONE}" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/app_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:items="@{appData}"
app:layout="@{@layout/app_list_item}" />
</FrameLayout>
</layout>
@@ -1,139 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.wireguard.android.model.Tunnel.State" />
<import type="com.wireguard.android.util.ClipboardUtils" />
<variable
name="fragment"
type="com.wireguard.android.fragment.TunnelDetailFragment" />
<variable
name="tunnel"
type="com.wireguard.android.model.Tunnel" />
<variable
name="config"
type="com.wireguard.config.Config" />
</data>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/colorBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:background="?android:attr/colorBackground"
app:cardCornerRadius="4dp"
app:cardElevation="2dp"
app:contentPadding="8dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/interface_title"
style="?android:attr/textAppearanceMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:text="@string/interface_title" />
<com.wireguard.android.widget.ToggleSwitch
android:id="@+id/tunnel_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/interface_title"
android:layout_alignParentEnd="true"
app:checked="@{tunnel.state == State.UP}"
app:onBeforeCheckedChanged="@{fragment::setTunnelState}" />
<TextView
android:id="@+id/interface_name_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/interface_title"
android:layout_marginTop="8dp"
android:labelFor="@+id/interface_name_text"
android:text="@string/name" />
<TextView
android:id="@+id/interface_name_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/interface_name_label"
android:text="@{tunnel.name}" />
<TextView
android:id="@+id/public_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/interface_name_text"
android:layout_marginTop="8dp"
android:labelFor="@+id/public_key_text"
android:text="@string/public_key" />
<TextView
android:id="@+id/public_key_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/public_key_label"
android:contentDescription="@string/public_key_description"
android:ellipsize="end"
android:maxLines="1"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{config.interface.keyPair.publicKey.toBase64}" />
<TextView
android:id="@+id/addresses_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/public_key_text"
android:layout_marginTop="8dp"
android:labelFor="@+id/addresses_text"
android:text="@string/addresses" />
<TextView
android:id="@+id/addresses_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/addresses_label"
android:contentDescription="@string/addresses"
android:text="@{config.interface.addresses}" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
<LinearLayout
android:id="@+id/peers_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:divider="@null"
android:orientation="vertical"
app:items="@{config.peers}"
app:layout="@{@layout/tunnel_detail_peer}"
tools:ignore="UselessLeaf" />
</LinearLayout>
</ScrollView>
</layout>
@@ -1,112 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="com.wireguard.android.util.ClipboardUtils" />
<variable
name="item"
type="com.wireguard.config.Peer" />
</data>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:background="?android:attr/colorBackground"
app:cardCornerRadius="4dp"
app:cardElevation="2dp"
app:contentPadding="8dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/peer_title"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:text="@string/peer" />
<TextView
android:id="@+id/public_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/peer_title"
android:layout_marginTop="8dp"
android:labelFor="@+id/public_key_text"
android:text="@string/public_key" />
<TextView
android:id="@+id/public_key_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/public_key_label"
android:contentDescription="@string/public_key_description"
android:ellipsize="end"
android:maxLines="1"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{item.publicKey.toBase64}" />
<TextView
android:id="@+id/allowed_ips_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/public_key_text"
android:layout_marginTop="8dp"
android:labelFor="@+id/allowed_ips_text"
android:text="@string/allowed_ips" />
<TextView
android:id="@+id/allowed_ips_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/allowed_ips_label"
android:text="@{item.allowedIps}" />
<TextView
android:id="@+id/endpoint_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/allowed_ips_text"
android:layout_marginTop="8dp"
android:labelFor="@+id/endpoint_text"
android:text="@string/endpoint" />
<TextView
android:id="@+id/endpoint_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/endpoint_label"
android:text="@{item.endpoint}" />
<TextView
android:id="@+id/transfer_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/endpoint_text"
android:layout_marginTop="8dp"
android:labelFor="@+id/transfer_text"
android:text="@string/transfer"
android:visibility="gone" />
<TextView
android:id="@+id/transfer_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/transfer_label"
android:visibility="gone" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
</layout>
@@ -1,254 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.wireguard.android.util.ClipboardUtils" />
<import type="com.wireguard.android.widget.KeyInputFilter" />
<import type="com.wireguard.android.widget.NameInputFilter" />
<variable
name="fragment"
type="com.wireguard.android.fragment.TunnelEditorFragment" />
<variable
name="config"
type="com.wireguard.android.viewmodel.ConfigProxy" />
<variable
name="name"
type="String" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/main_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/colorBackground">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:background="?android:attr/colorBackground"
app:cardCornerRadius="4dp"
app:cardElevation="2dp"
app:contentPadding="8dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/interface_title"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:text="@string/interface_title" />
<TextView
android:id="@+id/interface_name_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/interface_title"
android:layout_marginTop="8dp"
android:labelFor="@+id/interface_name_text"
android:text="@string/name" />
<EditText
android:id="@+id/interface_name_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/interface_name_label"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={name}"
app:filter="@{NameInputFilter.newInstance()}" />
<TextView
android:id="@+id/private_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/interface_name_text"
android:labelFor="@+id/private_key_text"
android:text="@string/private_key" />
<EditText
android:id="@+id/private_key_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/private_key_label"
android:layout_toStartOf="@+id/generate_private_key_button"
android:contentDescription="@string/public_key_description"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={config.interface.privateKey}"
app:filter="@{KeyInputFilter.newInstance()}" />
<Button
android:id="@+id/generate_private_key_button"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="96dp"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/private_key_text"
android:layout_alignParentEnd="true"
android:layout_below="@+id/private_key_label"
android:onClick="@{() -> config.interface.generateKeyPair()}"
android:text="@string/generate" />
<TextView
android:id="@+id/public_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/private_key_text"
android:labelFor="@+id/public_key_text"
android:text="@string/public_key" />
<TextView
android:id="@+id/public_key_text"
style="?attr/editTextStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/public_key_label"
android:contentDescription="@string/public_key_description"
android:ellipsize="end"
android:focusable="false"
android:hint="@string/hint_generated"
android:maxLines="1"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{config.interface.publicKey}" />
<TextView
android:id="@+id/addresses_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/public_key_text"
android:layout_toStartOf="@+id/listen_port_label"
android:labelFor="@+id/addresses_text"
android:text="@string/addresses" />
<EditText
android:id="@+id/addresses_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/addresses_label"
android:layout_toStartOf="@+id/listen_port_text"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={config.interface.addresses}" />
<TextView
android:id="@+id/listen_port_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/addresses_label"
android:layout_alignParentEnd="true"
android:layout_alignStart="@+id/generate_private_key_button"
android:labelFor="@+id/listen_port_text"
android:text="@string/listen_port" />
<EditText
android:id="@+id/listen_port_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/addresses_text"
android:layout_alignParentEnd="true"
android:layout_alignStart="@+id/generate_private_key_button"
android:hint="@string/hint_random"
android:inputType="number"
android:text="@={config.interface.listenPort}"
android:textAlignment="center" />
<TextView
android:id="@+id/dns_servers_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/addresses_text"
android:layout_toStartOf="@+id/mtu_label"
android:labelFor="@+id/dns_servers_text"
android:text="@string/dns_servers" />
<EditText
android:id="@+id/dns_servers_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/dns_servers_label"
android:layout_toStartOf="@+id/mtu_text"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={config.interface.dnsServers}" />
<TextView
android:id="@+id/mtu_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/dns_servers_label"
android:layout_alignParentEnd="true"
android:layout_alignStart="@+id/generate_private_key_button"
android:labelFor="@+id/mtu_text"
android:text="@string/mtu" />
<EditText
android:id="@+id/mtu_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/dns_servers_text"
android:layout_alignParentEnd="true"
android:layout_alignStart="@+id/generate_private_key_button"
android:hint="@string/hint_automatic"
android:inputType="number"
android:text="@={config.interface.mtu}"
android:textAlignment="center" />
<Button
android:id="@+id/set_excluded_applications"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/dns_servers_text"
android:layout_marginLeft="-8dp"
android:onClick="@{fragment::onRequestSetExcludedApplications}"
android:text="@{@plurals/set_excluded_applications(config.interface.excludedApplications.size, config.interface.excludedApplications.size)}" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="@null"
android:orientation="vertical"
app:items="@{config.peers}"
app:layout="@{@layout/tunnel_editor_peer}"
tools:ignore="UselessLeaf" />
<Button
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginEnd="4dp"
android:layout_marginStart="4dp"
android:onClick="@{() -> config.addPeer()}"
android:text="@string/add_peer" />
</LinearLayout>
</ScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
@@ -1,161 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<import type="com.wireguard.android.widget.KeyInputFilter" />
<variable
name="collection"
type="androidx.databinding.ObservableList&lt;com.wireguard.android.viewmodel.PeerProxy&gt;" />
<variable
name="item"
type="com.wireguard.android.viewmodel.PeerProxy" />
</data>
<androidx.cardview.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:background="?android:attr/colorBackground"
app:cardCornerRadius="4dp"
app:cardElevation="2dp"
app:contentPadding="8dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/peer_title"
style="?android:attr/textAppearanceMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginBottom="8dp"
android:layout_toStartOf="@+id/peer_action_delete"
android:text="@string/peer" />
<ImageButton
android:id="@+id/peer_action_delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:background="@null"
android:contentDescription="@string/delete"
android:onClick="@{() -> item.unbind()}"
android:src="@drawable/ic_action_delete" />
<TextView
android:id="@+id/public_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/peer_title"
android:labelFor="@+id/public_key_text"
android:text="@string/public_key" />
<EditText
android:id="@+id/public_key_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/public_key_label"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={item.publicKey}"
app:filter="@{KeyInputFilter.newInstance()}" />
<TextView
android:id="@+id/pre_shared_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/public_key_text"
android:labelFor="@+id/pre_shared_key_text"
android:text="@string/pre_shared_key" />
<EditText
android:id="@+id/pre_shared_key_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/pre_shared_key_label"
android:hint="@string/hint_optional"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={item.preSharedKey}" />
<TextView
android:id="@+id/allowed_ips_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/pre_shared_key_text"
android:layout_toStartOf="@+id/exclude_private_ips"
android:labelFor="@+id/allowed_ips_text"
android:text="@string/allowed_ips" />
<CheckBox
android:id="@+id/exclude_private_ips"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/allowed_ips_label"
android:layout_alignParentEnd="true"
android:checked="@={item.excludingPrivateIps}"
android:text="@string/exclude_private_ips"
android:visibility="@{item.ableToExcludePrivateIps ? View.VISIBLE : View.GONE}" />
<EditText
android:id="@+id/allowed_ips_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/allowed_ips_label"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={item.allowedIps}" />
<TextView
android:id="@+id/endpoint_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/allowed_ips_text"
android:layout_toStartOf="@+id/persistent_keepalive_label"
android:labelFor="@+id/endpoint_text"
android:text="@string/endpoint" />
<EditText
android:id="@+id/endpoint_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/endpoint_label"
android:layout_toStartOf="@+id/persistent_keepalive_text"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={item.endpoint}" />
<TextView
android:id="@+id/persistent_keepalive_label"
android:layout_width="96dp"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/endpoint_label"
android:layout_alignParentEnd="true"
android:labelFor="@+id/persistent_keepalive_text"
android:text="@string/persistent_keepalive" />
<EditText
android:id="@+id/persistent_keepalive_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/endpoint_text"
android:layout_alignParentEnd="true"
android:layout_alignStart="@+id/persistent_keepalive_label"
android:hint="@string/hint_optional"
android:inputType="number"
android:text="@={item.persistentKeepalive}"
android:textAlignment="center" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
</layout>
-4
View File
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="label_position">1</integer>
</resources>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- TODO(msf): remove these 2 hard-coded colors and replace with theme colors -->
<color name="fab_label_text_color">#000000</color>
<color name="fab_label_background_color">#bbbbbb</color>
<color name="accent">#5e97f6</color>
</resources>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="Multiselected">
<attr name="state_multiselected" format="boolean" />
</declare-styleable>
</resources>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- TODO(msf): remove these 2 hard-coded colors and replace with theme colors -->
<color name="fab_label_text_color">#ffffff</color>
<color name="fab_label_background_color">#444444</color>
<color name="accent">#2196F3</color>
</resources>
-4
View File
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="fab_margin">16dp</dimen>
</resources>
-31
View File
@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="fab_expand_menu_button" type="id" />
<item name="fab_label" type="id" />
<dimen name="fab_shadow_offset">3dp</dimen>
<dimen name="fab_shadow_radius">9dp</dimen>
<dimen name="fab_stroke_width">1dp</dimen>
<dimen name="fab_actions_spacing">24dp</dimen>
<dimen name="fab_labels_margin">8dp</dimen>
<declare-styleable name="LabeledFloatingActionButton">
<attr name="fab_title" format="string" />
</declare-styleable>
<declare-styleable name="FloatingActionsMenu">
<attr name="fab_labelStyle" format="reference" />
<attr name="fab_labelsPosition" format="enum">
<enum name="left" value="0" />
<enum name="right" value="1" />
</attr>
<attr name="fab_expandDirection" format="enum">
<enum name="up" value="0" />
<enum name="down" value="1" />
<enum name="left" value="2" />
<enum name="right" value="3" />
</attr>
</declare-styleable>
<integer name="label_position">0</integer>
</resources>
-26
View File
@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="AppTheme" parent="Theme.AppCompat.DayNight.DarkActionBar">
<item name="colorAccent">@color/accent</item>
</style>
<style name="NoBackgroundTheme" parent="AppTheme">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:background">@android:color/transparent</item>
<item name="colorPrimaryDark">@android:color/transparent</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:windowEnterAnimation">@android:anim/fade_in</item>
<item name="android:windowExitAnimation">@android:anim/fade_out</item>
</style>
<style name="fab_label" parent="TextAppearance.AppCompat.Inverse">
<item name="android:background">@drawable/fab_label_background</item>
<item name="android:textColor">@color/fab_label_text_color</item>
</style>
</resources>
-19
View File
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<com.wireguard.android.preference.VersionPreference android:icon="@mipmap/ic_launcher" />
<CheckBoxPreference
android:defaultValue="false"
android:key="restore_on_boot"
android:summary="@string/restore_on_boot_summary"
android:title="@string/restore_on_boot_title" />
<com.wireguard.android.preference.ModuleDownloaderPreference android:key="module_downloader" />
<com.wireguard.android.preference.ToolsInstallerPreference android:key="tools_installer" />
<com.wireguard.android.preference.ZipExporterPreference />
<com.wireguard.android.preference.LogExporterPreference />
<CheckBoxPreference
android:defaultValue="false"
android:key="dark_theme"
android:summaryOff="@string/dark_theme_summary_off"
android:summaryOn="@string/dark_theme_summary_on"
android:title="@string/dark_theme_title" />
</androidx.preference.PreferenceScreen>
-35
View File
@@ -1,35 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
#
# Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
BUILDDIR ?= $(CURDIR)/build
DESTDIR ?= $(CURDIR)/out
NDK_GO_ARCH_MAP_x86 := 386
NDK_GO_ARCH_MAP_x86_64 := amd64
NDK_GO_ARCH_MAP_arm := arm
NDK_GO_ARCH_MAP_arm64 := arm64
NDK_GO_ARCH_MAP_mips := mipsx
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 CC := $(ANDROID_C_COMPILER)
export GOARCH := $(NDK_GO_ARCH_MAP_$(ANDROID_ARCH_NAME))
export GOOS := android
export CGO_ENABLED := 1
DESIRED_GO_VERSION := 1.13.3
default: $(DESTDIR)/libwg-go.so
$(BUILDDIR)/go-$(DESIRED_GO_VERSION)/.prepared:
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 "$@"
$(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
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
-10
View File
@@ -1,10 +0,0 @@
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
)
-19
View File
@@ -1,19 +0,0 @@
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/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/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/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=
+39 -6
View File
@@ -1,17 +1,47 @@
allprojects {
buildscript {
ext {
activityVersion = '1.2.0-alpha07'
agpVersion = '4.0.1'
annotationsVersion = '1.1.0'
appcompatVersion = '1.2.0'
bintrayPluginVersion = '1.8.5'
biometricVersion = '1.1.0-alpha02'
collectionVersion = '1.1.0'
constraintLayoutVersion = '2.0.1'
coordinatorLayoutVersion = '1.1.0'
coreKtxVersion = '1.3.1'
coroutinesVersion = '1.3.9'
datastoreVersion = '1.0.0-alpha01'
desugarVersion = '1.0.10'
fragmentVersion = '1.3.0-alpha07'
jsr305Version = '3.0.2'
junitVersion = '4.13'
kotlinVersion = '1.4.10'
lifecycleRuntimeKtxVersion = '2.3.0-alpha07'
materialComponentsVersion = '22973ff10305397bfc72fed41684175fee8aeefb'
mavenPluginVersion = '2.1'
preferenceVersion = '1.1.1'
zxingEmbeddedVersion = '3.6.0'
groupName = 'com.wireguard.android'
}
dependencies {
classpath "com.android.tools.build:gradle:$agpVersion"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath "com.github.dcendents:android-maven-gradle-plugin:$mavenPluginVersion"
classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:$bintrayPluginVersion"
}
repositories {
google()
jcenter()
}
}
buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:3.5.3'
}
allprojects {
repositories {
google()
jcenter()
maven { url 'https://jitpack.io' }
}
}
@@ -21,7 +51,10 @@ task clean(type: Delete) {
tasks {
wrapper {
gradleVersion = "6.1"
gradleVersion = "6.6.1"
distributionType = Wrapper.DistributionType.ALL
distributionSha256Sum = "11657af6356b7587bfb37287b5992e94a9686d5c8a0a1b60b87b9928a2decde5"
}
}
apply from: "version.gradle"
+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.
+2 -1
View File
@@ -1,5 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1-all.zip
distributionSha256Sum=11657af6356b7587bfb37287b5992e94a9686d5c8a0a1b60b87b9928a2decde5
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+2
View File
@@ -82,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -129,6 +130,7 @@ fi
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
+2 -1
View File
@@ -1 +1,2 @@
include ':app'
include ':tunnel'
include ':ui'
+59
View File
@@ -0,0 +1,59 @@
apply plugin: 'com.android.library'
version wireguardVersionName
group groupName
android {
compileSdkVersion 30
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion 21
targetSdkVersion 30
versionCode wireguardVersionCode
versionName wireguardVersionName
}
externalNativeBuild {
cmake {
path 'tools/CMakeLists.txt'
}
}
libraryVariants.all {
it.generateBuildConfigProvider.configure { enabled = false }
}
testOptions.unitTests.all {
testLogging {
events 'passed', 'skipped', 'failed', 'standardOut', 'standardError'
}
}
buildTypes {
release {
externalNativeBuild {
cmake {
arguments "-DANDROID_PACKAGE_NAME=${groupName}", "-DGRADLE_USER_HOME=${project.gradle.gradleUserHomeDir}"
}
}
}
debug {
externalNativeBuild {
cmake {
arguments "-DANDROID_PACKAGE_NAME=${groupName}.debug", "-DGRADLE_USER_HOME=${project.gradle.gradleUserHomeDir}"
}
}
}
}
lintOptions {
disable('LongLogTag')
}
}
dependencies {
implementation "androidx.annotation:annotation:$annotationsVersion"
implementation "androidx.collection:collection:$collectionVersion"
implementation "com.google.code.findbugs:jsr305:$jsr305Version"
testImplementation "junit:junit:$junitVersion"
}
apply from: "publish.gradle"
+76
View File
@@ -0,0 +1,76 @@
apply plugin: 'com.github.dcendents.android-maven'
apply plugin: 'com.jfrog.bintray'
install {
repositories.mavenInstaller {
pom.project {
name 'WireGuard Tunnel Library'
description 'Embeddable tunnel library for WireGuard for Android'
url 'https://www.wireguard.com/'
packaging 'aar'
groupId groupName
artifactId 'tunnel'
version wireguardVersionName
licenses {
license {
name 'The Apache Software License, Version 2.0'
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
distribution 'repo'
}
}
scm {
connection 'scm:git:https://git.zx2c4.com/wireguard-android'
url 'https://git.zx2c4.com/wireguard-android'
}
organization {
name 'WireGuard'
url 'https://www.wireguard.com/'
}
}
}
}
bintray {
user = hasProperty('BINTRAY_USER') ? getProperty('BINTRAY_USER') : System.getenv('BINTRAY_USER')
key = hasProperty('BINTRAY_KEY') ? getProperty('BINTRAY_KEY') : System.getenv('BINTRAY_KEY')
configurations = [ 'archives' ]
pkg {
repo = 'wireguard-android'
name = 'wireguard-android'
userOrg = 'wireguard'
licenses = [ 'Apache-2.0' ]
vcsUrl = 'https://git.zx2c4.com/wireguard-android'
publish = true
version {
name = wireguardVersionName
}
}
}
android.libraryVariants.all { variant ->
if (variant.name == 'release') {
task javadoc(type: Javadoc) {
source = variant.javaCompileProvider.get().source
classpath = files((android.bootClasspath.join(File.pathSeparator)))
classpath += variant.javaCompileProvider.get().classpath
title = 'Embeddable WireGuard Tunnel for Android v$wireguardVersionName'
}
task javadocJar(type: Jar, dependsOn: javadoc) {
archiveClassifier = 'javadoc'
from javadoc.destinationDir
}
task sourcesJar(type: Jar) {
archiveClassifier = 'sources'
from android.sourceSets.main.java.srcDirs
}
artifacts {
archives sourcesJar
archives javadocJar
}
}
}
+18
View File
@@ -0,0 +1,18 @@
<!--
~ Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
~ SPDX-License-Identifier: Apache-2.0
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.wireguard.android.tunnel">
<application>
<service
android:name="com.wireguard.android.backend.GoBackend$VpnService"
android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
</application>
</manifest>
@@ -5,43 +5,34 @@
package com.wireguard.android.backend;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.model.Tunnel.Statistics;
import com.wireguard.config.Config;
import com.wireguard.util.NonNullForAll;
import java.util.Set;
import androidx.annotation.Nullable;
/**
* Interface for implementations of the WireGuard secure network tunnel.
*/
@NonNullForAll
public interface Backend {
/**
* Update the volatile configuration of a running tunnel and return the resulting configuration.
* If the tunnel is not up, return the configuration that would result (if known), or else
* simply return the given configuration.
*
* @param tunnel The tunnel to apply the configuration to.
* @param config The new configuration for this tunnel.
* @return The updated configuration of the tunnel.
*/
Config applyConfig(Tunnel tunnel, Config config) throws Exception;
/**
* Enumerate the names of currently-running tunnels.
* Enumerate names of currently-running tunnels.
*
* @return The set of running tunnel names.
*/
Set<String> enumerate();
Set<String> getRunningTunnelNames();
/**
* Get the actual state of a tunnel.
* Get the state of a tunnel.
*
* @param tunnel The tunnel to examine the state of.
* @return The state of the tunnel.
* @throws Exception Exception raised when retrieving tunnel's state.
*/
State getState(Tunnel tunnel) throws Exception;
Tunnel.State getState(Tunnel tunnel) throws Exception;
/**
* Get statistics about traffic and errors on this tunnel. If the tunnel is not running, the
@@ -49,31 +40,28 @@ public interface Backend {
*
* @param tunnel The tunnel to retrieve statistics for.
* @return The statistics for the tunnel.
* @throws Exception Exception raised when retrieving statistics.
*/
Statistics getStatistics(Tunnel tunnel) throws Exception;
/**
* Determine type name of underlying backend.
*
* @return Type name
*/
String getTypePrettyName();
/**
* Determine version of underlying backend.
*
* @return The version of the backend.
* @throws Exception
* @throws Exception Exception raised while retrieving version.
*/
String getVersion() throws Exception;
/**
* Set the state of a tunnel.
* Set the state of a tunnel, updating it's configuration. If the tunnel is already up, config
* may update the running configuration; config may be null when setting the tunnel down.
*
* @param tunnel The tunnel to control the state of.
* @param state The new state for this tunnel. Must be {@code UP}, {@code DOWN}, or
* {@code TOGGLE}.
* @param config The configuration for this tunnel, may be null if state is {@code DOWN}.
* @return The updated state of the tunnel.
* @throws Exception Exception raised while changing state.
*/
State setState(Tunnel tunnel, State state) throws Exception;
Tunnel.State setState(Tunnel tunnel, Tunnel.State state, @Nullable Config config) throws Exception;
}
@@ -0,0 +1,60 @@
/*
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.backend;
import com.wireguard.util.NonNullForAll;
/**
* A subclass of {@link Exception} that encapsulates the reasons for a failure originating in
* implementations of {@link Backend}.
*/
@NonNullForAll
public final class BackendException extends Exception {
private final Object[] format;
private final Reason reason;
/**
* Public constructor for BackendException.
*
* @param reason The {@link Reason} which caused this exception to be thrown
* @param format Format string values used when converting exceptions to user-facing strings.
*/
public BackendException(final Reason reason, final Object... format) {
this.reason = reason;
this.format = format;
}
/**
* Get the format string values associated with the instance.
*
* @return Array of {@link Object} for string formatting purposes
*/
public Object[] getFormat() {
return format;
}
/**
* Get the reason for this exception.
*
* @return Associated {@link Reason} for this exception.
*/
public Reason getReason() {
return reason;
}
/**
* Enum class containing all known reasons for why a {@link BackendException} might be thrown.
*/
public enum Reason {
UNKNOWN_KERNEL_MODULE_NAME,
WG_QUICK_CONFIG_ERROR_CODE,
TUNNEL_MISSING_CONFIG,
VPN_NOT_AUTHORIZED,
UNABLE_TO_START_VPN,
TUN_CREATION_ERROR,
GO_ACTIVATION_ERROR_CODE
}
}
@@ -5,51 +5,69 @@
package com.wireguard.android.backend;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import androidx.annotation.Nullable;
import androidx.collection.ArraySet;
import android.system.OsConstants;
import android.util.Log;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.activity.MainActivity;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.model.Tunnel.Statistics;
import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.android.backend.BackendException.Reason;
import com.wireguard.android.backend.Tunnel.State;
import com.wireguard.android.util.SharedLibraryLoader;
import com.wireguard.config.Config;
import com.wireguard.config.InetNetwork;
import com.wireguard.config.Peer;
import com.wireguard.crypto.Key;
import com.wireguard.crypto.KeyFormatException;
import com.wireguard.util.NonNullForAll;
import java.net.InetAddress;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java9.util.concurrent.CompletableFuture;
import androidx.annotation.Nullable;
import androidx.collection.ArraySet;
/**
* Implementation of {@link Backend} that uses the wireguard-go userspace implementation to provide
* WireGuard tunnels.
*/
@NonNullForAll
public final class GoBackend implements Backend {
private static final String TAG = "WireGuard/" + GoBackend.class.getSimpleName();
private static CompletableFuture<VpnService> vpnService = new CompletableFuture<>();
private static final String TAG = "WireGuard/GoBackend";
@Nullable private static AlwaysOnCallback alwaysOnCallback;
private static GhettoCompletableFuture<VpnService> vpnService = new GhettoCompletableFuture<>();
private final Context context;
@Nullable private Config currentConfig;
@Nullable private Tunnel currentTunnel;
private int currentTunnelHandle = -1;
/**
* Public constructor for GoBackend.
*
* @param context An Android {@link Context}
*/
public GoBackend(final Context context) {
SharedLibraryLoader.loadSharedLibrary(context, "wg-go");
this.context = context;
}
/**
* Set a {@link AlwaysOnCallback} to be invoked when {@link VpnService} is started by the
* system's Always-On VPN mode.
*
* @param cb Callback to be invoked
*/
public static void setAlwaysOnCallback(final AlwaysOnCallback cb) {
alwaysOnCallback = cb;
}
private static native String wgGetConfig(int handle);
private static native int wgGetSocketV4(int handle);
@@ -62,24 +80,13 @@ public final class GoBackend implements Backend {
private static native String wgVersion();
/**
* Method to get the names of running tunnels.
*
* @return A set of string values denoting names of running tunnels.
*/
@Override
public Config applyConfig(final Tunnel tunnel, final Config config) throws Exception {
if (tunnel.getState() == State.UP) {
// Restart the tunnel to apply the new config.
setStateInternal(tunnel, tunnel.getConfig(), State.DOWN);
try {
setStateInternal(tunnel, config, State.UP);
} catch (final Exception e) {
// The new configuration didn't work, so try to go back to the old one.
setStateInternal(tunnel, tunnel.getConfig(), State.UP);
throw e;
}
}
return config;
}
@Override
public Set<String> enumerate() {
public Set<String> getRunningTunnelNames() {
if (currentTunnel != null) {
final Set<String> runningTunnels = new ArraySet<>();
runningTunnels.add(currentTunnel.getName());
@@ -88,11 +95,23 @@ public final class GoBackend implements Backend {
return Collections.emptySet();
}
/**
* Get the associated {@link State} for a given {@link Tunnel}.
*
* @param tunnel The tunnel to examine the state of.
* @return {@link State} associated with the given tunnel.
*/
@Override
public State getState(final Tunnel tunnel) {
return currentTunnel == tunnel ? State.UP : State.DOWN;
}
/**
* Get the associated {@link Statistics} for a given {@link Tunnel}.
*
* @param tunnel The tunnel to retrieve statistics for.
* @return {@link Statistics} associated with the given tunnel.
*/
@Override
public Statistics getStatistics(final Tunnel tunnel) {
final Statistics stats = new Statistics();
@@ -101,7 +120,8 @@ public final class GoBackend implements Backend {
}
final String config = wgGetConfig(currentTunnelHandle);
Key key = null;
long rx = 0, tx = 0;
long rx = 0;
long tx = 0;
for (final String line : config.split("\\n")) {
if (line.startsWith("public_key=")) {
if (key != null)
@@ -136,40 +156,62 @@ public final class GoBackend implements Backend {
return stats;
}
@Override
public String getTypePrettyName() {
return context.getString(R.string.type_name_go_userspace);
}
/**
* Get the version of the underlying wireguard-go library.
*
* @return {@link String} value of the version of the wireguard-go library.
*/
@Override
public String getVersion() {
return wgVersion();
}
/**
* Change the state of a given {@link Tunnel}, optionally applying a given {@link Config}.
*
* @param tunnel The tunnel to control the state of.
* @param state The new state for this tunnel. Must be {@code UP}, {@code DOWN}, or
* {@code TOGGLE}.
* @param config The configuration for this tunnel, may be null if state is {@code DOWN}.
* @return {@link State} of the tunnel after state changes are applied.
* @throws Exception Exception raised while changing tunnel state.
*/
@Override
public State setState(final Tunnel tunnel, State state) throws Exception {
public State setState(final Tunnel tunnel, State state, @Nullable final Config config) throws Exception {
final State originalState = getState(tunnel);
if (state == State.TOGGLE)
state = originalState == State.UP ? State.DOWN : State.UP;
if (state == originalState)
if (state == originalState && tunnel == currentTunnel && config == currentConfig)
return originalState;
if (state == State.UP && currentTunnel != null)
throw new IllegalStateException(context.getString(R.string.multiple_tunnels_error));
Log.d(TAG, "Changing tunnel " + tunnel.getName() + " to state " + state);
setStateInternal(tunnel, tunnel.getConfig(), state);
if (state == State.UP) {
final Config originalConfig = currentConfig;
final Tunnel originalTunnel = currentTunnel;
if (currentTunnel != null)
setStateInternal(currentTunnel, null, State.DOWN);
try {
setStateInternal(tunnel, config, state);
} catch (final Exception e) {
if (originalTunnel != null)
setStateInternal(originalTunnel, originalConfig, State.UP);
throw e;
}
} else if (state == State.DOWN && tunnel == currentTunnel) {
setStateInternal(tunnel, null, State.DOWN);
}
return getState(tunnel);
}
private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state)
throws Exception {
Log.i(TAG, "Bringing tunnel " + tunnel.getName() + ' ' + state);
if (state == State.UP) {
Log.i(TAG, "Bringing tunnel up");
Objects.requireNonNull(config, context.getString(R.string.no_config_error));
if (config == null)
throw new BackendException(Reason.TUNNEL_MISSING_CONFIG);
if (VpnService.prepare(context) != null)
throw new Exception(context.getString(R.string.vpn_not_authorized_error));
throw new BackendException(Reason.VPN_NOT_AUTHORIZED);
final VpnService service;
if (!vpnService.isDone())
@@ -178,8 +220,11 @@ public final class GoBackend implements Backend {
try {
service = vpnService.get(2, TimeUnit.SECONDS);
} catch (final TimeoutException e) {
throw new Exception(context.getString(R.string.vpn_start_error), e);
final Exception be = new BackendException(Reason.UNABLE_TO_START_VPN);
be.initCause(e);
throw be;
}
service.setOwner(this);
if (currentTunnelHandle != -1) {
Log.w(TAG, "Tunnel already up");
@@ -193,22 +238,31 @@ public final class GoBackend implements Backend {
final VpnService.Builder builder = service.getBuilder();
builder.setSession(tunnel.getName());
final Intent configureIntent = new Intent(context, MainActivity.class);
configureIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
builder.setConfigureIntent(PendingIntent.getActivity(context, 0, configureIntent, 0));
for (final String excludedApplication : config.getInterface().getExcludedApplications())
builder.addDisallowedApplication(excludedApplication);
for (final String includedApplication : config.getInterface().getIncludedApplications())
builder.addAllowedApplication(includedApplication);
for (final InetNetwork addr : config.getInterface().getAddresses())
builder.addAddress(addr.getAddress(), addr.getMask());
for (final InetAddress addr : config.getInterface().getDnsServers())
builder.addDnsServer(addr.getHostAddress());
boolean sawDefaultRoute = false;
for (final Peer peer : config.getPeers()) {
for (final InetNetwork addr : peer.getAllowedIps())
for (final InetNetwork addr : peer.getAllowedIps()) {
if (addr.getMask() == 0)
sawDefaultRoute = true;
builder.addRoute(addr.getAddress(), addr.getMask());
}
}
// "Kill-switch" semantics
if (!(sawDefaultRoute && config.getPeers().size() == 1)) {
builder.allowFamily(OsConstants.AF_INET);
builder.allowFamily(OsConstants.AF_INET6);
}
builder.setMtu(config.getInterface().getMtu().orElse(1280));
@@ -216,25 +270,24 @@ public final class GoBackend implements Backend {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
builder.setMetered(false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
service.setUnderlyingNetworks(null);
service.setUnderlyingNetworks(null);
builder.setBlocking(true);
try (final ParcelFileDescriptor tun = builder.establish()) {
if (tun == null)
throw new Exception(context.getString(R.string.tun_create_error));
throw new BackendException(Reason.TUN_CREATION_ERROR);
Log.d(TAG, "Go backend v" + wgVersion());
currentTunnelHandle = wgTurnOn(tunnel.getName(), tun.detachFd(), goConfig);
}
if (currentTunnelHandle < 0)
throw new Exception(context.getString(R.string.tunnel_on_error, currentTunnelHandle));
throw new BackendException(Reason.GO_ACTIVATION_ERROR_CODE, currentTunnelHandle);
currentTunnel = tunnel;
currentConfig = config;
service.protect(wgGetSocketV4(currentTunnelHandle));
service.protect(wgGetSocketV6(currentTunnelHandle));
} else {
Log.i(TAG, "Bringing tunnel down");
if (currentTunnelHandle == -1) {
Log.w(TAG, "Tunnel already down");
return;
@@ -243,7 +296,10 @@ public final class GoBackend implements Backend {
wgTurnOff(currentTunnelHandle);
currentTunnel = null;
currentTunnelHandle = -1;
currentConfig = null;
}
tunnel.onStateChange(state);
}
private void startVpnService() {
@@ -251,7 +307,49 @@ public final class GoBackend implements Backend {
context.startService(new Intent(context, VpnService.class));
}
/**
* Callback for {@link GoBackend} that is invoked when {@link VpnService} is started by the
* system's Always-On VPN mode.
*/
public interface AlwaysOnCallback {
void alwaysOnTriggered();
}
// TODO: When we finally drop API 21 and move to API 24, delete this and replace with the ordinary CompletableFuture.
private static final class GhettoCompletableFuture<V> {
private final LinkedBlockingQueue<V> completion = new LinkedBlockingQueue<>(1);
private final FutureTask<V> result = new FutureTask<>(completion::peek);
public boolean complete(final V value) {
final boolean offered = completion.offer(value);
if (offered)
result.run();
return offered;
}
public V get() throws ExecutionException, InterruptedException {
return result.get();
}
public V get(final long timeout, final TimeUnit unit) throws ExecutionException, InterruptedException, TimeoutException {
return result.get(timeout, unit);
}
public boolean isDone() {
return !completion.isEmpty();
}
public GhettoCompletableFuture<V> newIncompleteFuture() {
return new GhettoCompletableFuture<>();
}
}
/**
* {@link android.net.VpnService} implementation for {@link GoBackend}
*/
public static class VpnService extends android.net.VpnService {
@Nullable private GoBackend owner;
public Builder getBuilder() {
return new Builder();
}
@@ -264,13 +362,17 @@ public final class GoBackend implements Backend {
@Override
public void onDestroy() {
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
for (final Tunnel tunnel : tunnels) {
if (tunnel != null && tunnel.getState() != State.DOWN)
tunnel.setState(State.DOWN);
if (owner != null) {
final Tunnel tunnel = owner.currentTunnel;
if (tunnel != null) {
if (owner.currentTunnelHandle != -1)
wgTurnOff(owner.currentTunnelHandle);
owner.currentTunnel = null;
owner.currentTunnelHandle = -1;
owner.currentConfig = null;
tunnel.onStateChange(State.DOWN);
}
});
}
vpnService = vpnService.newIncompleteFuture();
super.onDestroy();
}
@@ -280,10 +382,14 @@ public final class GoBackend implements Backend {
vpnService.complete(this);
if (intent == null || intent.getComponent() == null || !intent.getComponent().getPackageName().equals(getPackageName())) {
Log.d(TAG, "Service started by Always-on VPN feature");
Application.getTunnelManager().restoreState(true).whenComplete(ExceptionLoggers.D);
if (alwaysOnCallback != null)
alwaysOnCallback.alwaysOnTriggered();
}
return super.onStartCommand(intent, flags, startId);
}
public void setOwner(final GoBackend owner) {
this.owner = owner;
}
}
}
@@ -0,0 +1,112 @@
/*
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.backend;
import android.os.SystemClock;
import android.util.Pair;
import com.wireguard.crypto.Key;
import com.wireguard.util.NonNullForAll;
import java.util.HashMap;
import java.util.Map;
/**
* Class representing transfer statistics for a {@link Tunnel} instance.
*/
@NonNullForAll
public class Statistics {
private final Map<Key, Pair<Long, Long>> peerBytes = new HashMap<>();
private long lastTouched = SystemClock.elapsedRealtime();
Statistics() {
}
/**
* Add a peer and its current data usage to the internal map.
*
* @param key A WireGuard public key bound to a particular peer
* @param rx The received traffic for the {@link com.wireguard.config.Peer} referenced by
* the provided {@link Key}. This value is in bytes
* @param tx The transmitted traffic for the {@link com.wireguard.config.Peer} referenced by
* the provided {@link Key}. This value is in bytes.
*/
void add(final Key key, final long rx, final long tx) {
peerBytes.put(key, Pair.create(rx, tx));
lastTouched = SystemClock.elapsedRealtime();
}
/**
* Check if the statistics are stale, indicating the need for the {@link Backend} to update them.
*
* @return boolean indicating if the current statistics instance has stale values.
*/
public boolean isStale() {
return SystemClock.elapsedRealtime() - lastTouched > 900;
}
/**
* Get the received traffic (in bytes) for the {@link com.wireguard.config.Peer} referenced by
* the provided {@link Key}
*
* @param peer A {@link Key} representing a {@link com.wireguard.config.Peer}.
* @return a long representing the number of bytes received by this peer.
*/
public long peerRx(final Key peer) {
if (!peerBytes.containsKey(peer))
return 0;
return peerBytes.get(peer).first;
}
/**
* Get the transmitted traffic (in bytes) for the {@link com.wireguard.config.Peer} referenced by
* the provided {@link Key}
*
* @param peer A {@link Key} representing a {@link com.wireguard.config.Peer}.
* @return a long representing the number of bytes transmitted by this peer.
*/
public long peerTx(final Key peer) {
if (!peerBytes.containsKey(peer))
return 0;
return peerBytes.get(peer).second;
}
/**
* Get the list of peers being tracked by this instance.
*
* @return An array of {@link Key} instances representing WireGuard
* {@link com.wireguard.config.Peer}s
*/
public Key[] peers() {
return peerBytes.keySet().toArray(new Key[0]);
}
/**
* Get the total received traffic by all the peers being tracked by this instance
*
* @return a long representing the number of bytes received by the peers being tracked.
*/
public long totalRx() {
long rx = 0;
for (final Pair<Long, Long> val : peerBytes.values()) {
rx += val.first;
}
return rx;
}
/**
* Get the total transmitted traffic by all the peers being tracked by this instance
*
* @return a long representing the number of bytes transmitted by the peers being tracked.
*/
public long totalTx() {
long tx = 0;
for (final Pair<Long, Long> val : peerBytes.values()) {
tx += val.second;
}
return tx;
}
}
@@ -0,0 +1,57 @@
/*
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.backend;
import com.wireguard.util.NonNullForAll;
import java.util.regex.Pattern;
/**
* Represents a WireGuard tunnel.
*/
@NonNullForAll
public interface Tunnel {
int NAME_MAX_LENGTH = 15;
Pattern NAME_PATTERN = Pattern.compile("[a-zA-Z0-9_=+.-]{1,15}");
static boolean isNameInvalid(final CharSequence name) {
return !NAME_PATTERN.matcher(name).matches();
}
/**
* Get the name of the tunnel, which should always pass the !isNameInvalid test.
*
* @return The name of the tunnel.
*/
String getName();
/**
* React to a change in state of the tunnel. Should only be directly called by Backend.
*
* @param newState The new state of the tunnel.
*/
void onStateChange(State newState);
/**
* Enum class to represent all possible states of a {@link Tunnel}.
*/
enum State {
DOWN,
TOGGLE,
UP;
/**
* Get the state of a {@link Tunnel}
*
* @param running boolean indicating if the tunnel is running.
* @return State of the tunnel based on whether or not it is running.
*/
public static State of(final boolean running) {
return running ? UP : DOWN;
}
}
}
@@ -0,0 +1,190 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.backend;
import android.content.Context;
import android.util.Log;
import android.util.Pair;
import com.wireguard.android.backend.BackendException.Reason;
import com.wireguard.android.backend.Tunnel.State;
import com.wireguard.android.util.RootShell;
import com.wireguard.android.util.ToolsInstaller;
import com.wireguard.config.Config;
import com.wireguard.crypto.Key;
import com.wireguard.util.NonNullForAll;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import androidx.annotation.Nullable;
/**
* WireGuard backend that uses {@code wg-quick} to implement tunnel configuration.
*/
@NonNullForAll
public final class WgQuickBackend implements Backend {
private static final String TAG = "WireGuard/WgQuickBackend";
private final File localTemporaryDir;
private final RootShell rootShell;
private final Map<Tunnel, Config> runningConfigs = new HashMap<>();
private final ToolsInstaller toolsInstaller;
private boolean multipleTunnels;
public WgQuickBackend(final Context context, final RootShell rootShell, final ToolsInstaller toolsInstaller) {
localTemporaryDir = new File(context.getCacheDir(), "tmp");
this.rootShell = rootShell;
this.toolsInstaller = toolsInstaller;
}
@Override
public Set<String> getRunningTunnelNames() {
final List<String> output = new ArrayList<>();
// Don't throw an exception here or nothing will show up in the UI.
try {
toolsInstaller.ensureToolsAvailable();
if (rootShell.run(output, "wg show interfaces") != 0 || output.isEmpty())
return Collections.emptySet();
} catch (final Exception e) {
Log.w(TAG, "Unable to enumerate running tunnels", e);
return Collections.emptySet();
}
// wg puts all interface names on the same line. Split them into separate elements.
return Set.of(output.get(0).split(" "));
}
@Override
public State getState(final Tunnel tunnel) {
return getRunningTunnelNames().contains(tunnel.getName()) ? State.UP : State.DOWN;
}
@Override
public Statistics getStatistics(final Tunnel tunnel) {
final Statistics stats = new Statistics();
final Collection<String> output = new ArrayList<>();
try {
if (rootShell.run(output, String.format("wg show '%s' transfer", tunnel.getName())) != 0)
return stats;
} catch (final Exception ignored) {
return stats;
}
for (final String line : output) {
final String[] parts = line.split("\\t");
if (parts.length != 3)
continue;
try {
stats.add(Key.fromBase64(parts[0]), Long.parseLong(parts[1]), Long.parseLong(parts[2]));
} catch (final Exception ignored) {
}
}
return stats;
}
@Override
public String getVersion() throws Exception {
final List<String> output = new ArrayList<>();
if (rootShell.run(output, "cat /sys/module/wireguard/version") != 0 || output.isEmpty())
throw new BackendException(Reason.UNKNOWN_KERNEL_MODULE_NAME);
return output.get(0);
}
public void setMultipleTunnels(final boolean on) {
multipleTunnels = on;
}
@Override
public State setState(final Tunnel tunnel, State state, @Nullable final Config config) throws Exception {
final State originalState = getState(tunnel);
final Config originalConfig = runningConfigs.get(tunnel);
final Map<Tunnel, Config> runningConfigsSnapshot = new HashMap<>(runningConfigs);
if (state == State.TOGGLE)
state = originalState == State.UP ? State.DOWN : State.UP;
if ((state == State.UP && originalState == State.UP && originalConfig != null && originalConfig == config) ||
(state == State.DOWN && originalState == State.DOWN))
return originalState;
if (state == State.UP) {
toolsInstaller.ensureToolsAvailable();
if (!multipleTunnels && originalState == State.DOWN) {
final List<Pair<Tunnel, Config>> rewind = new LinkedList<>();
try {
for (final Map.Entry<Tunnel, Config> entry : runningConfigsSnapshot.entrySet()) {
setStateInternal(entry.getKey(), entry.getValue(), State.DOWN);
rewind.add(Pair.create(entry.getKey(), entry.getValue()));
}
} catch (final Exception e) {
try {
for (final Pair<Tunnel, Config> entry : rewind) {
setStateInternal(entry.first, entry.second, State.UP);
}
} catch (final Exception ignored) {
}
throw e;
}
}
if (originalState == State.UP)
setStateInternal(tunnel, originalConfig == null ? config : originalConfig, State.DOWN);
try {
setStateInternal(tunnel, config, State.UP);
} catch (final Exception e) {
try {
if (originalState == State.UP && originalConfig != null) {
setStateInternal(tunnel, originalConfig, State.UP);
}
if (!multipleTunnels && originalState == State.DOWN) {
for (final Map.Entry<Tunnel, Config> entry : runningConfigsSnapshot.entrySet()) {
setStateInternal(entry.getKey(), entry.getValue(), State.UP);
}
}
} catch (final Exception ignored) {
}
throw e;
}
} else if (state == State.DOWN) {
setStateInternal(tunnel, originalConfig == null ? config : originalConfig, State.DOWN);
}
return state;
}
private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state) throws Exception {
Log.i(TAG, "Bringing tunnel " + tunnel.getName() + ' ' + state);
Objects.requireNonNull(config, "Trying to set state up with a null config");
final File tempFile = new File(localTemporaryDir, tunnel.getName() + ".conf");
try (final FileOutputStream stream = new FileOutputStream(tempFile, false)) {
stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8));
}
String command = String.format("wg-quick %s '%s'",
state.toString().toLowerCase(Locale.ENGLISH), tempFile.getAbsolutePath());
if (state == State.UP)
command = "cat /sys/module/wireguard/version && " + command;
final int result = rootShell.run(null, command);
// noinspection ResultOfMethodCallIgnored
tempFile.delete();
if (result != 0)
throw new BackendException(Reason.WG_QUICK_CONFIG_ERROR_CODE, result);
if (state == State.UP)
runningConfigs.put(tunnel, config);
else
runningConfigs.remove(tunnel);
tunnel.onStateChange(state);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2019 WireGuard LLC. All Rights Reserved.
* Copyright © 2019-2020 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
@@ -9,14 +9,9 @@ import android.content.Context;
import android.system.OsConstants;
import android.util.Base64;
import com.wireguard.android.Application;
import com.wireguard.android.util.RootShell.NoRootException;
import net.i2p.crypto.eddsa.EdDSAEngine;
import net.i2p.crypto.eddsa.EdDSAPublicKey;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec;
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
import com.wireguard.android.util.RootShell.RootShellException;
import com.wireguard.crypto.Ed25519;
import com.wireguard.util.NonNullForAll;
import java.io.File;
import java.io.FileOutputStream;
@@ -28,112 +23,75 @@ import java.nio.charset.StandardCharsets;
import java.security.InvalidParameterException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import androidx.annotation.Nullable;
/**
* Class that implements the logic for downloading and loading signed, prebuilt modules for
* WireGuard into the running kernel.
*/
@NonNullForAll
@SuppressWarnings("MagicNumber")
public class ModuleLoader {
private static final String MODULE_PUBLIC_KEY_BASE64 = "RWRmHuT9PSqtwfsLtEx+QS06BJtLgFYteL9WCNjH7yuyu5Y1DieSN7If";
private static final String MODULE_LIST_URL = "https://download.wireguard.com/android-module/modules.txt.sig";
private static final String MODULE_URL = "https://download.wireguard.com/android-module/%s";
private static final String MODULE_NAME = "wireguard-%s.ko";
private static final String MODULE_PUBLIC_KEY_BASE64 = "RWRmHuT9PSqtwfsLtEx+QS06BJtLgFYteL9WCNjH7yuyu5Y1DieSN7If";
private static final String MODULE_URL = "https://download.wireguard.com/android-module/%s";
private final File moduleDir;
private final RootShell rootShell;
private final File tmpDir;
private final String userAgent;
public ModuleLoader(final Context context) {
/**
* Public constructor for ModuleLoader
*
* @param context A {@link Context} instance.
* @param rootShell A {@link RootShell} instance used to run elevated commands required for module
* loading.
* @param userAgent A {@link String} that represents the User-Agent string used for connections
* to the upstream server.
*/
public ModuleLoader(final Context context, final RootShell rootShell, final String userAgent) {
moduleDir = new File(context.getCacheDir(), "kmod");
tmpDir = new File(context.getCacheDir(), "tmp");
this.rootShell = rootShell;
this.userAgent = userAgent;
}
public boolean moduleMightExist() {
return moduleDir.exists() && moduleDir.isDirectory();
}
public void loadModule() throws IOException, NoRootException {
Application.getRootShell().run(null, String.format("insmod \"%s/wireguard-$(sha256sum /proc/version|cut -d ' ' -f 1).ko\"", moduleDir.getAbsolutePath()));
}
public boolean isModuleLoaded() {
/**
* Check whether a WireGuard module is already loaded into the kernel.
*
* @return boolean indicating if WireGuard is already enabled in the kernel.
*/
public static boolean isModuleLoaded() {
return new File("/sys/module/wireguard").exists();
}
private static final class Sha256Digest {
private byte[] bytes;
private Sha256Digest(final String hex) {
if (hex.length() != 64)
throw new InvalidParameterException("SHA256 hashes must be 32 bytes long");
bytes = new byte[32];
for (int i = 0; i < 32; ++i)
bytes[i] = (byte)Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16);
}
}
@Nullable
private Map<String, Sha256Digest> verifySignedHashes(final String signifyDigest) {
final byte[] publicKeyBytes = Base64.decode(MODULE_PUBLIC_KEY_BASE64, Base64.DEFAULT);
if (publicKeyBytes == null || publicKeyBytes.length != 32 + 10 || publicKeyBytes[0] != 'E' || publicKeyBytes[1] != 'd')
return null;
final String[] lines = signifyDigest.split("\n", 3);
if (lines.length != 3)
return null;
if (!lines[0].startsWith("untrusted comment: "))
return null;
final byte[] signatureBytes = Base64.decode(lines[1], Base64.DEFAULT);
if (signatureBytes == null || signatureBytes.length != 64 + 10)
return null;
for (int i = 0; i < 10; ++i) {
if (signatureBytes[i] != publicKeyBytes[i])
return null;
}
try {
EdDSAParameterSpec parameterSpec = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519);
Signature signature = new EdDSAEngine(MessageDigest.getInstance(parameterSpec.getHashAlgorithm()));
byte[] rawPublicKeyBytes = new byte[32];
System.arraycopy(publicKeyBytes, 10, rawPublicKeyBytes, 0, 32);
signature.initVerify(new EdDSAPublicKey(new EdDSAPublicKeySpec(rawPublicKeyBytes, parameterSpec)));
signature.update(lines[2].getBytes(StandardCharsets.UTF_8));
if (!signature.verify(signatureBytes, 10, 64))
return null;
} catch (final Exception ignored) {
return null;
}
Map<String, Sha256Digest> hashes = new HashMap<>();
for (final String line : lines[2].split("\n")) {
final String[] components = line.split(" ", 2);
if (components.length != 2)
return null;
try {
hashes.put(components[1], new Sha256Digest(components[0]));
} catch (final Exception ignored) {
return null;
}
}
return hashes;
}
public Integer download() throws IOException, NoRootException, NoSuchAlgorithmException {
/**
* Download the correct WireGuard module for the device
*
* @return {@link OsConstants}.EXIT_SUCCESS if everything succeeds, ENOENT otherwise.
* @throws IOException if the remote hash list was not found or empty.
* @throws RootShellException if {@link RootShell} has a failure executing elevated commands.
* @throws NoSuchAlgorithmException if SHA256 algorithm is not available in device JDK.
*/
public Integer download() throws IOException, RootShellException, NoSuchAlgorithmException {
final List<String> output = new ArrayList<>();
Application.getRootShell().run(output, "sha256sum /proc/version|cut -d ' ' -f 1");
rootShell.run(output, "sha256sum /proc/version|cut -d ' ' -f 1");
if (output.size() != 1 || output.get(0).length() != 64)
throw new InvalidParameterException("Invalid sha256 of /proc/version");
final String moduleName = String.format(MODULE_NAME, output.get(0));
HttpURLConnection connection = (HttpURLConnection)new URL(MODULE_LIST_URL).openConnection();
connection.setRequestProperty("User-Agent", Application.USER_AGENT);
HttpURLConnection connection = (HttpURLConnection) new URL(MODULE_LIST_URL).openConnection();
connection.setRequestProperty("User-Agent", userAgent);
connection.connect();
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK)
throw new IOException("Hash list could not be found");
byte[] input = new byte[1024 * 1024 * 3 /* 3MiB */];
final byte[] input = new byte[1024 * 1024 * 3 /* 3MiB */];
int len;
try (final InputStream inputStream = connection.getInputStream()) {
len = inputStream.read(input);
@@ -145,8 +103,8 @@ public class ModuleLoader {
throw new InvalidParameterException("The signature did not verify or invalid hash list format");
if (!modules.containsKey(moduleName))
return OsConstants.ENOENT;
connection = (HttpURLConnection)new URL(String.format(MODULE_URL, moduleName)).openConnection();
connection.setRequestProperty("User-Agent", Application.USER_AGENT);
connection = (HttpURLConnection) new URL(String.format(MODULE_URL, moduleName)).openConnection();
connection.setRequestProperty("User-Agent", userAgent);
connection.connect();
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK)
throw new IOException("Module file could not be found, despite being on hash list");
@@ -156,7 +114,7 @@ public class ModuleLoader {
File tempFile = null;
try {
tempFile = File.createTempFile("UNVERIFIED-", null, tmpDir);
MessageDigest digest = MessageDigest.getInstance("SHA-256");
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (final InputStream inputStream = connection.getInputStream();
final FileOutputStream outputStream = new FileOutputStream(tempFile)) {
int total = 0;
@@ -180,4 +138,74 @@ public class ModuleLoader {
}
return OsConstants.EXIT_SUCCESS;
}
/**
* Load the downloaded module. ModuleLoader#download must be called before this.
*
* @throws IOException if {@link RootShell} has a failure executing elevated commands.
* @throws RootShellException if {@link RootShell} has a failure executing elevated commands.
*/
public void loadModule() throws IOException, RootShellException {
rootShell.run(null, String.format("insmod \"%s/wireguard-$(sha256sum /proc/version|cut -d ' ' -f 1).ko\"", moduleDir.getAbsolutePath()));
}
/**
* Check if the module might already exist in the app's data.
*
* @return boolean indicating whether downloadable module might exist already.
*/
public boolean moduleMightExist() {
return moduleDir.exists() && moduleDir.isDirectory();
}
@Nullable
private Map<String, Sha256Digest> verifySignedHashes(final String signifyDigest) {
byte[] publicKeyBytes = Base64.decode(MODULE_PUBLIC_KEY_BASE64, Base64.DEFAULT);
if (publicKeyBytes == null || publicKeyBytes.length != 32 + 10 || publicKeyBytes[0] != 'E' || publicKeyBytes[1] != 'd')
return null;
final String[] lines = signifyDigest.split("\n", 3);
if (lines.length != 3)
return null;
if (!lines[0].startsWith("untrusted comment: "))
return null;
byte[] signatureBytes = Base64.decode(lines[1], Base64.DEFAULT);
if (signatureBytes == null || signatureBytes.length != 64 + 10)
return null;
for (int i = 0; i < 10; ++i) {
if (signatureBytes[i] != publicKeyBytes[i])
return null;
}
publicKeyBytes = Arrays.copyOfRange(publicKeyBytes, 10, 10 + 32);
signatureBytes = Arrays.copyOfRange(signatureBytes, 10, 10 + 64);
if (!Ed25519.verify(lines[2].getBytes(StandardCharsets.UTF_8), signatureBytes, publicKeyBytes))
return null;
final Map<String, Sha256Digest> hashes = new HashMap<>();
for (final String line : lines[2].split("\n")) {
final String[] components = line.split(" {2}", 2);
if (components.length != 2)
return null;
try {
hashes.put(components[1], new Sha256Digest(components[0]));
} catch (final Exception ignored) {
return null;
}
}
return hashes;
}
private static final class Sha256Digest {
private final byte[] bytes;
private Sha256Digest(final String hex) {
if (hex.length() != 64)
throw new InvalidParameterException("SHA256 hashes must be 32 bytes long");
bytes = new byte[32];
for (int i = 0; i < 32; ++i)
bytes[i] = (byte) Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16);
}
}
}
@@ -6,15 +6,13 @@
package com.wireguard.android.util;
import android.content.Context;
import androidx.annotation.Nullable;
import android.util.Log;
import com.wireguard.android.BuildConfig;
import com.wireguard.android.R;
import com.wireguard.android.util.RootShell.RootShellException.Reason;
import com.wireguard.util.NonNullForAll;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
@@ -22,16 +20,17 @@ import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.UUID;
import androidx.annotation.Nullable;
/**
* Helper class for running commands as root.
*/
@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 Context context;
private final String deviceNotRootedMessage;
private final File localBinaryDir;
private final File localTemporaryDir;
private final Object lock = new Object();
@@ -42,12 +41,10 @@ public class RootShell {
@Nullable private BufferedReader stdout;
public RootShell(final Context context) {
deviceNotRootedMessage = context.getString(R.string.error_root);
localBinaryDir = new File(context.getCodeCacheDir(), "bin");
localTemporaryDir = new File(context.getCacheDir(), "tmp");
preamble = String.format("export CALLING_PACKAGE=%s PATH=\"%s:$PATH\" TMPDIR='%s'; id -u\n",
BuildConfig.APPLICATION_ID, localBinaryDir, localTemporaryDir);
this.context = context;
context.getPackageName(), localBinaryDir, localTemporaryDir);
}
private static boolean isExecutableInPath(final String name) {
@@ -83,7 +80,7 @@ public class RootShell {
* @return The exit value of the command.
*/
public int run(@Nullable final Collection<String> output, final String command)
throws IOException, NoRootException {
throws IOException, RootShellException {
synchronized (lock) {
/* Start inside synchronized block to prevent a concurrent call to stop(). */
start();
@@ -122,24 +119,24 @@ public class RootShell {
}
}
if (markersSeen != 4)
throw new IOException(context.getString(R.string.shell_marker_count_error, markersSeen));
throw new RootShellException(Reason.SHELL_MARKER_COUNT_ERROR, markersSeen);
if (errnoStdout != errnoStderr)
throw new IOException(context.getString(R.string.shell_exit_status_read_error));
throw new RootShellException(Reason.SHELL_EXIT_STATUS_READ_ERROR);
Log.v(TAG, "exit: " + errnoStdout);
return errnoStdout;
}
}
public void start() throws IOException, NoRootException {
public void start() throws IOException, RootShellException {
if (!isExecutableInPath(SU))
throw new NoRootException(deviceNotRootedMessage);
throw new RootShellException(Reason.NO_ROOT_ACCESS);
synchronized (lock) {
if (isRunning())
return;
if (!localBinaryDir.isDirectory() && !localBinaryDir.mkdirs())
throw new FileNotFoundException(context.getString(R.string.create_bin_dir_error));
throw new RootShellException(Reason.CREATE_BIN_DIR_ERROR);
if (!localTemporaryDir.isDirectory() && !localTemporaryDir.mkdirs())
throw new FileNotFoundException(context.getString(R.string.create_temp_dir_error));
throw new RootShellException(Reason.CREATE_TEMP_DIR_ERROR);
try {
final ProcessBuilder builder = new ProcessBuilder().command(SU);
builder.environment().put("LC_ALL", "C");
@@ -147,7 +144,9 @@ public class RootShell {
process = builder.start();
} catch (final IOException e) {
// A failure at this stage means the device isn't rooted.
throw new NoRootException(deviceNotRootedMessage, e);
final RootShellException rse = new RootShellException(Reason.NO_ROOT_ACCESS);
rse.initCause(e);
throw rse;
}
stdin = new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8);
stdout = new BufferedReader(new InputStreamReader(process.getInputStream(),
@@ -160,18 +159,18 @@ public class RootShell {
final String uid = stdout.readLine();
if (!"0".equals(uid)) {
Log.w(TAG, "Root check did not return correct UID: " + uid);
throw new NoRootException(deviceNotRootedMessage);
throw new RootShellException(Reason.NO_ROOT_ACCESS);
}
if (!isRunning()) {
String line;
while ((line = stderr.readLine()) != null) {
Log.w(TAG, "Root check returned an error: " + line);
if (line.contains("Permission denied"))
throw new NoRootException(deviceNotRootedMessage);
throw new RootShellException(Reason.NO_ROOT_ACCESS);
}
throw new IOException(context.getString(R.string.shell_start_error, process.exitValue()));
throw new RootShellException(Reason.SHELL_START_ERROR, process.exitValue());
}
} catch (final IOException | NoRootException e) {
} catch (final IOException | RootShellException e) {
stop();
throw e;
}
@@ -187,13 +186,34 @@ public class RootShell {
}
}
public static class NoRootException extends Exception {
public NoRootException(final String message, final Throwable cause) {
super(message, cause);
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;
}
public NoRootException(final String message) {
super(message);
public Object[] getFormat() {
return format;
}
public Reason getReason() {
return reason;
}
public boolean isIORelated() {
return reason != Reason.NO_ROOT_ACCESS;
}
public enum Reason {
NO_ROOT_ACCESS,
SHELL_MARKER_COUNT_ERROR,
SHELL_EXIT_STATUS_READ_ERROR,
SHELL_START_ERROR,
CREATE_BIN_DIR_ERROR,
CREATE_TEMP_DIR_ERROR
}
}
}

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