Compare commits

..

712 Commits

Author SHA1 Message Date
Jason A. Donenfeld b83538d08d Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-14 00:24:47 +02:00
Jason A. Donenfeld 3c31c340d8 Download modules after verifying signify signature
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-14 00:03:39 +02:00
Jason A. Donenfeld 59620456ee Revert "Fix activity leak on Android Q"
This reverts commit 489518000971914b2608da43e2146690dcc02cb9.

October has arrived.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-13 14:10:11 +02:00
Jason A. Donenfeld e42bd29382 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-13 14:10:11 +02:00
Jason A. Donenfeld 18dbc21f96 libwg-go: overwrite socket directory correctly
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-13 14:09:40 +02:00
Jason A. Donenfeld 68d871c47c Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-13 11:53:17 +02:00
Jason A. Donenfeld a45a219e5f proguard: reenable obfuscation
Android bundles let us keep everything together.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-13 11:53:17 +02:00
Jason A. Donenfeld 49788240aa libwg-go: version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-13 11:46:41 +02:00
Jason A. Donenfeld 52166500fd ToolsInstaller: extract from apk instead of relying on native extraction
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-13 11:46:41 +02:00
Jason A. Donenfeld 3c8fef2655 SharedLibraryLoader: separate out extraction
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-13 11:46:41 +02:00
Jason A. Donenfeld 21af2f2f62 libwg-go: overwrite socket directory correctly
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-13 11:46:41 +02:00
Jason A. Donenfeld 6d01296e8b SharedLibraryLoader: prioritize ABI ordering
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-13 11:46:41 +02:00
Jason A. Donenfeld 749efcde21 SharedLibraryLoader: iterate through all apks for bundles
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-13 11:46:41 +02:00
Jason A. Donenfeld 3af2420da9 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-12 18:55:28 +02:00
Nicolas Douma d0d24f4554 tools: prepare for binder usage in wg-quick
Signed-off-by: Nicolas Douma <nicolas@serveur.io>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-12 18:55:28 +02:00
Jason A. Donenfeld 96b44c1771 Activity: make dark/night theme follow system on Q
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-12 18:55:28 +02:00
Jason A. Donenfeld 4a1d07b364 Application: use preferences from compat libs
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-12 18:55:28 +02:00
Jason A. Donenfeld 7fbe5349a2 export: use content resolver on android Q+
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-12 18:55:28 +02:00
Jason A. Donenfeld d8bad72fd6 preferences: add donation link
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-12 18:55:28 +02:00
Jason A. Donenfeld 927b32c99f libwg-go: update to go 1.13
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-12 18:55:21 +02:00
Jason A. Donenfeld 27b691bef6 idea: update settings for 3.5
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-12 16:51:15 +02:00
Harsh Shandilya d2b9de740d Migrate to Android 10
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2019-09-30 19:45:11 +02:00
Jason A. Donenfeld eb45b4b1bd InetAddresses: prepare for Android 10's real method support
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-09-30 19:42:02 +02:00
Harsh Shandilya c545b5e65f FragmentUtils: Directly cast context as SettingsActivity
ContextThemeWrapper#getContext seems to be an instance of ContextImpl now which
is not public API and also not what we want. Directly cast context as SettingsActivity
which seems to work exactly how we need this to.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2019-09-27 17:26:56 +05:30
Harsh Shandilya 992b6486a1 Update runtime dependencies
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2019-09-27 13:27:51 +05:30
Harsh Shandilya 421b1f889b Add LeakCanary to debug builds
LeakCanary is an advanced memory leak detection library for Android designed by the fine folks
at Square.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2019-09-27 13:18:27 +05:30
Harsh Shandilya 1ee1368e18 Fix activity leak on Android Q
This workaround was discussed at https://twitter.com/Piwai/status/1169274622614704129 after
Google had closed the issuetracker with a WONTFIX at https://issuetracker.google.com/issues/139738913.

The situation has since changed with Google promising a fix on October's ASB but since we can't really
know, patch this ourselves for the timebeing.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2019-09-27 13:18:27 +05:30
Harsh Shandilya 1a6a8789c1 Update to Golang 1.12.10
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2019-09-27 13:15:06 +05:30
Harsh Shandilya b5c155db1b Update AGP to 3.5.0
Also update the gitignore to ignore the '.cxx' directory AGP 3.5.0
uses for native build artifacts.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2019-09-27 13:15:06 +05:30
Harsh Shandilya 6c5b46eadd Uprev to Gradle 5.6.2
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2019-09-27 12:59:53 +05:30
Jason A. Donenfeld 5abbab2635 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-07-08 15:46:39 +02:00
Jason A. Donenfeld e5766094f4 tools: bump deps
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-07-08 15:46:04 +02:00
Jason A. Donenfeld 96d77988d3 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-06-05 21:08:47 +02:00
Revath S Kumar e9e4fd4e8b TunnelList: Get focus to toggle button via remote for Fire TV
This removes a no-longer-needed workaround for the ListView
OnItemClickListener (it won't fire if a focusable view is inside the
item view). Since converting our ListView instances to RecyclerView
instances, we set the OnClick and OnLongClick listeners directly on the
item view, and this workaround no longer has any effect.

Unsurprisingly, the workaround breaks focusability of the Switch, which
is necessary to toggle tunnels on devices with keypad-based navigation,
such as the Fire TV.

This commit also adds explicit focusability hints for the Switch.

Related mail thread:
https://lists.zx2c4.com/pipermail/wireguard/2019-May/004112.html

Reported-by: Christophe-Marie Duquesne <chmd@chmd.fr>
Reported-by: Revath S Kumar <gmail@revathskumar.com>
[Samuel: sorted attributes; expanded commit message]
Signed-off-by: Samuel Holland <samuel@sholland.org>
2019-06-05 21:07:50 +02:00
Jason A. Donenfeld b5d9fbf1f4 libwg-go: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-06-05 21:07:50 +02:00
Jason A. Donenfeld 81dc89f85b Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-19 00:56:13 -06:00
Jason A. Donenfeld 76305045db InetEndpoint: properly match IPv6 addresses
The old one didn't account for trailing digits.

Reported-by: Brandon Jackson <bjackson@napshome.net>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-19 00:53:14 -06:00
Jason A. Donenfeld c4ba48d7a9 libwg-go: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-19 00:40:45 -06:00
Jason A. Donenfeld d5dcdf13bf strings: %i is invalid
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-19 00:30:53 -06:00
Jason A. Donenfeld 7feb3dccbf WgQuickBackend: pass result to error string
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-16 12:24:28 -06:00
Jason A. Donenfeld 2251d74fce Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-08 01:41:41 +01:00
Jason A. Donenfeld 2c89d3fa7f libwg-go: use netpoll rather than rwcancel
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-07 02:05:42 +01:00
Jason A. Donenfeld 49cc634678 libwg-go: don't use submodule
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-03 05:43:42 +01:00
Jason A. Donenfeld c93e81c632 libwg-go: update to 1.12
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-27 05:24:25 +01:00
Jason A. Donenfeld 050e202291 project: bump dependencies
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-17 04:14:11 +01:00
Jason A. Donenfeld efe04d6602 ToolsInstaller: fix typo on cleanup
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-17 03:32:35 +01:00
Jason A. Donenfeld 823b8324c7 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-15 18:03:43 +01:00
Jason A. Donenfeld ae2f88a9ee tools: bump upstream version
This now includes the latest ARM64 ChaCha20 implementation, which should
improve performance.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-14 11:59:10 +01:00
Jason A. Donenfeld 32287c60c4 ToolsInstaller: Require Magisk 18
People installing magisk modules are people capable of updating Magisk.
No need to leave around old compat cruft.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-14 11:59:10 +01:00
Harsh Shandilya 69c6fa0a24 ToolsInstaller: Use chcon over restorecon
restorecon probes file_contexts to get the context
to be applied to the file. /sbin/.magisk does not
exist in file_contexts for obvious reasons so restorecon
always fails. Use chcon directly with the system_file
context to allow contexts to be applied.

Suggested-by: Chris Renshaw <osm0sis@outlook.com>
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2019-02-14 11:03:41 +01:00
Jason A. Donenfeld c3e63df7b5 Update copyright
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-07 19:21:34 -05:00
Samuel Holland fb4f1e30d5 PeerProxy: Only add IPv4 DNS servers when excluding private IPs
Reported-By: Christophe-Marie Duquesne <chmd@chmd.fr>

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-07 19:21:34 -05:00
Harsh Shandilya 6d0fde218b Bump stream support libraries to latest stable
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-12-18 17:58:55 +01:00
Jason A. Donenfeld 6d02b0a26d strings: squelch warning on multiple %s
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-18 17:58:12 +01:00
Harsh Shandilya 495bf9c954 Enable proguard and wire up rules
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-12-18 22:18:00 +05:30
Jason A. Donenfeld cb1bc95e6b Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-18 17:07:04 +01:00
Harsh Shandilya 6fdf0266cf Migrate to AndroidX
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-12-18 16:54:54 +01:00
Jason A. Donenfeld ee26198e2c Bump the go runtime
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-16 01:54:22 +01:00
Jason A. Donenfeld fe424197da InetEndpoint: disallow huge ports
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-15 22:05:43 +01:00
Samuel Holland 053ca232aa Adjust code generation to match existing style
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-15 14:46:23 -06:00
Samuel Holland 2e8d566bd4 Clean up error messages
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-15 14:46:23 -06:00
Samuel Holland dcb0e9b3e8 Provide semantically meaningful exceptions for translation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-15 14:46:23 -06:00
Jason A. Donenfeld 3497882ea6 Bump the go runtime
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-15 06:13:55 +01:00
Jason A. Donenfeld dc2463a0ab Fix locale usage
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-11 03:47:32 +01:00
Jason A. Donenfeld b514717076 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-11 02:38:18 +01:00
Jason A. Donenfeld 23932952d8 Squelch xml warning
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-11 02:38:18 +01:00
Jason A. Donenfeld 266ee7626c Throw illegalargumentexception instead of nullpointerexception for builder errors
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-11 02:28:34 +01:00
Zachary Wander c1ba1f409c Unwrap the correct exception
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-11 02:21:37 +01:00
Jason A. Donenfeld 0f669e8ca3 Order strings
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-11 02:21:37 +01:00
Zachary Wander 9de711a4f5 Localize exception messages
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-11 02:21:23 +01:00
Jason A. Donenfeld e1965f121c Lowercase endpoint in exception message
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-11 01:02:55 +01:00
Jason A. Donenfeld b2c9b3500c Do not allow for an empty port in endpoint
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-10 15:54:38 +01:00
Jason A. Donenfeld f60d26c4bf Export actual configuration for zips
Reported-by: John Greenwood <ijohnyyh@gmail.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-10 15:47:16 +01:00
Jason A. Donenfeld b43044fee9 Add upstream go patch for lstat
Android O disallows lstat, and this upstream golang patch hasn't made it
into a release yet.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-10 04:33:45 +01:00
Jason A. Donenfeld 2bc66e4574 Bump go submodule
Fixes a problem with inotify_init being blocked by Android 9's seccomp
policy.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-10 02:56:51 +01:00
Jason A. Donenfeld a641a093ad Use English lower casing
In Turkish, I becomes ı instead of i, which is a problem when matching
things like "AllowedIPs".

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-09 05:54:47 +01:00
Jason A. Donenfeld 704369d431 Version bump
I feel a bit uneasy releasing this, because who knows how much Samuel
has tested his model rewrite, but nothing looks obviously horrible, so
let's give it a shot. We're still "alpha", after all.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-08 03:51:42 +01:00
Jason A. Donenfeld 5aa8191cd3 Do not close zip input stream
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-08 03:48:50 +01:00
Jason A. Donenfeld bb43804d58 Downgrade support library
We're not ready for the newer one yet, but we will be soon if all goes
well.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-08 03:19:54 +01:00
Jason A. Donenfeld 7a8d14c85c Remove ACRA
This was requested by developers who never wound up using it. It's not
really worth keeping around, since the play console gives us most of
what we need anyway.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-08 03:17:19 +01:00
Samuel Holland d1e85633fb Remodel the Model
- The configuration and crypto model is now entirely independent
of Android classes other than Nullable and TextUtils.
- Model classes are immutable and use builders that enforce the
appropriate optional/required attributes.
- The Android config proxies (for Parcelable and databinding) are
moved to the Android side of the codebase, and are designed to be
safe for two-way databinding. This allows proper observability in
TunnelDetailFragment.
- Various robustness fixes and documentation updates to helper classes.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-08 02:39:41 +01:00
Samuel Holland a264f7ab36 Auto-format the source directories
Blame Jason for writing Java in vim.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-11-11 21:50:23 -06:00
Jason A. Donenfeld 4e134772d7 tools: wg-quick: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-11-11 22:16:28 -05:00
Jason A. Donenfeld 3eb6c91c9e libwg-go: do not mix C style and Go style variable names
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-11-06 15:43:09 +01:00
Jason A. Donenfeld e7fd53b809 libwg-go: don't forget to include jni calls
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-11-03 20:17:19 +01:00
Jason A. Donenfeld 164ec1e31d Bump version so that we have correct submodule hashes
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-11-01 03:41:56 +01:00
Jason A. Donenfeld 373a5f18d6 tools: update submodules
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-10-30 01:01:49 +01:00
Jason A. Donenfeld 36058ead7d Never use system go
Telling people to patch their system go is bonkers.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-10-11 00:23:17 +02:00
Jason A. Donenfeld 33fd5b4634 Update to go modules
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-10-09 19:46:48 +02:00
Jason A. Donenfeld 91647978e6 Fix small error
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-10-01 17:54:47 +02:00
Jason A. Donenfeld d0b64f4bd5 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-10-01 17:47:24 +02:00
Jason A. Donenfeld c23d58bc27 Peer: prefer v4 endpoints to v6
This works around DNS64 XLAT changeovers.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-10-01 17:13:14 +02:00
Jason A. Donenfeld 49a9475c4a strings: properly mark positionals
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-10-01 16:33:37 +02:00
Jason A. Donenfeld 364032fe84 Update gradle and external deps
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-10-01 16:30:07 +02:00
Jason A. Donenfeld 5658584803 global: update copyright headers
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-09-06 21:32:46 -06:00
Jason A. Donenfeld d580200989 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-08-26 11:15:23 -06:00
Jason A. Donenfeld c2cdde73d1 tools: bump submodules
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-08-26 11:09:36 -06:00
Jason A. Donenfeld bc74d4d7f8 libwg-go: update golang version
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-08-26 11:08:45 -06:00
Harsh Shandilya 535c611f2d QuickTileService: Don't use deprecated getDrawable method
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-08-26 11:04:01 -06:00
Harsh Shandilya 6efbf65405 Target SDK 28
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-08-26 11:04:01 -06:00
Harsh Shandilya abb121224d FragmentUtils: Make final and prevent instantiation
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-08-26 11:04:01 -06:00
Harsh Shandilya 8ad4657d6f Sort dependencies
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-08-26 11:04:01 -06:00
Harsh Shandilya 6d3f1e00a5 Supress false-positive DefaultLocale warnings
We decided in 402472237e8f that it's a bad idea for our use-case

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-08-26 11:04:01 -06:00
Harsh Shandilya c38f6c471d Extract error messages to string resources
Useful for validation errors and localisation later on

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-08-26 11:04:01 -06:00
Jason A. Donenfeld 85462de254 MonkeyedSnackbar: remove
This didn't actually help with much and caused problems.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-08-26 10:51:21 -06:00
Zhao Gang 61d4f17f5d config: fix wrong Peer endpoint string format
When a tunnel is running, saving the tunnel's config with an IPv6
address endpoint like [::1]:42 would result in the wrong format ::1:42.
This patch fixes it.

For endpoints with an IPv6 address(e.g. [::1]:42). Since the default
endpoint InetSocketAddress is created unresolved, getEndpointString()
returns "[::1]:42" (InetSocketAddress.getHostString() returns the
literal hostname). After the endpoint is resolved, getEndpointString()
returns "::1:42" (InetSocketAddress.getHostString() returns the IPv6
address without the square brackets). This inconsistent return values
caused the above mentioned bug.

With this patch, function getEndpointString would return the right
format string whether the endpoint is resolved or not.

Signed-off-by: Zhao Gang <gang.zhao.42@gmail.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-08-16 12:08:09 -07:00
Jason A. Donenfeld 09cf73cd3c GoBackend: make socket IPC optional
This fixes the multi-user case, which cannot be determined at compile
time and probably isn't reasonable to consider anyway in a global
manner.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-31 14:27:29 +02:00
Jason A. Donenfeld ffa3cefa67 Style: use attrs from appcompat rather than from frameworks
This isn't possible for colorForeground, colorBackground,
textAppearanceMedium, but at least it's useful for some things here.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-30 22:41:55 +02:00
Jason A. Donenfeld 8ec2cc8582 FloatingActionsMenu: don't wrap context on asus api 21
Works around frameworks bug.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-30 17:35:08 +02:00
Jason A. Donenfeld 75dfa0643b Show different color for multiselection
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-30 16:28:03 +02:00
Jason A. Donenfeld d5cde43158 Replace hard-coded colors with theme colors
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-30 14:43:56 +02:00
Harsh Shandilya 6493a9a1f2 Remove placeholder color and directly use alpha
Why wasn't this done like this in the first place?

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-30 14:28:27 +02:00
Jason A. Donenfeld f35e059194 MonkeyedSnackbar: fix typos
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 19:17:10 +02:00
Jason A. Donenfeld 3cb3e9d8b7 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 18:52:54 +02:00
Jason A. Donenfeld 5dfc5659ad ConfigNamingDialog: use name input filter
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 18:44:30 +02:00
Jason A. Donenfeld c4102992ae MonkeyedSnackbar: work around Harsh's broken phone
I think I'd probably like to revert this, since presumably there's a
good reason in the first place why the support lib disables animations
when accessibility services are turned on?

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 18:35:26 +02:00
Jason A. Donenfeld 219f4e8016 MSF: make it more obvious what needs to be done
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 17:21:30 +02:00
Jason A. Donenfeld 6558140a7c FloatingActionButtonBehavior: animate transitions
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 17:21:30 +02:00
Jason A. Donenfeld db7b61ab80 Roll back to API 27 for now
There's no source available for API 28, which is a pain. But this commit
should be reverted whenever source is released.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 16:01:44 +02:00
Jason A. Donenfeld bce5d852e1 Target API 28
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 15:46:01 +02:00
Jason A. Donenfeld b960b4a6cd TunnelListFragment: show selected tunnel
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 05:57:53 +02:00
Jason A. Donenfeld 1b10e75168 TunnelListFragment: fix multiselection on rotation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 04:50:16 +02:00
Jason A. Donenfeld 07359e392c FloatingActionButonBehavior: set translation back to 0 when snackbar dies
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 04:23:30 +02:00
Jason A. Donenfeld e5a5bad240 MainActivity: only show two column on tablets
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 03:46:45 +02:00
Samuel Holland ca92ac60b7 MainActivity: Fix fragment selection logic
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-07-28 17:07:37 -05:00
Jason A. Donenfeld e29c21f8df Application: refuse to run on old android
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-28 18:27:45 +02:00
Jason A. Donenfeld 6ceeac93bf QuickTileService: fix bug the wrong way
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-28 17:55:24 +02:00
Jason A. Donenfeld 9f861096ac config: show more informative error message on wrong key
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-27 15:20:23 +02:00
Jason A. Donenfeld 520df16885 Set ACRA install source
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-26 13:46:18 +02:00
Jason A. Donenfeld c905ef6083 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-26 05:29:49 +02:00
Eric Kuck 9652fe99df TunnelDetailFragment now restores state correctly after process death
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-26 05:17:43 +02:00
Jason A. Donenfeld 62d8beff96 Application: use proper completablefuture for backend
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-26 04:54:14 +02:00
Jason A. Donenfeld 7d438e9dbc Wire up ACRA
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-26 01:41:29 +02:00
Harsh Shandilya b364221c93 FloatingActionBehaviour: Adjust constructors
Get these in line with the parent class

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-26 01:36:53 +02:00
Jason A. Donenfeld b8052bd8fb ThemeChangeAwareActivity: reintroduce cache buster
This is still needed by certain icons, like the trash icon in the peer
editor.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-25 04:05:41 +02:00
Jason A. Donenfeld e437ac389e Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-25 03:11:33 +02:00
Jason A. Donenfeld 2db2a0921d MainActivity: attempt to fix commit state exceptions
This is an attempt to fix:

java.lang.IllegalStateException:
at android.support.v4.app.FragmentManagerImpl.checkStateLoss (FragmentManager.java:2053)
at android.support.v4.app.FragmentManagerImpl.enqueueAction (FragmentManager.java:2079)
at android.support.v4.app.BackStackRecord.commitInternal (BackStackRecord.java:678)
at android.support.v4.app.BackStackRecord.commit (BackStackRecord.java:632)
at com.wireguard.android.activity.MainActivity.moveToState (MainActivity.java:58)
at com.wireguard.android.activity.MainActivity.onSelectedTunnelChanged (MainActivity.java:157)
at com.wireguard.android.activity.BaseActivity.setSelectedTunnel (BaseActivity.java:75)
at com.wireguard.android.fragment.BaseFragment.setSelectedTunnel (BaseFragment.java:82)
at com.wireguard.android.fragment.TunnelListFragment.lambda$null$4$TunnelListFragment (TunnelListFragment.java:307)
at com.wireguard.android.fragment.TunnelListFragment$$Lambda$4.onClick (Unknown Source:6)
at android.view.View.performClick (View.java:6274)
at android.view.View$PerformClick.run (View.java:24729)
at android.os.Handler.handleCallback (Handler.java:789)
at android.os.Handler.dispatchMessage (Handler.java:98)
at android.os.Looper.loop (Looper.java:169)
at android.app.ActivityThread.main (ActivityThread.java:6595)
at java.lang.reflect.Method.invoke (Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run (Zygote.java:240)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:767)

But this is probably the wrong way to fix it and instead moveToState
needs to be reimagined.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-25 03:07:16 +02:00
Jason A. Donenfeld d7889c4e88 style: coloring the navbar looks a bit strange when rotated
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-25 02:59:26 +02:00
Jason A. Donenfeld 559add3f71 TunnelEditorFragment: plug memory leak on listeners
Apparently these don't get GC'd unless they're removed explicitly,
because there's a global singleton registry of them. So, introduce a
little registry of our own.

Reported-by: Samuel Holland <samuel@sholland.org>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-25 02:59:26 +02:00
Jason A. Donenfeld d615304e83 qrcode: minor adjustments
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-25 02:59:26 +02:00
Eric Kuck 8e0835e570 Added QR code scanner as tunnel import method
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-25 02:05:11 +02:00
Harsh Shandilya bb20c89cd5 tools: Bump wireguard submodule
This force pushing is going to be the death of me

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-24 18:26:53 +02:00
Jason A. Donenfeld 996cbb5f2b tools: support ancient NDKs
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-24 18:09:18 +02:00
Jason A. Donenfeld 8028d708cb tools: let wg(8) play with userspace implementation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-24 17:57:56 +02:00
Harsh Shandilya 7689905c78 config: Remove Locale based string format
The configurations are supposed to be in a very specific
format which is not user-facing and hence doesn't have to
be adjusted for locale avoiding both the redundancy as well
as potential breakages in the configuration file format from
different locales.

Fixes: 71c67aa24ae2 ("config: Minor cleanup")
Reported-by: Samuel Holland <samuel@sholland.org>
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-24 17:57:56 +02:00
Jason A. Donenfeld 284e42647c tools: pass in debug package name
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-24 17:57:56 +02:00
Jason A. Donenfeld 3a16c08821 Make placeholder pretty
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-23 00:05:12 +02:00
Harsh Shandilya 76fb6a318e Show help text when no tunnels are imported
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-22 23:21:17 +02:00
Jason A. Donenfeld c633f96374 FloatingActionsMenuRecyclerViewScrollListener: add final modifiers
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-22 23:21:17 +02:00
Harsh Shandilya 3357be9557 RTL layout fixes
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-22 18:21:39 +02:00
Harsh Shandilya 6696e838da treewide: Optimize imports
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-22 18:20:23 +02:00
Jason A. Donenfeld d0d56f3a1b fab: move in direct ratio to scroll
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-20 16:41:53 +02:00
Jason A. Donenfeld fbf32a6c29 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-20 15:54:46 +02:00
Harsh Shandilya 3e37289b68 UI: use background color for navbar in dark mode
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-20 15:53:41 +02:00
Jason A. Donenfeld 2d6a45f824 fab: make icons always white
With the new shade of blue, perhaps this simply looks better.

I don't like hard coding the color away from the theme, however.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-20 15:45:21 +02:00
Jason A. Donenfeld ed802336e6 theme: use less disgusting color
Harsh's "users" were offended by the dark theme. So, we change the
accent to that used by gboard's dark theme, which should be pretty
uncontroversial.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-20 03:23:17 +02:00
Harsh Shandilya b1d1e3b436 TunnelListFragment: Allocate interpolators outside scroll listener
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-20 03:17:49 +02:00
Jason A. Donenfeld d33e322b67 TunnelEditorFragment: fix null pointer dereference
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-19 20:57:00 +02:00
Jason A. Donenfeld 60a6e29350 QuickTileService: fix null pointer dereference
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-19 20:57:00 +02:00
Jason A. Donenfeld 3b0e0c2f16 tools: bump version
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-18 21:54:14 +02:00
Jason A. Donenfeld ffb8bccbc5 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-18 18:40:16 +02:00
Jason A. Donenfeld 08170f7e55 TunnelListFragment: setOnScrollListener is old, but we support API 21, so we have to use it
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-18 18:32:42 +02:00
Jason A. Donenfeld 28d47b3470 AppListDialogFragment: getArguments is null before onCreate
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-18 18:08:08 +02:00
Jason A. Donenfeld 8e3586328c fab: use auto calculated fling threshold
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-18 02:32:05 +02:00
Jason A. Donenfeld f315654d40 fab: add fab sized padding at bottom of recycler view
This way we can keep scrolling when there are exactly the right number
of items, so that the toggle switch becomes visible.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-18 02:15:14 +02:00
Harsh Shandilya fdfab18d45 fab: make fab respond to recyclerview scroll events
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-18 02:15:06 +02:00
Harsh Shandilya d43e77867c fab: slide fab up when a snackbar shows
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-18 02:15:06 +02:00
Harsh Shandilya df03bdd7f9 android: QuickTileService: Do not use slashed icon on Android P
Android P features circle masked QS tiles which make use of colors
to denote STATE_ACTIVE/STATE_INACTIVE rather than a slash across
the drawable as seen on Android Oreo.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-16 17:31:14 +02:00
Jason A. Donenfeld ae5bf6fbb2 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-16 15:05:18 +02:00
Jason A. Donenfeld 04ff63f1b5 InetAddresses: unwrap reflection exceptions
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-16 15:05:18 +02:00
Harsh Shandilya 71cf39660f android: TunnelListFragment: Annotate parameter to match super method
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-16 03:26:02 +02:00
Jason A. Donenfeld 7364f2540e BaseFragment: do not allow tunnel to be null
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-13 04:13:35 +02:00
Jason A. Donenfeld 5d66f6b2e5 config: dns servers can be null
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-13 04:09:09 +02:00
Jason A. Donenfeld fec5fa8caf config: make loadData private
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-13 04:06:55 +02:00
Jason A. Donenfeld 6f48e138a4 TunnelEditorFragment: binding might be null
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-13 04:04:11 +02:00
Jason A. Donenfeld 21c15fe4ea QuickTileService: show intermediate state when changing
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-13 03:57:02 +02:00
Eric Kuck 67ea8b2936 global: Add nullity annotations
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-07-13 03:46:23 +02:00
Jason A. Donenfeld fbaa4d9ab1 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-12 02:14:25 +02:00
Harsh Shandilya 19b57c41b7 Address lint issues in TunnelListFragment
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-12 02:07:19 +02:00
Jason A. Donenfeld 26d762bc5c TunnelEditorFragment: add DNSes to allowedIPs when excluding rfc1918
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-12 02:04:52 +02:00
Jason A. Donenfeld eab0248aaa Clean up warnings
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-11 21:03:22 +02:00
Jason A. Donenfeld a5bbe171cb fab: remove asus hack
Let's hope Eric's changes make this no longer required.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-11 21:03:22 +02:00
Eric Kuck 5463086e75 fab: use support library's rendering
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-07-11 21:03:22 +02:00
Jason A. Donenfeld 1f7bdd4f5f Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-10 22:00:10 +02:00
Jason A. Donenfeld 3cf6aad083 QuickTileService: automatically slash the tile
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-09 18:55:55 +02:00
Jason A. Donenfeld b997a2581b BaseFragment: in the event no view is available, use toast
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-09 17:29:35 +02:00
Eric Kuck d7ea078cdf Request VPN permissions on activation
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-07-09 17:08:41 +02:00
Jason A. Donenfeld d50e0f5fb9 Use instanceOf instead of getClass
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-08 23:59:25 +02:00
Eric Kuck c696e9f275 Build with different name and ID in debug mode
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-07-08 03:26:42 +02:00
Jason A. Donenfeld 707c8c19a8 gradle: bump build tools version
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-08 03:18:15 +02:00
Eric Kuck b37b48b8dc Switch from ListView to RecyclerView
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-07-08 02:50:49 +02:00
Jason A. Donenfeld 2c7203ab8d Another bump for misbuild
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-06 21:13:36 +02:00
Jason A. Donenfeld d1a812042c Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-06 18:17:58 +02:00
Jason A. Donenfeld 78d976162d PeerEditor: put exclusion checkbox next to label
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-06 18:17:58 +02:00
Jason A. Donenfeld 7078162c69 AppListDialogFragment: add deselect all button
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-06 18:17:58 +02:00
Jason A. Donenfeld 2742b09b5a tools: update wg-quick for ExcludedApplications support
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-06 18:17:58 +02:00
Jason A. Donenfeld 7b28d51cdd global: move to Apache 2.0
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-06 16:28:46 +02:00
Jason A. Donenfeld d132087b3c PeerEditor: add exclude private IPs functionality
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-06 16:28:46 +02:00
Jason A. Donenfeld 124f186983 TunnelEditor: fix nits
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-06 16:28:46 +02:00
Eric Kuck 500a705531 AppListDialogFragment: add implementation for excluding applications
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-07-06 04:14:19 +02:00
Jason A. Donenfeld 5729947d6c TunnelEditor: better looking buttons
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-06 02:40:11 +02:00
Harsh Shandilya 363d0b9126 android: model: Make some methods static
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-06-30 21:19:59 +02:00
Harsh Shandilya e985452f3b android: FloatingActionsMenu: Don't create labels on ASUS' Android 5 devices
They have completely wrecked the framework there and all efforts to
work around their absolutely broken software have been in vain, hence
let's atleast let users be able to use the app, labels or otherwise.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-06-30 21:10:52 +02:00
Harsh Shandilya 5c9643a23b android: VersionPreference: Handle no-browser-installed case
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-06-30 21:10:22 +02:00
Harsh Shandilya 0e3e3ae37b android: Add select all button to action mode
Thanks to Jason for suggesting the not-clinically-insane
method to go about this.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-06-30 21:07:24 +02:00
Harsh Shandilya b41d473f64 wireguard: Bump snapshot
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-06-30 21:06:40 +02:00
Jason A. Donenfeld 3de549d2c7 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-25 16:18:57 +02:00
Jason A. Donenfeld 408e9004b0 libwg-quick: add iptables output allowance
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-25 16:16:27 +02:00
Jason A. Donenfeld e1a66d5766 global: Small cleanups
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-25 16:10:02 +02:00
Harsh Shandilya 99cf2152c4 android: Consolidate getPrefActivity into FragmentUtils
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
[Samuel: Changed static to non-static import]
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-06-23 01:00:07 -05:00
Harsh Shandilya 53e8d425e9 QuickTileService: Remove useless override
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-06-23 00:57:31 -05:00
Harsh Shandilya 9e5f45da15 gradle: Style
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-06-23 00:57:31 -05:00
Harsh Shandilya 4c0caa10e9 treewide: Optimize imports
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-06-23 00:57:31 -05:00
Samuel Holland b9991e4229 config: Refactor IPCidr and use of InetAddress
Use a canonically-named utility class to tack on methods to the existing
InetAddress class. Rename IPCidr to InetNetwork so it better matches
InetAddress and is more pronouceable :) While here, simplify the
constructor and toString() functions, and properly implement hashCode().

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-19 21:59:44 -05:00
Samuel Holland 4acee49d4b util: Extract non-Android utility interfaces
As part of a refactoring that will likely introduce more custom
collection classes, move the non-Android-specific parts outside the
com.wireguard.android package.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-19 21:59:44 -05:00
Samuel Holland d3a8291a7a crypto: Slightly Java-ify the Curve25519 implementation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-19 21:59:42 -05:00
Samuel Holland bcae77b989 app: Regularly scheduled gradle updates
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-19 21:35:54 -05:00
Samuel Holland de75ff4bea idea: Disable an unwanted inspection
Android Studio isn't smart enough to realize that the public/private
keys and the keypair are effectively the same thing. Just turn off the
inspection because it's usually tripped by intentional things.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-19 21:33:36 -05:00
Jason A. Donenfeld b10a6171a5 Application: make lock final
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-17 22:34:22 +02:00
Jason A. Donenfeld 6534f45a3a application: style fix
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-17 14:53:31 +02:00
Jason A. Donenfeld 373145d30e Version bump for clat fix
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-17 05:25:42 +02:00
Jason A. Donenfeld 5feea74f28 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-14 05:07:15 +02:00
Jason A. Donenfeld e8891d775b global: supply backend asynchronously
We can't block for IO, so move everything to async workers or to
callbacks.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-14 05:06:18 +02:00
Jason A. Donenfeld 0f128f99a1 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-12 05:46:02 +02:00
Jason A. Donenfeld 61e3441bfb Application: require rootshell to use wgquick backend
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-12 05:42:24 +02:00
Jason A. Donenfeld 15e10d8fde ToolsInstaller: safer state machine
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-10 05:17:09 +02:00
Jason A. Donenfeld ea72e8b656 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-07 04:59:47 +02:00
Jason A. Donenfeld 2ca27c2783 BaseActivity: style
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-07 04:49:15 +02:00
Jason A. Donenfeld f190db0754 BootShutdownReceiver: style
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-07 04:37:28 +02:00
Jason A. Donenfeld 8d27570eea Backend: abstract version information
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-07 04:35:07 +02:00
Jason A. Donenfeld 24605c9c01 Give Samuel heart attack by removing Dagger
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-07 04:12:42 +02:00
Jason A. Donenfeld 7b59353910 VersionPreference: account for checking state and move away from tools installer
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-07 03:27:06 +02:00
Jason A. Donenfeld ca7f4e5be9 FloatingActionMenu: 5.1 requires explicit text color
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-06 03:12:29 +02:00
Harsh Shandilya ab95ac83c9 MainActivity: Fix style
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-06-06 02:07:26 +02:00
Jason A. Donenfeld 51fb57433b ToolsInstallerPreference: do not check for magisk on main thread
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-06 00:29:44 +02:00
Harsh Shandilya 0496b94aa8 build: Allow building release artifacts in-tree
This change avoids all need for changing any file under
VCS to insert signing keys and configs for release builds.

Example contents of keystore.properties

```
// Location of keystore, relative to module build.gradle,
// in this case, of the app module
storeFile=../wireguard.jks
storePassword=b3ty0uc4nth4xxth1s
keyAlias=wireguard
keyPassword=4ndr01dsux
```

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-06-05 19:58:34 +05:30
Harsh Shandilya d8d6e99df1 MainActivity: Silence useless warning
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-06-05 19:52:08 +05:30
Jason A. Donenfeld 377ba1c9d1 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-05 16:07:56 +02:00
Jason A. Donenfeld 8ebeeb6d90 FloatingActionMenu: use appcompat theme
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-05 16:01:38 +02:00
Harsh Shandilya 3c3de065c6 MainActivity: collapse action menu on toolbar touch
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-06-05 15:22:40 +02:00
Jason A. Donenfeld a17ec6b1f7 ToolsInstaller: allow installing as Magisk module
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-05 06:17:00 +02:00
Jason A. Donenfeld cbf8ac6538 libwg-go: better error when using unpatched Go
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-05 03:28:16 +02:00
Jason A. Donenfeld d4b1295e94 ActionBar: show single menu item as toolicon
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-05 03:09:51 +02:00
Jason A. Donenfeld db53e17e58 TunnelListFragment: hide menu when going to settings
Really the menu should be hidden when clicking on the action bar, too.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-05 03:05:58 +02:00
Jason A. Donenfeld f0a3e63743 MainActivity: style
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-05 02:47:59 +02:00
Jason A. Donenfeld d636e13717 fab: properly get theme color
Harsh changed this before, but my original way is what the support
library does internally.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-05 02:44:32 +02:00
Jason A. Donenfeld dea60e13c0 libwg-go: more efficient and safer string passing
It was unclear when the Go string was actually freed.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-05 02:18:12 +02:00
Harsh Shandilya 40a30d997d fab: Remove useless override
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-06-05 02:05:35 +02:00
Harsh Shandilya dba8d0305e fab: Use themed context to set style
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-06-05 02:05:23 +02:00
Jason A. Donenfeld 27072972ab DarkMode: move to shared preferences listener
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-05 02:02:09 +02:00
Jason A. Donenfeld d56e95c576 MainActivity: style
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-04 19:55:27 +02:00
Jason A. Donenfeld d0ef2c43d9 FloatingActionsMenu: remove unused imports
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-04 19:12:12 +02:00
Jason A. Donenfeld 0c0c1acc3b BaseActivity: support android 5 and 6 when clearing drawable cache
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-04 18:35:18 +02:00
Jason A. Donenfeld 48f796c463 LogExporterPreference: get all past processes
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-04 17:57:02 +02:00
Jason A. Donenfeld 8aab35a70d BaseActivity: invalidate icon cache on Android P
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-04 17:57:02 +02:00
Jason A. Donenfeld e5f6c24174 Bump version
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-04 07:16:10 +02:00
Jason A. Donenfeld 4cad06b7ce app: disable proguard
It's mostly a hindrance to debugging.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-04 07:16:10 +02:00
Jason A. Donenfeld c4e32328fc TunnelManager: disable dangerous intents for now
We need to think about how to allow this securely. It's not okay for all
apps to be allowed to twiddle with VPN settings.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-04 07:06:28 +02:00
Jason A. Donenfeld a2ccbf003c TunnelManager: new intents
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-04 07:06:28 +02:00
Jason A. Donenfeld 10ca2c8681 Preferences: don't use round icon
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-03 16:15:30 +02:00
Harsh Shandilya fd63e496e5 Preferences: Move version pref to top and add icon
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-06-02 19:35:11 +02:00
Jason A. Donenfeld 567503abc7 Backends: print versions somewhere in log
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-02 19:26:33 +02:00
Harsh Shandilya 4671f59c67 android: Cleanup classes
- Use final modifer wherever possible
- Use try-with-resources for input/output streams

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-06-02 19:14:56 +02:00
Harsh Shandilya 4986d92f3d crypto: KeyEncoding: Fix style
- Replace python style variable names with camel case
- Don't declare multiple variables in the same line

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-06-02 19:12:30 +02:00
Jason A. Donenfeld 0b1c7cc35f Topic: John does things differently
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-02 18:44:35 +02:00
Jason A. Donenfeld 9e278c88e6 ExporterPreferences: disable control immediately
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-02 18:38:56 +02:00
Jason A. Donenfeld c3246060f5 Preferences: add log exporter
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-02 18:35:09 +02:00
Jason A. Donenfeld b7e025e381 libwg-go: fix style
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-01 18:27:30 +02:00
Jason A. Donenfeld 858ec4c0ab Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-01 18:01:25 +02:00
Jason A. Donenfeld 65292aaa79 Topic: make reentrant
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-01 17:34:59 +02:00
Jason A. Donenfeld 3d57eb633e BaseActivity: flush themed icon cache on theme change
The most terrible hack you have ever seen.

The drawable cache isn't properly flushed when changing the theme -- a
frameworks bug, evidently -- so we work around it by digging deep into
the mud.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-01 16:48:08 +02:00
Jason A. Donenfeld 1e45898d70 Settings: add version
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-01 16:06:56 +02:00
Jason A. Donenfeld 61431fb579 colors: final touches
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-01 15:18:23 +02:00
Harsh Shandilya 00d48e867b ui: Use better list colors in night mode
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-06-01 18:42:20 +05:30
Harsh Shandilya d5df0c10ab TunnelListFragment: Dejank action mode title
When unselecting items, the toolbar briefly says
'0 items selected' before it reverts back to the
non-action mode toolbar which feels janky at best.

To mitigate this, just set a blank title to the action
mode toolbar when item count is 0, to facilitate the
smoothness of the transition to non-action mode toolbar.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-06-01 18:42:20 +05:30
Harsh Shandilya af814951f3 fab: Make label responsive to night mode
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-06-01 18:42:20 +05:30
Harsh Shandilya a54a03aa2f FloatingActionButton: Cleanup declaration of TranslucentLayerDrawable class
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-06-01 18:42:20 +05:30
Harsh Shandilya 4edfdd8f3b FloatingActionButton: Make attribute grabbing not be terrible
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-06-01 18:42:19 +05:30
Jason A. Donenfeld 32d669a661 theme: add dark theme with toggle
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-01 14:57:25 +02:00
Jason A. Donenfeld 918076a670 global: fix theme situation and clean up cruft while adding more cruft
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-06-01 14:57:25 +02:00
Harsh Shandilya 752e61d1c7 fab: default to app theme colors
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-06-01 03:19:26 +02:00
Harsh Shandilya 125f725a03 fab: use AppCompatTextView for label
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-06-01 03:17:29 +02:00
Harsh Shandilya 3c84b48f08 fab: cleanup
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-06-01 03:16:32 +02:00
Jason A. Donenfeld 8c32c32c2b wg-quick: don't break push notifications
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-31 16:40:50 +02:00
Jason A. Donenfeld e664a05d4b KeyEncoding: more constant time
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-30 23:31:20 +02:00
Jason A. Donenfeld ba4672b422 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-29 19:08:55 +02:00
Jason A. Donenfeld 09833a1ba5 FloatingActionButton: import cleaned up getbase code
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-29 19:03:47 +02:00
Jason A. Donenfeld ded0191aae SharedLibraryLoader: introduce to work around Samsung PackageManager bugs
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-29 05:09:37 +02:00
Jason A. Donenfeld 9245e9536a Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-27 19:00:38 +02:00
Jason A. Donenfeld 292df12175 Support always-on-vpn
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-27 18:57:52 +02:00
Jason A. Donenfeld f988306c17 libwg-go: Saner way to deal with sockets
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-27 18:17:55 +02:00
Jason A. Donenfeld 64ce6cd5a2 EditorFragment: show toast on creation too
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-27 04:08:23 +02:00
Jason A. Donenfeld 5476fc2228 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-26 13:56:35 +02:00
Jason A. Donenfeld 7120ef9ffd libwg-go: try no stickiness and no roaming
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-26 03:03:15 +02:00
Jason A. Donenfeld 75f4478fb5 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-24 19:26:48 +02:00
Jason A. Donenfeld 23c4174fcd libwg-go: reenable sticky sockets, just slightly less sticky
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-24 18:39:29 +02:00
Jason A. Donenfeld 5754df6b71 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-23 17:12:18 +02:00
Jason A. Donenfeld 62dbeca732 libwg-go: use gopath
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-23 17:01:40 +02:00
Jason A. Donenfeld a533be82e8 libwg-go: No sticky sockets with VPNService.java
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-22 23:28:54 +02:00
Jason A. Donenfeld 52d90724bc Fix hang and version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-21 14:17:34 +02:00
Jason A. Donenfeld 4863bd647a Add stacktrace via SIGUSR2
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-21 14:13:55 +02:00
Jason A. Donenfeld cb62dc8fa2 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-21 04:23:31 +02:00
Jason A. Donenfeld e3442cde56 libwg-go: better up/down sync
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-16 22:24:20 +02:00
Jason A. Donenfeld 694577b9b4 ExceptionLoggers: never have a null message
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-16 17:36:25 +02:00
Harsh Shandilya 9c3091456d android: Directly use views for snackbars
There should be no need to deference an extra variable.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-05-16 17:31:31 +02:00
Jason A. Donenfeld 515be5e453 libwg-go: more fixes
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-15 21:57:17 +02:00
Jason A. Donenfeld b89b9e89c2 build: version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-15 13:44:59 +02:00
Jason A. Donenfeld 9d20c378ba libwg-go: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-15 13:44:22 +02:00
Jason A. Donenfeld f661c67c89 tools: move wg-quick to main wireguard repo
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-14 18:25:42 +02:00
Jason A. Donenfeld e1ac4247ff libwg-go: make changes for recent go code
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-14 17:10:59 +02:00
Jason A. Donenfeld 34422e57a9 build: another bump for mistake
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-10 17:14:48 +02:00
Jason A. Donenfeld 088ff2dbda build: version bump and fix disaster
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-10 16:18:34 +02:00
Jason A. Donenfeld 2853515e8f build: version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-09 18:49:03 +02:00
Jason A. Donenfeld 95dbb14cb5 theme: revert to old color scheme
Noble effort, but ultimately ugly.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-09 18:38:13 +02:00
Harsh Shandilya 0b9bcf0f9e global: Cleanup line lengths and misc lint warnings
I know we decided to ditch the idea of shutting up "Exception
thrown with empty param" warnings but this pesters me too
much and we can instead just treat this as a weird future proofing
thing if and when we end up needing the exception messages.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-05-09 18:16:14 +02:00
Harsh Shandilya e2ab2210cd MainActivity: Nip out as early as possible when moving to same fragment
Save the useless logging and fragment manager object creation.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-05-09 18:16:09 +02:00
Jason A. Donenfeld 332c35747e libwg-go: new timer system upstream
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-09 17:38:27 +02:00
Jason A. Donenfeld ebe3297c83 libwg-go: ensure local Go instance is patched
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-09 17:33:36 +02:00
Samuel Holland 7f6c861fd6 mipmap: Fix formatting of launcher icon XML
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-05-05 10:11:35 -05:00
Harsh Shandilya 58b9a88bcb styles: Fixup CAB background color
Long pressing any list item entry would cause the action bar to
use the dark color from the parent theme -- fix that.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-05-05 10:11:20 -05:00
Harsh Shandilya e4eac7a3cf drawables: Introduce material iconography
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-05-05 10:11:14 -05:00
Harsh Shandilya 55ec3046d1 styles: Propagate WireGuard blue in UI
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-05-05 10:08:49 -05:00
Harsh Shandilya 6bd4f7495d styles: Propagate WireGuard red in UI
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-05-05 10:08:24 -05:00
Harsh Shandilya fd571c53ce TunnelEditor: Add toast messages to success events
Letting the user know that the operation went successfully
via visual cues is a nice UX touch.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-05-05 10:08:14 -05:00
Jason A. Donenfeld 5ed36d7346 wireguard-go: bump submodule
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-05 06:09:45 +02:00
Harsh Shandilya 37413f2fb4 TunnelEditor: Remove unused parameter
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-05-03 23:49:14 -05:00
Harsh Shandilya 59aa3035a8 ui: Use a proper CardView based layout
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-05-03 23:45:03 -05:00
Jason A. Donenfeld 46b374cf25 tools: include libmnl paths after system paths for ndk 17
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-03 19:15:09 -05:00
Jason A. Donenfeld b276833b33 config: loosen parser to match reality
Mid-line comments, mixed case.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-03 19:15:09 -05:00
Jason A. Donenfeld f77eac6796 global: fix up copyrights
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-03 19:15:09 -05:00
Samuel Holland e8d8d5dc29 Keypair: Minimize the scope of warning suppression
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-05-02 10:31:25 -05:00
Samuel Holland 3e8d457501 KeyEncoding: Address lints/warnings and match line length
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-05-02 10:31:25 -05:00
Samuel Holland aa92c4a9d5 global: Add or update copyright headers in Java code
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-05-02 10:31:25 -05:00
Harsh Shandilya 24572aa861 global: Some more lint cleanup
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-05-02 17:14:35 +02:00
Jason A. Donenfeld cb6681b15a drawable: add adaptive icon
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-02 07:19:05 +02:00
Jason A. Donenfeld a46b2eb9e5 libwg-go: avoid phony target and update go
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-02 06:02:15 +02:00
Harsh Shandilya a77ff1627d wg-build: Update gradle build files
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-05-02 03:40:02 +02:00
Harsh Shandilya 96ebf4cd51 libwg-go: Ignore go{path,root}
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-05-02 03:40:02 +02:00
Samuel Holland 691d8a34a0 project: Import select Android Studio configuration files
Having a consistent set of code style and inspection rules makes it
easier for contributors to match the existing code.

Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-05-01 16:31:21 -05:00
Jason A. Donenfeld 92fc1dbac8 libwg-go: do not deadlock on Close
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-05-01 17:48:59 +02:00
Jason A. Donenfeld 997138cd22 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-30 23:11:32 +02:00
Jason A. Donenfeld ba1b6c7095 ZipExporterPreference: account for directory already existing
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-30 19:55:06 +02:00
Samuel Holland 843003f436 global: Clean up Java
Address Java and Android lints.

Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-04-30 11:50:21 -05:00
Samuel Holland 8e4fb91a28 global: Clean up XML
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-04-30 11:39:23 -05:00
Samuel Holland 7eedf08d4b global: Automatic code formatting
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-04-30 11:39:12 -05:00
Samuel Holland e2636320b7 app: Sort dependencies
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-04-30 11:38:15 -05:00
Samuel Holland a481aa244c project: Update gradle files
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-04-30 11:38:15 -05:00
Jason A. Donenfeld 6ce739f01f libwg-go: use system Go when possible
We're still maybe curlzipping and using a binary distribution of Go, but
at least this is only the case on machines that don't have go.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-30 18:19:37 +02:00
Jason A. Donenfeld 05b4e395a9 Tunnel importer: stricter file filters
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-30 17:37:34 +02:00
Jason A. Donenfeld 65ca5979d2 proguard: enable
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-30 16:57:57 +02:00
Jason A. Donenfeld 3c84db7788 Preferences: use v14 material style
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-30 15:35:21 +02:00
Jason A. Donenfeld a47ddf57d1 ZipExporterPreference: requireNonNull is redundant
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-30 15:12:49 +02:00
Harsh Shandilya f9d68185e2 wg: Collapse fab when pressing back before exiting
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-04-30 17:33:24 +05:30
Harsh Shandilya f1fa46829d SettingsActivity: Migrate permissions check to AppCompat
Pretty straightforward, I was clearly overthinking this.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-04-30 17:30:25 +05:30
Harsh Shandilya a6e530049c ZipExporterPreference: Correctly get preference activity
In AppCompat based preferences, this#getContext returns an
object of android.view.ContextThemeWrapper class from where
we can safely extract a reference to our parent activity.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-04-30 17:30:25 +05:30
Harsh Shandilya 77c0d4dfa6 TunnelListFragment: Use Collections methods in place of Arrays
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-04-30 17:30:25 +05:30
Harsh Shandilya 7e415a62c7 wg: Break out crossport dependency
Replace all crossport uses with upstream design
support library components.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-04-30 17:30:25 +05:30
Harsh Shandilya 431b75be15 wg-build: Upgrade databinding library to 3.1.2
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-04-30 17:30:24 +05:30
Harsh Shandilya fe4cc22ca0 SettingsActivity: Gracefully exit when options menu home is pressed
This is an activity, so it does not join the fragment backstack, but
instead piles on top

SettingsActivity
| -> MainActivity
| -> EditorFragment
| -> DetailFragment
| -> ListFragment

Without overriding the back button in the toolbar, it simply
kills the entire state on MainActivity and causes it to reload.
By calling finish() on the activity when home is pressed from the
item menu we can silently make it die without affecting any underlying
states held by MainActivity and instead return to the exact fragment
we launched settings from.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-04-30 17:30:24 +05:30
Harsh Shandilya 4fc1e61a83 SettingsActivity: Define preferenceTheme for AppCompat
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-04-30 17:30:24 +05:30
Harsh Shandilya e7e571a301 wg: Migrate menu visibility to AppCompat
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-04-30 17:30:24 +05:30
Harsh Shandilya 425d194e4f wg: Add and use dummy AppTheme
Preparation for when the app will start carrying
colors reflecting the WireGuard brand.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-04-30 17:30:24 +05:30
Harsh Shandilya 4ec0b3a908 Prefer AppCompat classes
AppCompat is the preferred way to go for any app targetting
a wider range of SDKs.

Replace all activities and fragments with their AppCompat
variants and fixup method calls to use support variants.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-04-30 17:30:24 +05:30
Harsh Shandilya af57824c14 wireguard: Extract dependency versions to ext struct
Maintain common versions in a single place

Also upgraded the support library to 27.1.1 and
databinding library to 3.1.1.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-04-30 17:30:24 +05:30
Jason A. Donenfeld 596904977a TunnelDetailFragment: rewrite and simplify
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-30 05:29:54 +02:00
Jason A. Donenfeld 73b0c4ea81 TunnelEditorFragment: rewrite and simplify
This should remove some null pointer dereferences and overall make the
thing more robust.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-30 05:29:54 +02:00
Jason A. Donenfeld 622f41f11f Allow exporting to zip file
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-29 07:53:56 +02:00
Jason A. Donenfeld f4e462fabd Allow importing from zip file
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-28 21:55:42 +02:00
Harsh Shandilya 217ab5e17f config: Minor cleanup
- Stop implicitly assuming locales in String.format
- Cleanup method visibilities
- Improve uses of Integer methods
- Remove unused getToken method

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-04-28 06:07:48 +02:00
Harsh Shandilya ba862b166b Quick cleanup across the board
- Clean up imports
- Adjust method visibility
- Drop in NonNull annotations where needed
- Prevent potential NPEs from nulled getActivity calls
- Remove unused methods

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-04-28 06:07:48 +02:00
Jason A. Donenfeld 592c5cbbd6 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-28 04:56:12 +02:00
Jason A. Donenfeld 63071f57b7 Use validation instead of two-way binding
This is insane, but it appears to be working. We essentially store
things in a separate class for editing, and then commit it back at a
given time.

This business with onViewStateRestored in both TunnelEditorFragment and
in TunnelDetailFragment is buggy and likely wrong.

In general TunnelEditorFragment should probably be rewritten. The
relationship with the changed name is not clear.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-28 04:51:43 +02:00
Jason A. Donenfeld 693228985d Do not do DNS lookups for IPs
This involves reflection, which is a bummer, but it's better than doing
unnecessary DNS lookups.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-27 18:34:52 +02:00
Jason A. Donenfeld 9c6f9135e9 Add build instructions
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-27 16:45:16 +02:00
Jason A. Donenfeld 9ee976823d Throw IllegalArgumentExceptions when arguments are bad
This will make the two way data binding crash more, but it will improve
the robustness of the config file parser, which deals with exceptions
gracefully, and when we move to one way data binding, it will help with
that too.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-27 05:43:07 +02:00
Jason A. Donenfeld 254cc4cc87 Latest go changes
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-27 05:43:07 +02:00
Jason A. Donenfeld 31101f2418 WgQuickBackend: always create configuration file
It might be removed on an update.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-26 19:42:55 +02:00
Jason A. Donenfeld 8d2510ae9e Manifest: disable always-on VPN
We don't actually comprehend how this works yet, so disable it. But
we'll need to add support for this at some point.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-26 02:25:55 +02:00
Jason A. Donenfeld 532edf774a GoBackend: bring down tunnels when revoked or disconnected
This synchronizes the OS's connection state with ours, such as when the
user disconnects using the system UI.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-25 18:27:18 +02:00
Jason A. Donenfeld 86ffb24748 Use variable map instead of shell switch
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-25 14:43:46 +02:00
Jason A. Donenfeld 2c9e11f4f2 Curve25519: fix up spacing
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-25 14:36:51 +02:00
Jason A. Donenfeld e6838f9cb0 Use binary distro of Golang
This is a bummer, but Gradle already specifies tons of specific versions
of various binary components, so this is not materially different than
the rest of how this whole thing works.

It also allows us to specify the Go version that will actually build a
working binary of wireguard-go, since all of the Go bugs mean not every
version works equally. We do *not* want to use whatever version a distro
happens to be shipping.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-24 15:34:13 +02:00
Jason A. Donenfeld 83b30bed7b Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-23 17:04:17 +02:00
Jason A. Donenfeld 88cf839c90 Update application state based on wg-quick
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-23 17:04:17 +02:00
Jason A. Donenfeld a1c6f4b3b7 Version bump
Due to https://github.com/golang/go/issues/24950

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-19 18:55:25 +02:00
Jason A. Donenfeld 32fcb3fccf Work around go fd closing limitation and version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-19 17:11:02 +02:00
Jason A. Donenfeld e7c6e487ea Version bump for go race condition
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-18 21:33:18 +02:00
Jason A. Donenfeld 80f22e6c3d Determine MTU automatically
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-18 16:44:05 +02:00
Jason A. Donenfeld ed42e30069 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-18 09:56:06 +02:00
Jason A. Donenfeld dc1b808f8e Update dependencies
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-18 09:51:55 +02:00
Jason A. Donenfeld 53d29b312f More javafication
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-18 05:28:31 +02:00
Jason A. Donenfeld 69f1a35a3f Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-17 06:02:03 +02:00
Jason A. Donenfeld 140fb395dc GoBackend: default MTU is 1280
This sucks, but it works with mobile networks. Later we can do something
sophisticated like we do with wg-quick.c, but not now.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-17 05:59:23 +02:00
Jason A. Donenfeld b5360871e8 Remove sloppy java with enterprise java horrors
Since the amount of mind numbing boiler plate has been increased, this
must be the proper way to do things.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-17 05:27:05 +02:00
Jason A. Donenfeld e40c579b0e global: update various upstreams
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-17 02:25:35 +02:00
Aurélien Chabot 4c9143c835 GoBackend: Handle vpn service expiration
After a timeout the android system is destroying the vpn service when it
is not used. By using a completable future we can wait for the service
to be relaunch on demand.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-17 02:15:24 +02:00
Aurélien Chabot 74eae55c87 Config: Handle multiple address or dns in config file
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-17 02:15:24 +02:00
Aurélien Chabot 44698bb000 GoBackend: Add support for multiple address and dns as a comma separated list
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-17 02:15:24 +02:00
Aurélien Chabot 175f7e16dd GoBackend: parse allowed ips
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-17 02:15:24 +02:00
Aurélien Chabot c6d311923a GoBackend: Parse the dns address
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-17 02:15:24 +02:00
Aurélien Chabot 6878aba911 GoBackend: IPv6 handling
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-17 02:15:24 +02:00
Aurélien Chabot d97312b96a GoBackend: Resolve endpoint before passing it to the go lib
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-17 02:15:24 +02:00
Aurélien Chabot 9dfab4d60f GoBackend: Use the android VpnService to encapsulate the go backend
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-04-17 02:15:24 +02:00
Jason A. Donenfeld 19aec7c5c6 GoBackend: support NO_PI mode
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-02-28 19:59:50 +01:00
Jason A. Donenfeld f2113f80db Update submodules
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-02-20 17:01:54 +01:00
Jason A. Donenfeld 0ea6f73332 GoBackend: integrate into app
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-02-17 21:14:09 +01:00
Jason A. Donenfeld b923f7bc57 wg: bump commit
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-02-17 21:14:09 +01:00
Jason A. Donenfeld d1c863b16e KeyEncoding: add constant time hex implementation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-02-13 19:47:31 +01:00
Samuel Holland b22e2b259b manifest: Add permission to read external storage
This fixes support for file managers that don't proxy the file access
from the file selection dialog through a content provider, and just
return a raw file:// URL. In this case, resolver.openInputStream() tries
to open the file directly, and fails with "Permission denied".

Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-02-03 12:47:14 -06:00
Jason A. Donenfeld a66348c315 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-01-18 23:17:29 +01:00
Samuel Holland c683d23125 FileConfigStore: Warn for deletion failure in exception path
There's nothing we can do about it at this point; we're already rolling
back changes.

Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-18 04:12:54 -06:00
Samuel Holland 23535c0577 WgQuickBackend: Always pass the full path to wg-quick
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-18 04:10:58 -06:00
Samuel Holland 8dde6c3c0f fragments: Be extra paranoid about the binding getting destroyed
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-18 04:10:58 -06:00
Samuel Holland 54f201174d TunnelManager: Use constants directly where appropriate
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-17 14:35:39 -06:00
Samuel Holland fb3138bdda Peer: Add missing @Override
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-17 14:35:39 -06:00
Samuel Holland f63aeea6b8 WgQuickBackend: Clean up unused fields and imports
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-17 14:35:39 -06:00
Samuel Holland 09c207b8a3 ToolsInstaller: Remove double-checked locking
It can't work and may be crashing the runtime.

Use a lock object to avoid exposing the synchronization in the class's
interface.

Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-17 14:35:39 -06:00
Samuel Holland d56eda2fd6 RootShell: Use a lock object instead of sync methods
This avoids exposing the synchronization implementation details
in the class's interface.

Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-17 14:35:14 -06:00
Jason A. Donenfeld 75aeec035c Build bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-01-10 23:52:02 +01:00
Jason A. Donenfeld c1cc2774be Manifest: show back arrow on sub activities
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-01-10 23:52:02 +01:00
Jason A. Donenfeld 7e06768f75 ToolsInstaller: symlink tools always
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-01-10 23:47:13 +01:00
Jason A. Donenfeld 7a618c1463 FileConfigStore: cleanup on rename problem
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-01-10 23:26:12 +01:00
Samuel Holland f8ee7dcce9 QuickTileService: Fix crash opening activity
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-10 11:13:16 -06:00
Samuel Holland 30901a6152 TunnelController: Use helper function to unwrap the Throwable
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-10 00:03:45 -06:00
Samuel Holland 7b9c1a536c model: Use ConfigStore.rename() to avoid recreating tunnels
Rename all of the functions to be in line with setConfig/setState

Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-10 00:03:03 -06:00
Samuel Holland 951afaa9b2 ConfigStore: Add a rename method and implement it
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-10 00:01:52 -06:00
Samuel Holland 5ce7eba2bf FileConfigStore: Simplify error handling
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-09 23:57:15 -06:00
Jason A. Donenfeld dfa4a2eb9d WgQuickBackend: properly report exception so alert shows
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-01-10 03:16:42 +01:00
Jason A. Donenfeld df3062c7ad Tunnel: ifname has max len 15
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-01-10 02:49:06 +01:00
Jason A. Donenfeld b3e928c1bf tools: error if setconf fails
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-01-10 02:48:05 +01:00
Jason A. Donenfeld 01faa9b358 tools: ifname has max len 15
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-01-10 02:38:29 +01:00
Jason A. Donenfeld a42892bc28 TunnelEditorFragment: do not pass null localConfig
Otherwise we might dereference it, per this play store gathered trace:

Xiaomi Redmi 3S (land), 2048MB RAM, Android 7.1
java.lang.NullPointerException:
at com.wireguard.android.model.Tunnel.setConfig (Tunnel.java:118)
at com.wireguard.android.fragment.TunnelEditorFragment.onOptionsItemSelected (TunnelEditorFragment.java:160)
at android.app.Fragment.performOptionsItemSelected (Fragment.java:2478)
at android.app.FragmentManagerImpl.dispatchOptionsItemSelected (FragmentManager.java:2182)
at android.app.FragmentController.dispatchOptionsItemSelected (FragmentController.java:336)
at android.app.Activity.onMenuItemSelected (Activity.java:3211)
at com.android.internal.policy.PhoneWindow.onMenuItemSelected (PhoneWindow.java:1219)
at com.android.internal.view.menu.MenuBuilder.dispatchMenuItemSelected (MenuBuilder.java:761)
at com.android.internal.view.menu.MenuItemImpl.invoke (MenuItemImpl.java:152)
at com.android.internal.view.menu.MenuBuilder.performItemAction (MenuBuilder.java:904)
at com.android.internal.view.menu.MenuBuilder.performItemAction (MenuBuilder.java:894)
at android.widget.ActionMenuView.invokeItem (ActionMenuView.java:616)
at com.android.internal.view.menu.ActionMenuItemView.onClick (ActionMenuItemView.java:152)
at android.view.View.performClick (View.java:5637)
at android.view.View$PerformClick.run (View.java:22433)
at android.os.Handler.handleCallback (Handler.java:751)
at android.os.Handler.dispatchMessage (Handler.java:95)
at android.os.Looper.loop (Looper.java:153)
at android.app.ActivityThread.main (ActivityThread.java:6244)
at java.lang.reflect.Method.invoke (Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run (ZygoteInit.java:891)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:781)

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-01-10 00:54:59 +01:00
Jason A. Donenfeld 138ba5c416 QuickTileService: sometimes tile is null
According to play store reports, such as:

wt88047, 0MB RAM, Android 8.1
java.lang.NullPointerException:
at com.wireguard.android.QuickTileService.updateTile (QuickTileService.java:97)
at com.wireguard.android.QuickTileService.onStartListening (QuickTileService.java:56)
at android.service.quicksettings.TileService$H.handleMessage (TileService.java:407)
at android.os.Handler.dispatchMessage (Handler.java:106)
at android.os.Looper.loop (Looper.java:164)
at android.app.ActivityThread.main (ActivityThread.java:6520)
at java.lang.reflect.Method.invoke (Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:442)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:807)

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-01-10 00:41:28 +01:00
Samuel Holland c6bdeb1d45 Prepare version 0.3.0
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-09 10:31:08 -06:00
Samuel Holland 17f8c65b2e WgQuickBackend: Finish the implementation
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-09 10:31:08 -06:00
Samuel Holland 933a685585 model: Chain completions to avoid race conditions
Otherwise getConfigAsync().thenCompose(x -> setState()) would be unsafe.

This reverts commit a6595a273afd50524cc66765c6bfbdcc34cb12e4.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-01-09 10:03:29 -06:00
Samuel Holland daacc06a0d global: Clean up error logging
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-09 09:37:49 -06:00
Samuel Holland a1d955ef62 WgQuickBackend: Log when tunnels can't be enumerated
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-09 09:18:03 -06:00
Samuel Holland ca077dd090 RootShell: Improve shell start error handling
No need to catch and re-throw exceptions before starting the process. If
running `su` itself fails, there's no (functional) root, so report that.

Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-09 09:17:08 -06:00
Samuel Holland 3d6737e32f RootShell: Be stricter about command delimiters
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-09 09:16:53 -06:00
Jason A. Donenfeld 643b698f30 RootShell: stop if we can't start
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-01-09 08:39:07 -06:00
Jason A. Donenfeld 23c09eb655 RootShell: hoist out synchronization
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-01-09 15:19:50 +01:00
Jason A. Donenfeld 14638b10ae tools: try not to overflow
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-01-09 15:15:15 +01:00
Samuel Holland 426fa7d50b ToolsInstaller: Propagate NoRootException properly
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-09 08:13:32 -06:00
Samuel Holland 52e8eef9ce RootShell: Simplify, synchronize, clean up
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-09 08:13:11 -06:00
Samuel Holland 662bc3894b TunnelListFragment: Remove useless keyword
final doesn't do anything in try-with-resources (it's always final).

Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-09 06:25:53 -06:00
Jason A. Donenfeld 1451c4baf5 ToolsInstaller: terminators are better than seperators
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-01-09 13:12:49 +01:00
Jason A. Donenfeld 2785def37a RootShell: synchronize accesses to single shell
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-01-09 13:12:49 +01:00
Samuel Holland 204755d689 RootShell: Simplify constructor
ToolsInstaller now takes care of installing the tools.

Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-08 23:49:14 -06:00
Samuel Holland c806e655fa WgQuickBackend: Ensure tools are available before use
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-08 23:39:32 -06:00
Samuel Holland 5201c7f849 ToolsInstaller: Add a method to ensure tool availability
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-08 23:39:32 -06:00
Samuel Holland ba30d46e08 WgQuickBackend: Inline resolveState()
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-08 23:39:32 -06:00
Samuel Holland 1cfb938dd1 ToolsInstaller: Add methods to check/make symlinks
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-08 23:39:32 -06:00
Samuel Holland 011d683c23 ToolsInstallerPreference: Examine current state on load
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-08 23:39:32 -06:00
Samuel Holland b87568482d ToolsInstaller: Add method to check installation status
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-08 23:39:32 -06:00
Samuel Holland 2776455d4f TunnelEditorFragment: Fix dismissing creator activity
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-08 23:39:32 -06:00
Samuel Holland 4f02817ef0 global: Fix or suppress most lints/warnings
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-08 23:39:26 -06:00
Samuel Holland dd69a2e778 project: Remove currently-unused library
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-08 23:39:15 -06:00
Samuel Holland 710ffc7bb3 Fragment/Tile: Make success/error messages translatable
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-08 20:30:03 -06:00
Jason A. Donenfeld 6361628e16 layout: never use suggestions
IP addresses and key material could be potentially sensitive. We also
don't want any interactions with the suggestive text APIs. So, we mark
it as both a visible password and we turn off suggestions. This will fix
the length limit, too, in Android 8.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-01-09 00:23:33 +01:00
Jason A. Donenfeld da30a6d449 TunnelListFragment: fix plural grammar
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-01-09 00:23:22 +01:00
Jason A. Donenfeld 1ae10b8f45 RootShell: fix off by one
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-01-08 20:42:12 +01:00
Jason A. Donenfeld 5944efc13c QuickTileService: show toast for longer and use right messages
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-01-08 20:30:25 +01:00
Jason A. Donenfeld 7645ea15c4 global: Prefix tags with WireGuard/
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-01-08 20:29:51 +01:00
Jason A. Donenfeld 31ba7e6593 RootShell: multiplex commands
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-01-08 19:46:51 +01:00
Samuel Holland ae2068dc16 Port over remaining error handling
This doesn't really belong here, but there's no sense in throwing it
away--even here, it's a minor optimization.

Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-08 04:34:28 -06:00
Samuel Holland c4e91f8040 Handle tunnel state change errors appropriately
This class should probably be renamed and moved, but I don't know to
what or where.

Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-08 04:33:54 -06:00
Samuel Holland c40555c1bd TunnelListFragment: Fix crash on deleting multiple tunnels
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-08 04:31:30 -06:00
Samuel Holland b6ed17884f ToggleSwitch: Improve reliability
It's not great, but it's better than it was.

Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-08 04:31:04 -06:00
Samuel Holland 668d90f063 WgQuickBackend: Improve error handling
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-08 04:25:15 -06:00
Samuel Holland ec81014c4e Tunnel: Add a State factory method
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-08 04:25:15 -06:00
Samuel Holland d645d69847 project: Global cleanup
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-08 02:19:13 -06:00
Samuel Holland 88e30431cb Application: Remove unneeded exported component
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-08 00:35:35 -06:00
Samuel Holland 08cca56388 ToolsInstaller: Extract to its own classes
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-08 00:34:55 -06:00
Samuel Holland 1f30e133d6 TunnelManager: Simplify save/resume methods
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-07 21:52:14 -06:00
Samuel Holland 4a3d68bb7d BaseActivity: Make variable names consistent
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-07 21:51:55 -06:00
Samuel Holland 1fd9547f6a TunnelManager/QuickTileService: Remember last used tunnel
This is actually a bit simpler than having a manually-selected "primary"
tunnel, and is hopefully easier for the user.

Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-07 21:50:43 -06:00
Samuel Holland 38b2aafce8 Remove primary configuration preference
It is no longer used.

Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-07 21:46:41 -06:00
Samuel Holland 16c0b5b15a FileConfigStore: Move function returns outside try blocks
This makes more clear what things can throw exceptions, and the
functions' control flow.

Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-07 20:33:39 -06:00
Samuel Holland 0ab3e29432 Tunnel: Remove UNKNOWN state
It's never used.

Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-07 20:33:07 -06:00
Samuel Holland 11f851abf8 Move bundle keys where they are used
This both clarifies and simplifies the code.

Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-07 20:32:01 -06:00
Samuel Holland cef97b990e WgQuickBackend: Prefer ArrayList over LinkedList
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-07 01:45:27 -06:00
Samuel Holland e01813487f app: Upgrade to build tools version 27.0.3
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-07 01:33:11 -06:00
Samuel Holland 137325d3cc ConfigEditorFragment: Support renaming tunnels
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-07 01:23:08 -06:00
Samuel Holland 30396b8718 model: Allow renaming tunnels
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-07 01:22:31 -06:00
Samuel Holland e70b242c01 model: Remove confusing uses of completion chaining
This looks like the builder pattern, but isn't.

Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-07 00:41:47 -06:00
Samuel Holland 77fdd7c883 TunnelListFragment: Name a CompletionStage what it is
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-07 00:26:08 -06:00
Samuel Holland 1862ff0bbe Tunnel: The name passed to isNameValid is never null
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-07 00:25:49 -06:00
Samuel Holland 2f5494d47c ConfigEditorFragment: Ensure localName is never null
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-07 00:25:33 -06:00
Samuel Holland be8b6017d5 Make TunnelManager the point of asynchronicity
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-07 00:24:56 -06:00
Samuel Holland 5a2f692d73 TunnelManager: Sort tunnels case-insensitively
The second comparator is required to allow multiple tunnels that differ
only in case.

Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-06 23:45:49 -06:00
Samuel Holland 536c6958fc SortedKeyedList...: Support arbitrary comparators
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-06 23:44:34 -06:00
Samuel Holland 4f2b6bef84 Keyed...: Rename all the things
Hooray for diamond interface inheritance.

Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-06 23:07:17 -06:00
Samuel Holland 4d38993832 project: Minor cleanups
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-06 22:14:10 -06:00
Samuel Holland 22bdffcecd model: Proxy all async work through the TunnelManager
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-06 06:30:41 -06:00
Samuel Holland 2315a699fb TunnelManager: Enumerate running tunnels only once
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-06 05:20:11 -06:00
Samuel Holland df7d18fb5d Tunnel: Require passing a state to the constructor
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-06 05:18:34 -06:00
Samuel Holland 1c2239ae91 Backend: Add a function to enumerate running tunnels
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-06 05:18:34 -06:00
Samuel Holland 2543f28274 MainActivity: Fix ActionBar after rotation
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-06 05:18:34 -06:00
Samuel Holland 2206dfc432 TunnelDetailFragment: Allow copying peer public keys
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-06 05:18:34 -06:00
Samuel Holland 193de10972 TunnelDetailFragment: Remove state text and last change
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-06 05:18:34 -06:00
Samuel Holland dbed7155d6 activity: Remove useless indirection
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-06 05:18:34 -06:00
Samuel Holland 63efaa2dcc databinding: Collection classes use generic E
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-06 04:09:30 -06:00
Samuel Holland 5f09afbc16 databinding: Simplify method signature
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-06 04:09:30 -06:00
Samuel Holland 6eef4093d8 databinding: Remove unused classes and methods
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-06 04:09:30 -06:00
Samuel Holland ff0bb081a0 Convert the list of tunnels to a KeyedObservableList
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-06 04:09:30 -06:00
Samuel Holland c73287f64b databinding: Add an adapter for the KeyedObservableList
This adapter actually finally implements stable IDs correctly.

Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-06 04:09:30 -06:00
Samuel Holland e24654ce7c util: Add a keyed list class and a sorted variant
This is inspired by C#'s KeyedCollection. The sorted variant removes the
need for an observable SortedMap.

Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-06 04:09:30 -06:00
Samuel Holland 58eedfd6d9 TunnelManager: Clear primary tunnel when it's removed
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-06 04:09:30 -06:00
Samuel Holland 609194fae2 Serviceless rewrite, part 1
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-06 04:09:29 -06:00
Samuel Holland 4c0869393e Rename package widgets -> widget
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-06 04:08:01 -06:00
Samuel Holland b7196e328a Rename package bindings -> databinding
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-01-06 04:08:01 -06:00
Samuel Holland 7120df7a45 app: Enable Java 8 features
Signed-off-by: Samuel Holland <samuel@sholland.org>
2017-12-26 14:06:32 -06:00
Samuel Holland 42a4579451 project: Ignore native build artifacts
Signed-off-by: Samuel Holland <samuel@sholland.org>
2017-12-26 14:06:10 -06:00
Samuel Holland e3a22e27c4 app: Upgrade to build tools 27.0.2
Signed-off-by: Samuel Holland <samuel@sholland.org>
2017-12-26 14:00:19 -06:00
Samuel Holland cd105312ca tools: Remove stale entry from .gitmodules
Signed-off-by: Samuel Holland <samuel@sholland.org>
2017-12-26 13:58:05 -06:00
Jason A. Donenfeld 2caa06d9fb wg-quick: set mtu after addresses
This has all sorts of terrible implications and fixes a problem in a
pretty terrible way. If the interface MTU is less than 1280, IPv6
addresses will fail to be added. Rather than explictly trying to catch
this and do something particular about it -- such as nicely warning the
user that there could be a v6 isolation issue, for example -- we just
set the MTU _after_ we set the addresses, so that in setting the MTU, we
wind up removing the addresses that were just added. This is pretty bad,
but it makes things a bit smoother.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-12-26 20:04:40 +01:00
Jason A. Donenfeld 53752eb21e tools: use cmp and right arguments for mount
Some systems don't have cmp.
Some systems have buggy mount tools.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-12-20 17:54:28 +01:00
Jason A. Donenfeld 8937cf82fb tools: add wg-quick directly
We also use all search paths for tool, since many want this to be CLI
too.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-12-20 05:02:51 +01:00
Jason A. Donenfeld 40198132e0 tools: force ld.gold
This is to work around a linker bug affecting the NDK.

Reference: https://github.com/android-ndk/ndk/issues/602

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-12-19 02:53:02 +01:00
Jason A. Donenfeld 9b92f4d3c5 SettingsActivity: allow for installing command line tools
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-12-19 02:42:00 +01:00
Jason A. Donenfeld ecc2a1c759 tools: better cmake
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-12-18 04:57:40 +01:00
Jason A. Donenfeld b7a6b44ec1 RootShell: rewrite
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-12-16 07:07:05 +01:00
Jason A. Donenfeld 6d1117a94c tools: build required tools with apk
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-12-15 06:00:45 +01:00
Jason A. Donenfeld 9e028ae8d6 VpnService: require root access
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-30 21:46:56 +01:00
Jason A. Donenfeld 417e973c8f NotSupportedActivity: fix font size
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-29 13:24:40 +01:00
Samuel Holland 97e05ff965 ConfigListFragment: Use correct icon for FAB
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-28 20:42:25 -06:00
Samuel Holland 2b88150fd8 VpnService: Avoid duplicating file check
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-28 20:42:25 -06:00
Jason A. Donenfeld eb5fdf200a Config: use consistant verb
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-29 03:30:23 +01:00
Jason A. Donenfeld af33378522 VpnService: search for binaries in most likely places first
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-29 03:29:36 +01:00
Samuel Holland ee9b374d49 ConfigList: Always dismiss FAB menu on ListView touch
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-28 20:18:11 -06:00
Samuel Holland 49df1ebc40 NotSupported: Fix links
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-28 20:15:50 -06:00
Samuel Holland 8ba902ae99 build.gradle: Consistent quoting style
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-28 20:15:40 -06:00
Samuel Holland 4a672fc05d ConfigListFragment: Use a floating action menu
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-28 20:14:47 -06:00
Samuel Holland 19f0089559 Show ConfigActivity when holding QS tile
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-28 17:33:32 -06:00
Samuel Holland 312816d4fd res/drawable: Fix icon sizes
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-28 17:30:56 -06:00
Samuel Holland 74a6526695 ConfigActivity: Listen for name changes
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-28 17:25:49 -06:00
Jason A. Donenfeld 2f310f0fad EditFragment: do not autocomplete or capitalize ifnames
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-28 22:14:00 +01:00
Jason A. Donenfeld e418c5d3b4 README: update with links
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-28 16:31:13 +01:00
Jason A. Donenfeld f00622a2a9 Open: better icon
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-28 13:09:00 +01:00
Jason A. Donenfeld bbe058418b NotSupported: Better sorry text
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-27 21:29:40 +01:00
Jason A. Donenfeld 982387b4e4 Build: update gradle
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-27 18:40:51 +01:00
Jason A. Donenfeld 641e45e727 SettingsActivity: directly pass through bundle
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-27 05:22:54 +01:00
Jason A. Donenfeld 2cc7f6c2ea ConfigDetailFragment: wire up toggle switch
Finally no gross colors.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-27 04:35:04 +01:00
Jason A. Donenfeld 6bc6aea2d0 NotSupported: check if the module exists
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-27 04:28:48 +01:00
Jason A. Donenfeld ad16d2cc7b Settings: show quick tile config if no primary is defined
This seems like a horrific set of hacks.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-27 03:13:34 +01:00
Jason A. Donenfeld b0bb46382c ConfigImporter: give updates when something goes wrong
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-27 01:28:43 +01:00
Jason A. Donenfeld d43c87a869 QuickTileService: show app if no profiles exist
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-27 00:43:34 +01:00
Jason A. Donenfeld e421b997cd Config: make parsing stricter
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-26 23:45:41 +01:00
Samuel Holland 14a7ada6e1 ConfigImporter: Get filename from content resolver
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-25 23:39:08 -06:00
Samuel Holland 283340f5dc ConfigList: A better toggle switch
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-25 23:05:39 -06:00
Samuel Holland 666dc25cb0 widgets: Import ToggleSwitch from the AOSP Settings app
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-25 22:17:17 -06:00
Samuel Holland 2ec43f38a3 project: License under GPL-2.0+
This allows using Apache 2.0-licensed libraries, because the Apache 2.0
license is only compatible with version 3 of the GPL.

Signed-off-by: Samuel Holland <samuel@sholland.org>
2017-11-25 22:12:10 -06:00
Samuel Holland f1febcf9b6 ConfigList: Poor man's switch
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-24 23:44:18 -06:00
Samuel Holland 3233855630 BindingAdapters: Sort existing adapters
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-24 21:27:18 -06:00
Samuel Holland 50a7a12de2 VpnService: Move it to a backends package
It should be split into two pieces: configuration file management
(loading/saving/renaming/deleting) and calling into wg-quick via
RootShell. The configuration file management part should then go
back into the main package. This is in preparation for adding
additional backends based on wg(8) and wireguard-go.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-24 21:16:37 -06:00
Jason A. Donenfeld 69d4fe9a81 cli: move to android_kernel_wireguard
These tools are now part of the ROM builder's toolkit at:
https://git.zx2c4.com/android_kernel_wireguard/about/

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-25 02:11:55 +01:00
Jason A. Donenfeld bdfb319854 Verison bump tools
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-16 22:15:05 +01:00
Samuel Holland 4a98d07806 ConfigActivity: Add import menu item
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-08 00:50:24 -06:00
Samuel Holland cd6a124c84 VpnService: Add helper for importing configs
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-08 00:49:44 -06:00
Samuel Holland 70156381a7 Bindings: Merge bugfixes and updates
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-08 00:07:29 -06:00
Samuel Holland 6231bb18ac Bindings: Move to a separate package
These are really app-independent reusable classes. Moving them to a
separate package helps avoid polluting the main app package, and makes
them easier to upgrade.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-07 23:34:22 -06:00
Samuel Holland 6945aa1107 project: Update gradle files
These are automatic changes made by Android Studio 3.0.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-07 22:52:50 -06:00
Jason A. Donenfeld 157f754510 cli: add wg(8) build script
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-11-01 18:23:38 +01:00
Samuel Holland 58c0b98c3f Prepare version 0.1.0
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-24 23:24:17 -05:00
Samuel Holland b7992d234c ConfigListFragment: Prevent config names from wrapping
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-24 23:24:09 -05:00
Samuel Holland 292b16b1a1 ConfigActivity: Only use split layout on tablet-sized screens
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-24 23:22:22 -05:00
Samuel Holland a828e83399 Centralize/unify validation of configurations
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-24 22:13:46 -05:00
Samuel Holland 23fdb72eda ConfigActivity: Invert isSplitLayout→isSingleLayout for clarity
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-24 17:25:49 -05:00
Samuel Holland b1d0091bc0 ObservableSortedMap: Document interface
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-24 02:18:13 -05:00
Samuel Holland 3daddad573 Remove state debugging messages
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-24 02:18:05 -05:00
Samuel Holland dcc7ddcd3b ConfigEditFragment: Copy public key on click
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-24 02:11:55 -05:00
Samuel Holland 4fdb3458ec ConfigEditFragment: Mark the persistent keepalive as optional
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-24 02:01:51 -05:00
Samuel Holland cd868bc997 ConfigEditFragment: Add a field for the optional pre-shared key
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-24 02:01:19 -05:00
Samuel Holland 3d6109e6d9 Peer: Add a field for the optional pre-shared key
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-24 02:00:53 -05:00
Samuel Holland 881ad4fd47 ConfigActivity: Fix condition for inserting back stack entries
Fixes saved editor state on rotation (removing and recreating the editor
discarded changes).

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-24 01:47:12 -05:00
Samuel Holland c0a76f87da ConfigActivity: Save editor state across fragment instances
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-24 01:31:19 -05:00
Samuel Holland e446870ac1 AddActivity: Update for ConfigEditFragment changes
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-24 01:31:19 -05:00
Samuel Holland e2df9931ac ConfigEditFragment: Save and restore editor state properly
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-24 01:31:16 -05:00
Samuel Holland f83c84ce4e ConfigActivity: Avoid unnecessary executePendingTransactions
Nothing later tries to find the list using findFragmentById().

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-24 01:22:49 -05:00
Samuel Holland 290c98a9e3 ConfigListFragment: Avoid unnecessary findViewById
The generated ViewDataBinding has fields for each view with an id.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-24 01:22:20 -05:00
Samuel Holland 632f3f73a2 ConfigActivity: Log state transitions
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-23 23:51:08 -05:00
Samuel Holland b2357e58e3 Config/Interface/Peer: Make Parcelable
This allows saving the editor state across restarts.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-23 23:47:44 -05:00
Samuel Holland 95384851cd Config/Interface/Peer: Fix some missed change notifications
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-23 23:43:58 -05:00
Samuel Holland d1a5c1a72e VpnService: Fix some edge cases in ConfigUpdater
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-23 18:04:45 -05:00
Samuel Holland 0e46f95668 Config/Interface: Allow copyFrom() to work on null
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-23 18:02:37 -05:00
Samuel Holland d3e6b311ca BaseConfigActivity: Remember editing state
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-23 18:02:16 -05:00
Samuel Holland b1c1889c50 manifest: ConfigActivity is parent to AddActivity
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-23 17:57:43 -05:00
Samuel Holland c1cdea42b6 Finish first pass of config editor fields
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-22 23:10:41 -05:00
Samuel Holland 3a264f7020 QuickTileService: Update the tile once on service init
This makes the tile show the correct info when it is first added.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-22 23:10:41 -05:00
Samuel Holland 353028420b Add an adapter for binding an ObservableList to a LinearLayout
EditTexts do not work in ListViews.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-22 23:10:35 -05:00
Samuel Holland fb919a7226 Use data binding to provide EditText input filters
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-22 21:48:42 -05:00
Samuel Holland 5d04714eea ConfigAddActivity: Rename to AddActivity
Not everything needs to start with Config

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-22 01:27:29 -05:00
Samuel Holland 90cd59c866 ConfigActivity: Fix fragment state when leaving/entering app
Do this by making the fragment transition functions idempotent.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-21 21:25:31 -05:00
Samuel Holland 9026317b0e Peer: Associate with a Config
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-19 18:50:35 -05:00
Samuel Holland fbd923a060 Minor cleanups
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-19 18:48:07 -05:00
Samuel Holland a49fe9adf5 ObservableAdapters: notify on map/list being set
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-18 23:25:40 -05:00
Samuel Holland 5b4e957122 MapAdapter: Stable IDs are based on value, not key
This fixes list selection to survive renaming configurations.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-17 08:22:19 -05:00
Samuel Holland 85fc5e7ebf ConfigListFragment: Restore the non-FAB add menu
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-17 06:06:35 -05:00
Samuel Holland 416d3adda7 QuickTileService: Implement and update from VpnService
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-17 05:45:40 -05:00
Samuel Holland d5d3566e6c BaseConfigActivity: Make use of inner class's this
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-17 05:39:58 -05:00
Samuel Holland 27241d074e VpnService: Use a string to remember the primary config
This allows simplifies the code a bit. Also, a few other minor changes.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-17 04:50:41 -05:00
Samuel Holland c2189a78b9 ConfigEditFragment: Add extremely basic validation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-17 03:32:01 -05:00
Samuel Holland cb48a7be3a Interface: Rework private key handling
This works much better with a data-bound UI -- no confusing erasing of
the text box, and no crashes, either!

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-17 03:30:06 -05:00
Samuel Holland 8bf12f3f55 VpnService: Remove extraneous call to super()
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-17 02:47:53 -05:00
Samuel Holland 44561a9cb6 ObservableArrayMapAdapter: Remove
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-17 02:44:18 -05:00
Samuel Holland 25412e0537 Convert to using the ObservableTreeMap and its adapter
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-17 02:43:15 -05:00
Samuel Holland 97149fff3f ObservableMapAdapter: Based on an observable TreeMap
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-17 02:40:05 -05:00
Samuel Holland 5023c937ad preferences: Control restoring enabled configs
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-16 04:37:11 -05:00
Samuel Holland b753ae09ca VpnService: Remember and optionally restore enabled configs
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-16 04:36:12 -05:00
Samuel Holland 08992185ec SettingsActivity: Implement primary config setting
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-16 04:34:14 -05:00
Samuel Holland e95b41e48b ConfigListPreference: ListPreference for configs
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-16 04:29:55 -05:00
Samuel Holland 4ac1a2fafc ConfigList: Denote the primary config with bold text
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-16 04:29:04 -05:00
Samuel Holland a5e2c1c1b3 BindingAdapters: Create an adapter for textStyle
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-16 04:30:42 -05:00
Samuel Holland eb43e49a95 VpnService: Remember and maintain the primary config
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-16 04:28:41 -05:00
Samuel Holland 8f0dcd0a32 Config: add an isPrimary property
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-16 04:26:45 -05:00
Samuel Holland 452641a0eb Detail/Edit/ListFragment: Update the layouts
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-16 01:47:18 -05:00
Samuel Holland 645e263424 ConfigActivity: Inline PlaceholderFragment into the layout
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-16 01:43:15 -05:00
Samuel Holland 9e36e0d9c6 ConfigActivity: Make the two-pane layout more readable
Highlight the selected configuration, and add a border between the two
panes.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-16 01:38:46 -05:00
Samuel Holland 76eb65d7d5 Config: Make it Comparable
This will be used for future sorting.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-16 01:06:05 -05:00
Samuel Holland 6d2960b853 ConfigActivity: Adjust when the back arrow is shown
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-16 01:05:13 -05:00
Samuel Holland 73217a098a ConfigListFragment: Implement config selection and removal
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-16 00:55:44 -05:00
Samuel Holland f1d97a585a Config: Rename function to make databinding happy
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-16 00:57:27 -05:00
Samuel Holland b60536222d VpnService: Tweaks for ConfigUpdater to fix adding
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-16 00:56:53 -05:00
Samuel Holland c3203ce90a ConfigDetailFragment: Move function to be in alphabetical order
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-16 00:47:58 -05:00
Samuel Holland ebb0091ad4 ConfigActivity: Fix restoring layout on resume
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-16 00:46:43 -05:00
Samuel Holland 61d6b89bbe ConfigAddActivity: Add an standalone config-creating activity
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-15 17:36:11 -05:00
Samuel Holland e9de916d69 BaseConfigActivity: Set initial config when service available
This was accidentally missed earlier when adding the optimization to
omit binding the service when unnecessary.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-15 17:37:06 -05:00
Samuel Holland 7f864badb2 KeyInputFilter: Extract to its own class
It will be reused for entering public keys of peers.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-15 17:33:00 -05:00
Samuel Holland 2103a28f8f ConfigActivity: Show the current config in the title
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-15 17:07:17 -05:00
Samuel Holland cfcda8bd13 BaseConfigActivity: Move menu handling to ConfigActivity
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-15 16:08:38 -05:00
Samuel Holland d91770a1b6 ConfigActivity: Show back arrow in single layout action bar
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-14 19:33:19 -05:00
Samuel Holland 6604be94eb ListFragment: Don't assume the ListView is the root
It may share the layout with a FAB, and that requires a parent
ViewGroup.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-14 18:40:46 -05:00
Samuel Holland e6a6605ff8 ConfigActivity: Fragments are hard; this un-breaks animations
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-14 18:32:14 -05:00
Samuel Holland ab271e7153 ConfigActivity: Enable animations on fragment transitions
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-14 15:20:57 -05:00
Samuel Holland 83da2aa199 EditFragment: Input filters for config name and private key
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-14 13:53:58 -05:00
Samuel Holland d5ede015cf ConfigActivity: Avoid crash when started while locked
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-14 10:44:29 -05:00
Samuel Holland 5e55d196be Major renaming and refactoring in activity and service
Apparently "configuration" is the proper term, not "profile".

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-13 07:24:03 -05:00
Samuel Holland c72d30a1af Profile: Add function to copy config from another profile
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-13 07:23:16 -05:00
Samuel Holland f84d178e46 ProfileActivity: Remove unnecessary parameterization
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-13 06:22:31 -05:00
Samuel Holland 43500090b2 ProfileService: Rework handling of updates, again
Prefer updating an object instead of replacing it. This preserves the
selection in the UI list. Also make renaming atomic as it should be. Now
the only possibility for data loss is if the old file can be opened but
not written to.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-13 06:16:05 -05:00
Samuel Holland dacd7457d2 ServiceClientFragment: Connect early if possible
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-10 01:11:00 -05:00
Samuel Holland 5fad89527d ProfileList: Fix state tracking
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-10 01:10:15 -05:00
Samuel Holland de53a1b50a ProfileEdit: Finish writing code-behind
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-10 01:05:20 -05:00
Samuel Holland 87d3200b29 ProfileActivity: Do menus correctly
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-10 01:04:46 -05:00
Samuel Holland d6d6f34088 ProfileFragment: Make good use of the cached profile
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-10 00:57:44 -05:00
Samuel Holland d00480a3a5 Settings: Add empty activity
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-09 07:34:28 -05:00
Samuel Holland af8e013e33 Hook up editing to the layout and menus
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-09 07:31:12 -05:00
Samuel Holland c199827b58 ProfileEdit: Add empty activity/fragment
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-09 07:30:25 -05:00
Samuel Holland 671aae7489 ProfileActivity: Do not use onClick for menus
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-09 07:28:04 -05:00
Samuel Holland 7d2d9f0cb3 ProfileActivity: Also remember editing state
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-09 07:25:11 -05:00
Samuel Holland 2c4f605b66 ProfileFragment: Helper class to remember a fragment's profile
Caching the actual profile object is important when the fragment is on
an activity's back stack: when it is shown again, onCreateView will be
called with profile already set and the service already connected. In
this case, there is no later notification from which to bind the
profile, so the existing object needs to be bound there.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-09 07:18:31 -05:00
Samuel Holland 39ed03f758 ProfileList: Add the list fragment in code so it knows the layout
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-09 06:19:37 -05:00
Samuel Holland 52cdf3e7e5 ProfileActivity: Replace fragments instead of hiding
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-09 05:12:00 -05:00
Samuel Holland 2e3daa8913 ProfileActivity: Extract base class for fixing fragments
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-09 03:23:25 -05:00
Samuel Holland 529e320e3f Interface: Correctly handle setting a null or empty key
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-09 02:51:54 -05:00
Samuel Holland f0f9192aed KeyEncoding: Clean up and reorganize to match style
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-09 02:51:08 -05:00
Samuel Holland c3afe5be2c Keypair: Convert to java-style array declarations
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-09 02:44:46 -05:00
Jason A. Donenfeld f6b864d4e7 Constant time base64
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-08 18:22:41 +02:00
Samuel Holland f8d8e5e23e res: Add icons for quick settings tile
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-08 05:55:27 -05:00
Samuel Holland 99973a4b08 Profile: Add helper to check name validity
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-08 05:54:35 -05:00
Samuel Holland c59b3e1230 Profile: Make name modifiable
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-08 05:54:12 -05:00
Samuel Holland 0685d4a159 ProfileActivity: Refactor into clean layers of functionality
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-08 05:51:38 -05:00
Samuel Holland 3076fd8c41 ProfileServiceInterface: Update for map-based collection
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-07 20:19:29 -05:00
Samuel Holland 01524c0dbf ProfileService: Use a map to store profile data
This has no visible changes at the moment, but will allow most functions
to pass around strings instead of Profile objects, obviating the need to
implement serialization for them. It also trades some naive linear
searches for the binary search in SimpleArrayMap.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-07 19:55:11 -05:00
Samuel Holland 7d3e796842 ObservableArrayMapAdapter: Copy ObservableListAdapter
Since the conversion to a sequential list (to implement ListAdapter)
depends on the implementation detail that ObservableArrayMap is backed
by an array, we can't use the ObservableMap interface here.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-07 19:46:19 -05:00
Samuel Holland 2df899eae5 ProfileActivity: Add two-pane master-detail layout
This is designed for tablets, but for testing purposes, it is currently
enabled for all devices in landscape orientation.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-07 18:51:47 -05:00
Samuel Holland 2154306fcb PlaceholderFragment: Simple fragment that shows a message
This is used for the detail pane when no profile is selected.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-07 18:48:46 -05:00
Samuel Holland 4c96e55b49 ProfileActivity: Make local variables final
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-07 18:43:52 -05:00
Samuel Holland 00a755f46f ProfileActivityFragment: Fix service connection tracking
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-07 18:42:38 -05:00
Samuel Holland 372477d0e0 ProfileList: Load fragment programmatically
This is necessary to replace it with the profile detail fragment later.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-07 16:55:02 -05:00
Samuel Holland 93e304ba2d ProfileList: Extract service management into a base class
It will be shared with other fragments.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-04 12:39:56 -05:00
Samuel Holland 81ab643d2b ProfileList: Convert to a fragment
This is required for a future two-fragment tablet layout, and simplifies
the code a bit since the profile detail (view/edit) will be implemented
as fragments anyway.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-04 12:24:18 -05:00
Samuel Holland d0bf3b6b32 ProfileList: remove generated config from UI
It was only there for developing the parser.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-04 00:40:43 -05:00
Samuel Holland 4b401a368f RootShell: Make shell command configurable
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-04 00:37:58 -05:00
Samuel Holland f0ac53e29b icon: Remove transparency inside non-round dragon
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-04 00:35:02 -05:00
Samuel Holland 6c71b88abd ProfileList: Remove "Add" menu action, to be replaced by a FAB
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-01 02:06:17 -05:00
Samuel Holland 5d5cdf54fa ProfileService: Rework profile updating
This should help discourage editing a Profile without making a copy
first, since the caller has to keep track of the old Profile as well.

ProfileAdder has been updated to prevent adding duplicate profiles. It
checks once in the constructor, so the caller can catch the exception
and pass the error back to the UI. It checks again in the worker thread
to prevent any race from happening if a profile is added twice quickly.
Either the file exists, or it doesn't.

Additionally, this change solves the race condition when the old
profile is removed before it is updated; previously this would lead
to the profile being re-added. Now, ProfileRemover will fail and the
profile will stay removed.

Finally, updating a profile's name should now work correctly. There were
previously multiple bugs with that (the old profile wasn't removed, the
new one could duplicate a name, the new one could overwrite some random
other one, etc.).

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-01 01:38:39 -05:00
Samuel Holland 874db0b95e Interface: Convert to using Keypair class
This allows retrieving the public key and generating keypairs, both of
which will be exposed in the UI.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-01 01:12:59 -05:00
Samuel Holland 19e8087642 Keypair: Create class for generating/storing keys
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-01 01:08:56 -05:00
Samuel Holland 4208d524b1 Curve25519: Import class from noise-java
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-01 01:06:34 -05:00
Samuel Holland 1e0a437c3b RootShell: rename SETUP to SETUP_TEMPLATE
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-08-04 00:37:29 -05:00
Samuel Holland e718a7c03c RootShell: Use the application cache dir as TMPDIR
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-31 23:21:59 -05:00
Samuel Holland 544812b2a7 ProfileService: Use wg to enumerate interfaces
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-31 23:11:18 -05:00
Samuel Holland b324e7b7f1 BootCompletedReceiver: Enforce receiving the correct broadcast
This satisfies an Android security lint.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-31 22:15:50 -05:00
Samuel Holland 84e19d6c72 ProfileList: Add a simple menu
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-29 06:39:37 -05:00
Samuel Holland 411b0716f2 ProfileList: Toggle connection state on click
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-31 21:30:51 -05:00
Samuel Holland b6653fd7f0 ProfileService: Implement the rest of its interface
As per the FIXMEs, the list of profiles should eventually become a map.
Since it can only be modified on the main thread anyway, the current
code is still safe, and it works, it's just not optimal.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-31 21:22:02 -05:00
Samuel Holland 0ea5ae605c ProfileService: Use deep copy to clone profiles
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-31 21:20:04 -05:00
Samuel Holland ca708ba382 Profile: Implement deep copying
This is a simple, naive implementation that {,de}serializes the
profile's state, but it does not depend on the internal representation
of Profile or its contained classes.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-31 21:14:42 -05:00
Samuel Holland c3b42b85cc Copyable: New utility interface for deep-copyable classes
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-31 21:11:33 -05:00
Samuel Holland 465a969a70 ProfileList: Add indicator of profile connection state
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-31 21:29:29 -05:00
Samuel Holland 0451370caf ProfileService: Check for existing interfaces when loading profiles
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-31 21:18:14 -05:00
Samuel Holland a1e334efa6 Profile: Track connection state
Observability is only enabled for isConnected because it is the only
mutable property.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-31 21:13:01 -05:00
Samuel Holland 8623437185 ProfileService: Expand and document service interface
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-31 19:06:47 -05:00
Samuel Holland 2b56dd5d8f RootShell: Add helper class for running commands as root
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-31 19:00:05 -05:00
Samuel Holland 773190f57d ProfileService: Add a listener to start it on boot
This will allow automatically starting profiles on boot.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-30 02:01:38 -05:00
Samuel Holland d8a5ec3f19 ProfileService: Only load from files ending with .conf
This condition was previously enforced in the AsyncTask, but was lost in
the move from ProfileListActivity to ProfileService.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-30 02:18:17 -05:00
Samuel Holland c65ac9fafe ProfileService: Create it and move profile loading
The long-running service is needed for keeping track of which profiles
are enabled, for showing notifications, and for the tile service to use.
Since it has to know which profiles exist anyway, moving the main
ObservableList there avoids some code duplication. It ensures the list
is only loaded once, so it cannot get out of sync. It also makes the
ProfileList activity load faster, because it doesn't have to wait for
file I/O; and it provides a canonical place for storing the Profile
objects so they are accessible everywhere, instead of having to look
them up by name.

This does present some challenges with leaking activities, because all
listeners must be removed from the profiles list (and its contents) when
an activity is stopped.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-30 01:48:57 -05:00
Samuel Holland 5af6703157 binding: Weakly reference adapter in list change callback
This allows the ObservableListAdapter to be garbage collected along with
its activity. On the next list change, the OnListChangedCallback will
detect that the adapter is gone and clean itself up. This avoids needing
to manually remove the list from the adapter, as would otherwise be
required to break the reference cycle adapter→list→listener→adapter.

This will be relevant once the list of profiles outlives the activity.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-30 18:44:57 -05:00
Samuel Holland 7ceafaf2bb ProfileList: Add minimal activity
For now, it simply reads the files in the app's data directory with
file names ending in ".conf" and displays them in a list.

This includes the generic list data binding setup for future use.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-29 06:09:55 -05:00
Samuel Holland 85f1d4f1d3 Profile: Add a blank line before each peer section
This is purely a cosmetic change in the generated config files.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-29 17:40:36 -05:00
Samuel Holland 748d780a47 Profile: Parse config file to a string per attribute
This parser should be able to handle any valid WireGuard or wg-quick
configuration file. It separates the file into a single interface object
and a peer object for each peer. All "[Interface]" sections in the file
are combined into the one object.

For now, later lines in a block with the same key overwrite earlier
lines. This is only relevant for attributes that are lists, such as
Address and AllowedIPs, where additional lines may be expected to append
to the list.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-29 17:30:33 -05:00
Samuel Holland 0494dd1404 Profile: Add minimal implementation
This represents a wg-quick profile as two strings: the file name and
the file contents.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-29 06:08:54 -05:00
Samuel Holland f8b7030e22 project: Add gradle wrapper
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-28 23:11:24 -05:00
Samuel Holland 5a82ecf18a icon: Use the uncropped logo for the non-round icon
The round icon remains the same.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-30 22:47:24 -05:00
Samuel Holland 56cf8816d3 project: Create empty project with WireGuard icon
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-28 23:10:37 -05:00
Jason A. Donenfeld b8cb52ed4b project: Skeleton with license and readme
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-31 03:00:41 +02:00
Jason A. Donenfeld 506d871013 cli: only call DNS if there is DNS
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-31 22:26:00 +02:00
Jason A. Donenfeld b2f4a755a0 cli: add multi-DNS to wg-quick
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-31 05:15:05 +02:00
Jason A. Donenfeld df4fc98493 cli: import from contrib/examples/android
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-31 03:02:26 +02:00
Samuel Holland e85142dbfc git: Initialize repository
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2017-07-28 22:59:47 -05:00
336 changed files with 10695 additions and 21129 deletions
+2 -2
View File
@@ -14,5 +14,5 @@ build/
*.dex
*.iml
*.jks
gradlew.bat
maint/
keystore.properties
package-info.java
+6 -6
View File
@@ -1,6 +1,6 @@
[submodule "tunnel/tools/amneziawg-tools"]
path = tunnel/tools/amneziawg-tools
url = https://github.com/amnezia-vpn/amneziawg-tools
[submodule "tunnel/tools/elf-cleaner"]
path = tunnel/tools/elf-cleaner
url = https://github.com/termux/termux-elf-cleaner
[submodule "app/tools/libmnl"]
path = app/tools/libmnl
url = https://git.netfilter.org/libmnl/
[submodule "app/tools/wireguard"]
path = app/tools/wireguard
url = https://git.zx2c4.com/WireGuard
+507
View File
@@ -0,0 +1,507 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="AUTODETECT_INDENTS" value="false" />
<option name="LINE_SEPARATOR" value="&#10;" />
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
<value />
</option>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
</value>
</option>
<JavaCodeStyleSettings>
<option name="GENERATE_FINAL_LOCALS" value="true" />
<option name="GENERATE_FINAL_PARAMETERS" value="true" />
<option name="INSERT_INNER_CLASS_IMPORTS" value="true" />
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
</value>
</option>
</JavaCodeStyleSettings>
<XML>
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
</XML>
<codeStyleSettings language="JAVA">
<option name="METHOD_ANNOTATION_WRAP" value="0" />
<option name="FIELD_ANNOTATION_WRAP" value="0" />
<arrangement>
<groups />
<rules>
<section>
<rule>
<match>
<AND>
<FIELD>true</FIELD>
<FINAL>true</FINAL>
<PUBLIC>true</PUBLIC>
<STATIC>true</STATIC>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD>true</FIELD>
<FINAL>true</FINAL>
<PROTECTED>true</PROTECTED>
<STATIC>true</STATIC>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD>true</FIELD>
<FINAL>true</FINAL>
<PACKAGE_PRIVATE>true</PACKAGE_PRIVATE>
<STATIC>true</STATIC>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD>true</FIELD>
<FINAL>true</FINAL>
<PRIVATE>true</PRIVATE>
<STATIC>true</STATIC>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD>true</FIELD>
<PUBLIC>true</PUBLIC>
<STATIC>true</STATIC>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD>true</FIELD>
<PROTECTED>true</PROTECTED>
<STATIC>true</STATIC>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD>true</FIELD>
<PACKAGE_PRIVATE>true</PACKAGE_PRIVATE>
<STATIC>true</STATIC>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD>true</FIELD>
<PRIVATE>true</PRIVATE>
<STATIC>true</STATIC>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<INITIALIZER_BLOCK>true</INITIALIZER_BLOCK>
<STATIC>true</STATIC>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD>true</FIELD>
<FINAL>true</FINAL>
<PUBLIC>true</PUBLIC>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD>true</FIELD>
<FINAL>true</FINAL>
<PROTECTED>true</PROTECTED>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD>true</FIELD>
<FINAL>true</FINAL>
<PACKAGE_PRIVATE>true</PACKAGE_PRIVATE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD>true</FIELD>
<FINAL>true</FINAL>
<PRIVATE>true</PRIVATE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD>true</FIELD>
<PUBLIC>true</PUBLIC>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD>true</FIELD>
<PROTECTED>true</PROTECTED>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD>true</FIELD>
<PACKAGE_PRIVATE>true</PACKAGE_PRIVATE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<FIELD>true</FIELD>
<PRIVATE>true</PRIVATE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<FIELD>true</FIELD>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<INITIALIZER_BLOCK>true</INITIALIZER_BLOCK>
</match>
</rule>
</section>
<section>
<rule>
<match>
<CONSTRUCTOR>true</CONSTRUCTOR>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<METHOD>true</METHOD>
<STATIC>true</STATIC>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<METHOD>true</METHOD>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<ENUM>true</ENUM>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<INTERFACE>true</INTERFACE>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<CLASS>true</CLASS>
<STATIC>true</STATIC>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<CLASS>true</CLASS>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_width</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_height</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:width</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:height</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
</code_scheme>
</component>
+5
View File
@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
+6
View File
@@ -0,0 +1,6 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="Copyright © &amp;#36;today.year WireGuard LLC. All Rights Reserved.&#10;SPDX-License-Identifier: Apache-2.0" />
<option name="myName" value="Default" />
</copyright>
</component>
+3
View File
@@ -0,0 +1,3 @@
<component name="CopyrightManager">
<settings default="Default" />
</component>
+526
View File
@@ -0,0 +1,526 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0" is_locked="false">
<option name="myName" value="Default" />
<inspection_tool class="AbstractClassNamingConvention" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="AbstractClassWithoutAbstractMethods" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AccessToNonThreadSafeStaticFieldFromInstance" enabled="true" level="WARNING" enabled_by_default="true">
<option name="nonThreadSafeClasses">
<value />
</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" />
<inspection_tool class="AnnotationNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
<option name="m_regex" value="[A-Z][A-Za-z\d]*" />
<option name="m_minLength" value="8" />
<option name="m_maxLength" value="64" />
</inspection_tool>
<inspection_tool class="AnonymousClassVariableHidesContainingMethodVariable" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AnonymousInnerClass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AnonymousInnerClassMayBeStatic" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ApiName" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="ApiNamespace" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="ApiParameter" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="ArrayEquality" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ArrayIssues" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="AssignmentOrReturnOfFieldWithMutableTypeMerged" />
<inspection_tool class="AssignmentToCatchBlockParameter" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AssignmentToCollectionFieldFromParameter" enabled="false" level="WARNING" enabled_by_default="false">
<option name="ignorePrivateMethods" value="false" />
</inspection_tool>
<inspection_tool class="AssignmentToDateFieldFromParameter" enabled="false" level="WARNING" enabled_by_default="false">
<option name="ignorePrivateMethods" value="true" />
</inspection_tool>
<inspection_tool class="AssignmentToForLoopParameter" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_checkForeachParameters" value="false" />
</inspection_tool>
<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="BadExceptionCaught" enabled="true" level="WARNING" enabled_by_default="true">
<option name="exceptionsString" value="" />
<option name="exceptions">
<value />
</option>
</inspection_tool>
<inspection_tool class="BadOddness" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="BooleanExpressionMayBeConditional" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="BooleanParameter" enabled="true" level="WARNING" enabled_by_default="true">
<option name="onlyReportMultiple" value="true" />
</inspection_tool>
<inspection_tool class="CallToSimpleGetterInClass" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreGetterCallsOnOtherObjects" value="false" />
<option name="onlyReportPrivateGetter" value="false" />
</inspection_tool>
<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="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" />
<inspection_tool class="ClassNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
<option name="m_regex" value="[A-Z][A-Za-z\d]*" />
<option name="m_minLength" value="8" />
<option name="m_maxLength" value="64" />
</inspection_tool>
<inspection_tool class="ClassReferencesSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ClassWithOnlyPrivateConstructors" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ComparableImplementedButEqualsNotOverridden" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CompareToUsesNonFinalVariable" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ConditionalExpression" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ConditionalExpressionWithIdenticalBranches" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ConstExpressionRequired" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="ConstantNamingConvention" enabled="true" level="WARNING" enabled_by_default="true">
<option name="onlyCheckImmutables" value="false" />
<option name="m_regex" value="[A-Z][A-Z_\d]*" />
<option name="m_minLength" value="0" />
<option name="m_maxLength" value="0" />
</inspection_tool>
<inspection_tool class="ConstantValueVariableUse" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ConstructionIsNotAllowed" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="Constructor" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="Convert2streamapi" enabled="true" level="WARNING" enabled_by_default="true">
<option name="REPLACE_TRIVIAL_FOREACH" value="true" />
<option name="SUGGEST_FOREACH" value="true" />
</inspection_tool>
<inspection_tool class="ConvertAnnotations" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="CovariantEquals" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DeclareCollectionAsInterface" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreLocalVariables" value="false" />
<option name="ignorePrivateMethodsAndFields" value="false" />
</inspection_tool>
<inspection_tool class="DefaultNotLastCaseInSwitch" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DerivedFunctionsReturnTypeMismatch" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="DeserializableClassInSecureContext" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="DoubleBraceInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DoubleCheckedLocking" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreOnVolatileVariables" value="false" />
</inspection_tool>
<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" />
<inspection_tool class="EmptyClass" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignorableAnnotations">
<value />
</option>
<option name="ignoreClassWithParameterization" value="true" />
<option name="ignoreThrowables" value="true" />
<option name="commentsAreContent" value="true" />
</inspection_tool>
<inspection_tool class="EmptySynchronizedStatement" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="EnumSwitchStatementWhichMissesCases" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreSwitchStatementsWithDefault" value="false" />
</inspection_tool>
<inspection_tool class="EnumeratedClassNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
<option name="m_regex" value="[A-Z][A-Za-z\d]*" />
<option name="m_minLength" value="8" />
<option name="m_maxLength" value="64" />
</inspection_tool>
<inspection_tool class="EnumeratedConstantNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
<option name="m_regex" value="[A-Z][A-Z_\d]*" />
<option name="m_minLength" value="5" />
<option name="m_maxLength" value="32" />
</inspection_tool>
<inspection_tool class="EnumerationCanBeIteration" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="EqualityOperatorComparesObjects" enabled="true" level="INFORMATION" enabled_by_default="true" />
<inspection_tool class="EqualsAndHashcode" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="EqualsCalledOnEnumConstant" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="EqualsUsesNonFinalVariable" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="EscapedMetaCharacter" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ExceptionFromCatchWhichDoesntWrap" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreGetMessage" value="false" />
<option name="ignoreCantWrap" value="false" />
</inspection_tool>
<inspection_tool class="ExtendsThrowable" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ExtendsUtilityClass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="FallthruInSwitchStatement" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="FieldAccessedSynchronizedAndUnsynchronized" enabled="true" level="WARNING" enabled_by_default="true">
<option name="countGettersAndSetters" value="false" />
</inspection_tool>
<inspection_tool class="FieldMayBeFinal" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="FieldMayBeStatic" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="FieldMustBeInitialized" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="FinalMethodInFinalClass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="FloatingPointEquality" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ForLoopReplaceableByWhile" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreLoopsWithoutConditions" value="false" />
</inspection_tool>
<inspection_tool class="FrequentlyUsedInheritorInspection" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="FullJavaName" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="FullMethodName" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="FunctionParameterCountMismatch" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="FuseStreamOperations" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="GrFieldAlreadyDefined" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="GroovyAnnotationNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
<option name="m_regex" value="[A-Z][A-Za-z\d]*" />
<option name="m_minLength" value="8" />
<option name="m_maxLength" value="64" />
</inspection_tool>
<inspection_tool class="GroovyClassNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
<option name="m_regex" value="[A-Z][A-Za-z\d]*" />
<option name="m_minLength" value="8" />
<option name="m_maxLength" value="64" />
</inspection_tool>
<inspection_tool class="GroovyEnumerationNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
<option name="m_regex" value="[A-Z][A-Za-z\d]*" />
<option name="m_minLength" value="8" />
<option name="m_maxLength" value="64" />
</inspection_tool>
<inspection_tool class="GroovyInterfaceNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
<option name="m_regex" value="[A-Z][A-Za-z\d]*" />
<option name="m_minLength" value="8" />
<option name="m_maxLength" value="64" />
</inspection_tool>
<inspection_tool class="HashCodeUsesNonFinalVariable" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="HtmlTagCanBeJavadocTag" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="IOResource" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredTypesString" value="java.io.ByteArrayOutputStream,java.io.ByteArrayInputStream,java.io.StringBufferInputStream,java.io.CharArrayWriter,java.io.CharArrayReader,java.io.StringWriter,java.io.StringReader" />
<option name="insideTryAllowed" value="false" />
</inspection_tool>
<inspection_tool class="IfMayBeConditional" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="IfStatementWithIdenticalBranches" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="IgnoreResultOfCall" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_reportAllNonLibraryCalls" value="false" />
<option name="callCheckString" value="java.io.File,.*,java.io.InputStream,read|skip|available|markSupported,java.io.Reader,read|skip|ready|markSupported,java.lang.Boolean,.*,java.lang.Byte,.*,java.lang.Character,.*,java.lang.Double,.*,java.lang.Float,.*,java.lang.Integer,.*,java.lang.Long,.*,java.lang.Math,.*,java.lang.Object,equals|hashCode|toString,java.lang.Short,.*,java.lang.StrictMath,.*,java.lang.String,.*,java.math.BigInteger,.*,java.math.BigDecimal,.*,java.net.InetAddress,.*,java.net.URI,.*,java.util.UUID,.*,java.util.regex.Matcher,pattern|toMatchResult|start|end|group|groupCount|matches|find|lookingAt|quoteReplacement|replaceAll|replaceFirst|regionStart|regionEnd|hasTransparantBounds|hasAnchoringBounds|hitEnd|requireEnd,java.util.regex.Pattern,.*,java.util.stream.BaseStream,.*" />
</inspection_tool>
<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" />
</inspection_tool>
<inspection_tool class="InstanceMethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
<option name="m_regex" value="[a-z][A-Za-z\d]*" />
<option name="m_minLength" value="4" />
<option name="m_maxLength" value="32" />
</inspection_tool>
<inspection_tool class="InstanceVariableNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
<option name="m_regex" value="m_[a-z][A-Za-z\d]*" />
<option name="m_minLength" value="5" />
<option name="m_maxLength" value="32" />
</inspection_tool>
<inspection_tool class="InstanceofCatchParameter" enabled="true" level="WARNING" enabled_by_default="true" />
<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">
<option name="m_regex" value="[A-Z][A-Za-z\d]*" />
<option name="m_minLength" value="8" />
<option name="m_maxLength" value="64" />
</inspection_tool>
<inspection_tool class="InvalidParameterAnnotations" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="IteratorNextDoesNotThrowNoSuchElementException" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="JUnit3MethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="JUnit4MethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="JUnitAbstractTestClassNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
<option name="m_regex" value="[A-Z][A-Za-z\d]*TestCase" />
<option name="m_minLength" value="12" />
<option name="m_maxLength" value="64" />
</inspection_tool>
<inspection_tool class="JUnitTestClassNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
<option name="m_regex" value="[A-Z][A-Za-z\d]*Test" />
<option name="m_minLength" value="8" />
<option name="m_maxLength" value="64" />
</inspection_tool>
<inspection_tool class="Java8ArraySetAll" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="Java9CollectionFactory" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="JavaRequiresAutoModule" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="JavadocHtmlLint" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="KeySetIterationMayUseEntrySet" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="LambdaCanBeMethodCall" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="LambdaParameterHidingMemberVariable" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="LengthOneStringsInConcatenation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="LimitedScopeInnerClass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ListIndexOfReplaceableByContains" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="LiteralAsArgToStringEquals" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="LocalCanBeFinal" enabled="true" level="WARNING" enabled_by_default="true">
<option name="REPORT_VARIABLES" value="true" />
<option name="REPORT_PARAMETERS" value="true" />
</inspection_tool>
<inspection_tool class="LocalVariableHidingMemberVariable" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreInvisibleFields" value="true" />
<option name="m_ignoreStaticMethods" value="true" />
</inspection_tool>
<inspection_tool class="LoopWithImplicitTerminationCondition" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="MagicNumber" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreInitialCapacity" value="true" />
</inspection_tool>
<inspection_tool class="MapReplaceableByEnumMap" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="MemberVisibility" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="MemberVisibilityCanPrivate" enabled="true" level="INFO" enabled_by_default="true" />
<inspection_tool class="MethodMayBeStatic" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_onlyPrivateOrFinal" value="false" />
<option name="m_ignoreEmptyMethods" value="true" />
</inspection_tool>
<inspection_tool class="MethodMayBeSynchronized" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="MethodName" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="MethodOnlyUsedFromInnerClass" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreMethodsAccessedFromAnonymousClass" value="false" />
<option name="ignoreStaticMethodsFromNonStaticInnerClass" value="false" />
<option name="onlyReportStaticMethods" value="false" />
</inspection_tool>
<inspection_tool class="MethodOverloadsParentMethod" enabled="true" level="WARNING" enabled_by_default="true">
<option name="reportIncompatibleParameters" value="true" />
</inspection_tool>
<inspection_tool class="MethodOverridesInaccessibleMethodOfSuper" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="MethodOverridesStaticMethod" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="MethodParameterType" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="MethodReturnType" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="MissingDeprecatedAnnotation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="MissingOverrideAnnotation" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreObjectMethods" value="false" />
<option name="ignoreAnonymousClassMethods" value="false" />
</inspection_tool>
<inspection_tool class="MissortedModifiers" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_requireAnnotationsFirst" value="true" />
</inspection_tool>
<inspection_tool class="MoveFieldAssignmentToInitializer" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="MultipleDeclaration" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreForLoopDeclarations" value="true" />
</inspection_tool>
<inspection_tool class="MultipleTopLevelClassesInFile" 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">
<option name="m_ignoreNegatedNullComparison" value="true" />
</inspection_tool>
<inspection_tool class="NegatedConditionalExpression" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NegatedEqualityExpression" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NegatedIfElse" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreNegatedNullComparison" value="true" />
<option name="m_ignoreNegatedZeroComparison" value="false" />
</inspection_tool>
<inspection_tool class="NegativelyNamedBooleanVariable" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NestedSynchronizedStatement" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NewClassNamingConventionMerged" />
<inspection_tool class="NewExceptionWithoutArguments" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NewGroovyClassNamingConventionMerged" />
<inspection_tool class="NewMethodNamingConventionMerged" />
<inspection_tool class="NoDefaultBaseConstructor" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="NonAsciiCharacters" enabled="true" level="WARNING" enabled_by_default="true">
<option name="CHECK_FOR_FILES_CONTAINING_BOM" value="true" />
</inspection_tool>
<inspection_tool class="NonExceptionNameEndsWithException" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NonFinalFieldInEnum" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NonFinalFieldOfException" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NonFinalStaticVariableUsedInClassInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NonFinalUtilityClass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NonProtectedConstructorInAbstractClass" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreNonPublicClasses" value="false" />
</inspection_tool>
<inspection_tool class="NonShortCircuitBoolean" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NonSynchronizedMethodOverridesSynchronizedMethod" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NonThreadSafeLazyInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NoopMethodInAbstractClass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NotAssignable" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="NullThrown" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ObjectEquality" enabled="false" level="WARNING" enabled_by_default="false">
<option name="m_ignoreEnums" value="true" />
<option name="m_ignoreClassObjects" value="false" />
<option name="m_ignorePrivateConstructors" value="false" />
</inspection_tool>
<inspection_tool class="ObjectInstantiationInEqualsHashCode" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ObjectToString" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ObsoleteCollection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreRequiredObsoleteCollectionTypes" value="false" />
</inspection_tool>
<inspection_tool class="OctalEscape" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="OptionalContainsCollection" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="OverlyStrongTypeCast" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreInMatchingInstanceof" value="true" />
</inspection_tool>
<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">
<option name="ignoreEnums" value="false" />
<option name="ignoreInterfaces" value="false" />
</inspection_tool>
<inspection_tool class="ProtectedMemberInFinalClass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PublicConstructorInNonPublicClass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PublicField" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreEnums" value="false" />
<option name="ignorableAnnotations">
<value />
</option>
</inspection_tool>
<inspection_tool class="PublicFieldAccessedInSynchronizedContext" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="RandomDoubleForRandomInteger" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="RedundantFieldInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="RedundantImplements" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreSerializable" value="false" />
<option name="ignoreCloneable" value="false" />
</inspection_tool>
<inspection_tool class="RedundantMethodOverride" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="RedundantThrowsDeclaration" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="RepeatedSpace" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ReplaceAssignmentWithOperatorAssignment" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreLazyOperators" value="true" />
<option name="ignoreObscureOperators" value="false" />
</inspection_tool>
<inspection_tool class="ReplaceCallWithComparison" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="RequireNonNull" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="ResourceParameter" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RestSignature" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="ResultOfObjectAllocationIgnored" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ReturnOfCollectionField" enabled="false" level="WARNING" enabled_by_default="false">
<option name="ignorePrivateMethods" value="true" />
</inspection_tool>
<inspection_tool class="ReturnOfDateField" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ScalarTypeRequired" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="SerializableClassInSecureContext" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SetReplaceableByEnumSet" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SimplifiableAnnotation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SimplifiableEqualsExpression" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SingleCharAlternation" enabled="true" level="WARNING" enabled_by_default="true" />
<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="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]*" />
<option name="m_minLength" value="4" />
<option name="m_maxLength" value="32" />
</inspection_tool>
<inspection_tool class="StaticNonFinalField" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StaticVariableNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
<option name="checkMutableFinals" value="false" />
<option name="m_regex" value="s_[a-z][A-Za-z\d]*" />
<option name="m_minLength" value="5" />
<option name="m_maxLength" value="32" />
</inspection_tool>
<inspection_tool class="StaticnessMismatch" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="StringBufferToStringInConcatenation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StringConcatenationInFormatCall" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StringConcatenationMissingWhitespace" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StringEqualsCharSequence" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="StringEqualsEmptyString" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StringReplaceableByStringBuffer" enabled="true" level="WARNING" enabled_by_default="true">
<option name="onlyWarnOnLoop" value="true" />
</inspection_tool>
<inspection_tool class="SubtractionInCompareTo" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SuperClassHasFrequentlyUsedInheritors" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SuspiciousArrayCast" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SuspiciousIndentAfterControlStatement" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SwitchStatementWithConfusingDeclaration" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SynchronizeOnLock" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SynchronizeOnThis" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SynchronizedMethod" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_includeNativeMethods" value="true" />
<option name="ignoreSynchronizedSuperMethods" value="true" />
</inspection_tool>
<inspection_tool class="SynchronizedOnLiteralObject" enabled="true" level="WARNING" enabled_by_default="true" />
<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" />
<inspection_tool class="ThrowCaughtLocally" enabled="true" level="WARNING" enabled_by_default="true">
<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" />
</inspection_tool>
<inspection_tool class="TrivialMethodReference" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="TrivialStringConcatenation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="TypeMayBeWeakened" enabled="true" level="WARNING" enabled_by_default="true">
<option name="useRighthandTypeAsWeakestTypeInAssignments" value="true" />
<option name="useParameterizedTypeForCollectionMethods" value="true" />
<option name="doNotWeakenToJavaLangObject" value="true" />
<option name="onlyWeakentoInterface" value="true" />
</inspection_tool>
<inspection_tool class="TypeParameterExtendsFinalClass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="TypeParameterNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
<option name="m_regex" value="[A-Z][A-Za-z\d]*" />
<option name="m_minLength" value="1" />
<option name="m_maxLength" value="1" />
</inspection_tool>
<inspection_tool class="UnaryPlus" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessarilyQualifiedInnerClassAccess" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreReferencesNeedingImport" value="true" />
</inspection_tool>
<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" />
<option name="ignoreInModuleStatements" value="true" />
</inspection_tool>
<inspection_tool class="UnnecessaryInheritDoc" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryJavaDocLink" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreInlineLinkToSuper" value="false" />
</inspection_tool>
<inspection_tool class="UnnecessaryQualifierForThis" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessarySuperConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessarySuperQualifier" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryThis" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryToStringCall" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryUnaryMinus" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnsecureRandomNumberGeneration" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnusedCatchParameter" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreCatchBlocksWithComments" value="false" />
<option name="m_ignoreTestCases" value="false" />
</inspection_tool>
<inspection_tool class="UnusedImport" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UpperCaseFieldNameNotConstant" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UseOfClone" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UseOfObsoleteDateTimeApi" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UtilityClassWithPublicConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UtilityClassWithoutPrivateConstructor" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignorableAnnotations">
<value />
</option>
<option name="ignoreClassesWithOnlyMain" value="false" />
</inspection_tool>
<inspection_tool class="WeakerAccess" enabled="true" level="WARNING" enabled_by_default="true">
<option name="SUGGEST_PACKAGE_LOCAL_FOR_MEMBERS" value="false" />
<option name="SUGGEST_PACKAGE_LOCAL_FOR_TOP_CLASSES" value="true" />
<option name="SUGGEST_PRIVATE_FOR_INNERS" value="false" />
</inspection_tool>
<inspection_tool class="WhileLoopSpinsOnField" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreNonEmtpyLoops" value="false" />
</inspection_tool>
<inspection_tool class="ZeroLengthArrayInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="unused" enabled="true" level="WARNING" enabled_by_default="true" klass="packageLocal" inner_class="protected" field="protected" method="protected" parameter="protected">
<option name="LOCAL_VARIABLE" value="true" />
<option name="FIELD" value="true" />
<option name="METHOD" value="true" />
<option name="CLASS" value="true" />
<option name="PARAMETER" value="true" />
<option name="REPORT_PARAMETER_FOR_PUBLIC_METHODS" value="false" />
<option name="ADD_MAINS_TO_ENTRIES" value="true" />
<option name="ADD_APPLET_TO_ENTRIES" value="true" />
<option name="ADD_SERVLET_TO_ENTRIES" value="true" />
<option name="ADD_NONJAVA_TO_ENTRIES" value="true" />
</inspection_tool>
</profile>
</component>
+6
View File
@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="PROJECT_PROFILE" value="Default" />
<version value="1.0" />
</settings>
</component>
+5 -7
View File
@@ -1,15 +1,13 @@
# Android GUI for [AmneziaWG](https://amnezia.org/learn-more/31_amneziawg)
# Android GUI for [WireGuard](https://www.wireguard.com/)
**[Download from the Play Store](https://play.google.com/store/apps/details?id=org.amnezia.awg)**
**[Download from the Play Store](https://play.google.com/store/apps/details?id=com.wireguard.android)**
This is an Android GUI for [AmneziaWG](https://amnezia.org/learn-more/31_amneziawg).
This is an Android GUI for [WireGuard](https://www.wireguard.com/). It [opportunistically uses the kernel implementation](https://git.zx2c4.com/android_kernel_wireguard/about/), and falls back to using the non-root [userspace implementation](https://git.zx2c4.com/wireguard-go/about/).
## Building
```
$ git clone --recurse-submodules https://github.com/amnezia-vpn/amneziawg-android
$ cd amneziawg-android
$ 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).
+104
View File
@@ -0,0 +1,104 @@
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 455
versionName '0.0.20191013'
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.0'
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
@@ -0,0 +1,87 @@
/*
* 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
@@ -0,0 +1,3 @@
# Squelch all warnings, they're harmless but ProGuard
# escalates them as errors.
-dontwarn sun.misc.Unsafe
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">WireGuard β</string>
</resources>
@@ -1,31 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.wireguard.android"
android:installLocation="internalOnly">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission
android:name="android.permission.SYSTEM_ALERT_WINDOW"
android:minSdkVersion="34" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"
tools:ignore="ScopedStorage" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-feature
android:name="android.hardware.camera.any"
android:required="false" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<permission
android:name="${applicationId}.permission.CONTROL_TUNNELS"
@@ -37,22 +19,15 @@
<application
android:name=".Application"
android:allowBackup="false"
android:banner="@mipmap/banner"
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme"
tools:ignore="UnusedAttribute">
<activity
android:name=".activity.TunnelToggleActivity"
android:excludeFromRecents="true"
android:theme="@style/NoBackgroundTheme" />
<activity android:name=".activity.MainActivity">
<activity
android:name=".activity.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -64,16 +39,6 @@
</intent-filter>
</activity>
<activity
android:name=".activity.TvMainActivity"
android:exported="true"
android:theme="@style/TvTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".activity.SettingsActivity"
android:label="@string/settings"
@@ -89,24 +54,7 @@
android:screenOrientation="fullSensor"
tools:replace="screenOrientation" />
<activity
android:name=".activity.LogViewerActivity"
android:exported="false"
android:label="@string/log_viewer_title">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>
<provider
android:name=".activity.LogViewerActivity$ExportedLogContentProvider"
android:authorities="${applicationId}.exported-log"
android:exported="false"
android:grantUriPermissions="true" />
<receiver
android:name=".BootShutdownReceiver"
android:exported="true">
<receiver android:name=".BootShutdownReceiver">
<intent-filter>
<action android:name="android.intent.action.ACTION_SHUTDOWN" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
@@ -115,18 +63,24 @@
<receiver
android:name=".model.TunnelManager$IntentReceiver"
android:exported="true"
android:permission="${applicationId}.permission.CONTROL_TUNNELS">
<intent-filter>
<action android:name="org.amnezia.awg.action.REFRESH_TUNNEL_STATES" />
<action android:name="org.amnezia.awg.action.SET_TUNNEL_UP" />
<action android:name="org.amnezia.awg.action.SET_TUNNEL_DOWN" />
<action android:name="com.wireguard.android.action.REFRESH_TUNNEL_STATES" />
<action android:name="com.wireguard.android.action.SET_TUNNEL_UP" />
<action android:name="com.wireguard.android.action.SET_TUNNEL_DOWN" />
</intent-filter>
</receiver>
<service
android:name=".backend.GoBackend$VpnService"
android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
<service
android:name=".QuickTileService"
android:exported="true"
android:icon="@drawable/ic_tile"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
@@ -137,21 +91,6 @@
<meta-data
android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="false" />
<meta-data
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
</service>
<meta-data
android:name="android.content.APP_RESTRICTIONS"
android:resource="@xml/app_restrictions" />
</application>
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent>
</queries>
</manifest>
@@ -0,0 +1,147 @@
/*
* 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 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.io.File;
import java.lang.ref.WeakReference;
import java9.util.concurrent.CompletableFuture;
public class Application extends android.app.Application {
@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() {
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);
}
}
@@ -0,0 +1,38 @@
/*
* 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();
}
});
}
}
@@ -0,0 +1,175 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android;
import android.annotation.TargetApi;
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 android.util.Log;
import android.widget.Toast;
import com.wireguard.android.activity.MainActivity;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.util.ErrorMessages;
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.
*/
@TargetApi(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) {
final Tile tile = getQsTile();
if (tile != null) {
tile.setIcon(tile.getIcon() == iconOn ? iconOff : iconOn);
tile.updateTile();
}
tunnel.setState(State.TOGGLE).whenComplete(this::onToggleFinished);
} 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 onToggleFinished(@SuppressWarnings("unused") final State state,
@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();
}
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();
}
}
}
@@ -0,0 +1,99 @@
/*
* 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());
}
}
}
@@ -0,0 +1,139 @@
/*
* 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();
}
}
}
@@ -0,0 +1,128 @@
/*
* 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);
});
}
}
}
}
@@ -0,0 +1,82 @@
/*
* 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();
}
}
}
@@ -0,0 +1,34 @@
/*
* 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();
}
}
@@ -0,0 +1,79 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
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 java.util.Set;
/**
* Interface for implementations of the WireGuard secure network tunnel.
*/
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.
*
* @return The set of running tunnel names.
*/
Set<String> enumerate();
/**
* Get the actual state of a tunnel.
*
* @param tunnel The tunnel to examine the state of.
* @return The state of the tunnel.
*/
State getState(Tunnel tunnel) throws Exception;
/**
* Get statistics about traffic and errors on this tunnel. If the tunnel is not running, the
* statistics object will be filled with zero values.
*
* @param tunnel The tunnel to retrieve statistics for.
* @return The statistics for the tunnel.
*/
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
*/
String getVersion() throws Exception;
/**
* Set the state of a tunnel.
*
* @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}.
* @return The updated state of the tunnel.
*/
State setState(Tunnel tunnel, State state) throws Exception;
}
@@ -0,0 +1,241 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.backend;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.ParcelFileDescriptor;
import androidx.annotation.Nullable;
import androidx.collection.ArraySet;
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.util.SharedLibraryLoader;
import com.wireguard.config.Config;
import com.wireguard.config.InetNetwork;
import com.wireguard.config.Peer;
import java.net.InetAddress;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java9.util.concurrent.CompletableFuture;
public final class GoBackend implements Backend {
private static final String TAG = "WireGuard/" + GoBackend.class.getSimpleName();
private static CompletableFuture<VpnService> vpnService = new CompletableFuture<>();
private final Context context;
@Nullable private Tunnel currentTunnel;
private int currentTunnelHandle = -1;
public GoBackend(final Context context) {
SharedLibraryLoader.loadSharedLibrary(context, "wg-go");
this.context = context;
}
private static native int wgGetSocketV4(int handle);
private static native int wgGetSocketV6(int handle);
private static native void wgTurnOff(int handle);
private static native int wgTurnOn(String ifName, int tunFd, String settings);
private static native String wgVersion();
@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() {
if (currentTunnel != null) {
final Set<String> runningTunnels = new ArraySet<>();
runningTunnels.add(currentTunnel.getName());
return runningTunnels;
}
return Collections.emptySet();
}
@Override
public State getState(final Tunnel tunnel) {
return currentTunnel == tunnel ? State.UP : State.DOWN;
}
@Override
public Statistics getStatistics(final Tunnel tunnel) {
return new Statistics();
}
@Override
public String getTypePrettyName() {
return context.getString(R.string.type_name_go_userspace);
}
@Override
public String getVersion() {
return wgVersion();
}
@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;
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);
return getState(tunnel);
}
private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state)
throws Exception {
if (state == State.UP) {
Log.i(TAG, "Bringing tunnel up");
Objects.requireNonNull(config, context.getString(R.string.no_config_error));
if (VpnService.prepare(context) != null)
throw new Exception(context.getString(R.string.vpn_not_authorized_error));
final VpnService service;
if (!vpnService.isDone())
startVpnService();
try {
service = vpnService.get(2, TimeUnit.SECONDS);
} catch (final TimeoutException e) {
throw new Exception(context.getString(R.string.vpn_start_error), e);
}
if (currentTunnelHandle != -1) {
Log.w(TAG, "Tunnel already up");
return;
}
// Build config
final String goConfig = config.toWgUserspaceString();
// Create the vpn tunnel with android API
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 InetNetwork addr : config.getInterface().getAddresses())
builder.addAddress(addr.getAddress(), addr.getMask());
for (final InetAddress addr : config.getInterface().getDnsServers())
builder.addDnsServer(addr.getHostAddress());
for (final Peer peer : config.getPeers()) {
for (final InetNetwork addr : peer.getAllowedIps())
builder.addRoute(addr.getAddress(), addr.getMask());
}
builder.setMtu(config.getInterface().getMtu().orElse(1280));
builder.setBlocking(true);
try (final ParcelFileDescriptor tun = builder.establish()) {
if (tun == null)
throw new Exception(context.getString(R.string.tun_create_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));
currentTunnel = tunnel;
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;
}
wgTurnOff(currentTunnelHandle);
currentTunnel = null;
currentTunnelHandle = -1;
}
}
private void startVpnService() {
Log.d(TAG, "Requesting to start VpnService");
context.startService(new Intent(context, VpnService.class));
}
public static class VpnService extends android.net.VpnService {
public Builder getBuilder() {
return new Builder();
}
@Override
public void onCreate() {
vpnService.complete(this);
super.onCreate();
}
@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);
}
});
vpnService = vpnService.newIncompleteFuture();
super.onDestroy();
}
@Override
public int onStartCommand(@Nullable final Intent intent, final int flags, final int startId) {
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);
}
return super.onStartCommand(intent, flags, startId);
}
}
}
@@ -0,0 +1,133 @@
/*
* 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 java.io.File;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
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) {
return new Statistics();
}
@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,15 +1,19 @@
/*
* Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package org.amnezia.awg.configStore
import org.amnezia.awg.config.Config
package com.wireguard.android.configStore;
import com.wireguard.config.Config;
import java.util.Set;
/**
* Interface for persistent storage providers for AmneziaWG configurations.
* Interface for persistent storage providers for WireGuard configurations.
*/
interface ConfigStore {
public interface ConfigStore {
/**
* Create a persistent tunnel, which must have a unique name within the persistent storage
* medium.
@@ -18,51 +22,46 @@ interface ConfigStore {
* @param config Configuration for the new tunnel.
* @return The configuration that was actually saved to persistent storage.
*/
@Throws(Exception::class)
fun create(name: String, config: Config): Config
Config create(final String name, final Config config) throws Exception;
/**
* Delete a persistent tunnel.
*
* @param name The name of the tunnel to delete.
*/
@Throws(Exception::class)
fun delete(name: String)
void delete(final String name) throws Exception;
/**
* Enumerate the names of tunnels present in persistent storage.
*
* @return The set of present tunnel names.
*/
fun enumerate(): Set<String>
Set<String> enumerate();
/**
* Load the configuration for the tunnel given by `name`.
* Load the configuration for the tunnel given by {@code name}.
*
* @param name The identifier for the configuration in persistent storage (i.e. the name of the
* tunnel).
* tunnel).
* @return An in-memory representation of the configuration loaded from persistent storage.
*/
@Throws(Exception::class)
fun load(name: String): Config
Config load(final String name) throws Exception;
/**
* Rename the configuration for the tunnel given by `name`.
* Rename the configuration for the tunnel given by {@code name}.
*
* @param name The identifier for the existing configuration in persistent storage.
* @param replacement The new identifier for the configuration in persistent storage.
*/
@Throws(Exception::class)
fun rename(name: String, replacement: String)
void rename(String name, String replacement) throws Exception;
/**
* Save the configuration for an existing tunnel given by `name`.
* Save the configuration for an existing tunnel given by {@code name}.
*
* @param name The identifier for the configuration in persistent storage (i.e. the name of
* the tunnel).
* the tunnel).
* @param config An updated configuration object for the tunnel.
* @return The configuration that was actually saved to persistent storage.
*/
@Throws(Exception::class)
fun save(name: String, config: Config): Config
Config save(final String name, final Config config) throws Exception;
}
@@ -0,0 +1,103 @@
/*
* 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;
}
}
@@ -0,0 +1,148 @@
/*
* 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) : "");
}
}
@@ -0,0 +1,140 @@
/*
* 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);
}
}
}
}
@@ -0,0 +1,159 @@
/*
* 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;
}
}
}
@@ -0,0 +1,133 @@
/*
* 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);
}
}
@@ -0,0 +1,126 @@
/*
* 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);
});
}
}
@@ -0,0 +1,118 @@
/*
* 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);
}
}
}
@@ -0,0 +1,75 @@
/*
* 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 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.model.Tunnel;
/**
* Fragment that shows details about a specific tunnel.
*/
public class TunnelDetailFragment extends BaseFragment {
@Nullable private TunnelDetailFragmentBinding binding;
@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 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);
}
@Override
public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
if (binding == null) {
return;
}
binding.setFragment(this);
onSelectedTunnelChanged(null, getSelectedTunnel());
super.onViewStateRestored(savedInstanceState);
}
}
@@ -0,0 +1,262 @@
/*
* 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);
}
}
@@ -0,0 +1,479 @@
/*
* 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));
}
}
}
}
@@ -0,0 +1,54 @@
/*
* 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);
}
}
@@ -0,0 +1,158 @@
/*
* 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 androidx.annotation.Nullable;
import com.wireguard.android.BR;
import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.config.Config;
import com.wireguard.util.Keyed;
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() {
// FIXME: Check age of statistics.
if (statistics == null)
TunnelManager.getTunnelStatistics(this).whenComplete(ExceptionLoggers.E);
return statistics;
}
public CompletionStage<Statistics> getStatisticsAsync() {
// FIXME: Check age of statistics.
if (statistics == null)
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 {
}
}
@@ -0,0 +1,300 @@
/*
* 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);
});
}
}
}
@@ -0,0 +1,44 @@
/*
* Copyright © 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 android.util.AttributeSet;
import com.wireguard.android.Application;
import com.wireguard.android.BuildConfig;
import com.wireguard.android.R;
import java.util.Locale;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
public class DonatePreference extends Preference {
public DonatePreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
@Override
public CharSequence getSummary() { return getContext().getString(R.string.donate_summary); }
@Override
public CharSequence getTitle() { return getContext().getString(R.string.donate_title); }
@Override
protected void onClick() {
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://www.wireguard.com/donations/"));
try {
getContext().startActivity(intent);
} catch (final ActivityNotFoundException ignored) {
}
}
}
@@ -0,0 +1,112 @@
/*
* 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();
}
});
}
}
@@ -0,0 +1,91 @@
/*
* 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;
}
}
}
@@ -0,0 +1,102 @@
/*
* 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;
}
}
}
@@ -0,0 +1,60 @@
/*
* 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) {
}
}
}
@@ -0,0 +1,119 @@
/*
* 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();
}
});
}
}
@@ -0,0 +1,63 @@
/*
* 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;
}
}
@@ -0,0 +1,37 @@
/*
* 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();
}
}
@@ -0,0 +1,96 @@
/*
* 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));
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 {
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);
}
}
}
@@ -0,0 +1,130 @@
/*
* 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;
}
}
@@ -0,0 +1,36 @@
/*
* 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");
}
}
@@ -0,0 +1,27 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.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;
}
}
@@ -0,0 +1,186 @@
/*
* Copyright © 2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.content.Context;
import android.system.OsConstants;
import android.util.Base64;
import com.wireguard.android.Application;
import com.wireguard.android.BuildConfig;
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 java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
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;
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 final File moduleDir;
private final File tmpDir;
public ModuleLoader(final Context context) {
moduleDir = new File(context.getCacheDir(), "kmod");
tmpDir = new File(context.getCacheDir(), "tmp");
}
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() {
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 {
final List<String> output = new ArrayList<>();
Application.getRootShell().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));
final String userAgent = String.format("WireGuard/%s (Android)", BuildConfig.VERSION_NAME); //TODO: expand a bit
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 */];
int len;
try (final InputStream inputStream = connection.getInputStream()) {
len = inputStream.read(input);
}
if (len <= 0)
throw new IOException("Hash list was empty");
final Map<String, Sha256Digest> modules = verifySignedHashes(new String(input, 0, len, StandardCharsets.UTF_8));
if (modules == null)
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", userAgent);
connection.connect();
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK)
throw new IOException("Module file could not be found, despite being on hash list");
tmpDir.mkdirs();
moduleDir.mkdir();
File tempFile = null;
try {
tempFile = File.createTempFile("UNVERIFIED-", null, tmpDir);
MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (final InputStream inputStream = connection.getInputStream();
final OutputStream outputStream = new FileOutputStream(tempFile)) {
int total = 0;
while ((len = inputStream.read(input)) > 0) {
total += len;
if (total > 1024 * 1024 * 15 /* 15 MiB */)
throw new IOException("File too big");
outputStream.write(input, 0, len);
digest.update(input, 0, len);
}
}
if (!Arrays.equals(digest.digest(), modules.get(moduleName).bytes))
throw new IOException("Incorrect file hash");
if (!tempFile.renameTo(new File(moduleDir, moduleName)))
throw new IOException("Unable to rename to final destination");
} finally {
if (tempFile != null)
tempFile.delete();
}
return OsConstants.EXIT_SUCCESS;
}
}
@@ -0,0 +1,109 @@
/*
* 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);
}
}
@@ -0,0 +1,19 @@
/*
* 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> {
}
@@ -0,0 +1,198 @@
/*
* 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();
}
}
}
@@ -0,0 +1,17 @@
/*
* 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,18 +1,20 @@
/*
* Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package org.amnezia.awg.util;
package com.wireguard.android.util;
import android.content.Context;
import androidx.annotation.Nullable;
import android.util.Log;
import org.amnezia.awg.util.RootShell.RootShellException.Reason;
import org.amnezia.awg.util.NonNullForAll;
import com.wireguard.android.BuildConfig;
import com.wireguard.android.R;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
@@ -20,17 +22,16 @@ 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 = "AmneziaWG/RootShell";
private static final String TAG = "WireGuard/" + RootShell.class.getSimpleName();
private final Context context;
private final String deviceNotRootedMessage;
private final File localBinaryDir;
private final File localTemporaryDir;
private final Object lock = new Object();
@@ -41,13 +42,12 @@ 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");
final String packageName = context.getPackageName();
if (packageName.contains("'"))
throw new RuntimeException("Impossibly invalid package name contains a single quote");
preamble = String.format("export CALLING_PACKAGE='%s' PATH=\"%s:$PATH\" TMPDIR='%s'; magisk --sqlite \"UPDATE policies SET notification=0, logging=0 WHERE uid=%d\" >/dev/null 2>&1; id -u\n",
packageName, localBinaryDir, localTemporaryDir, android.os.Process.myUid());
preamble = String.format("export CALLING_PACKAGE=%s PATH=\"%s:$PATH\" TMPDIR='%s'; id -u\n",
BuildConfig.APPLICATION_ID, localBinaryDir, localTemporaryDir);
this.context = context;
}
private static boolean isExecutableInPath(final String name) {
@@ -83,7 +83,7 @@ public class RootShell {
* @return The exit value of the command.
*/
public int run(@Nullable final Collection<String> output, final String command)
throws IOException, RootShellException {
throws IOException, NoRootException {
synchronized (lock) {
/* Start inside synchronized block to prevent a concurrent call to stop(). */
start();
@@ -122,24 +122,24 @@ public class RootShell {
}
}
if (markersSeen != 4)
throw new RootShellException(Reason.SHELL_MARKER_COUNT_ERROR, markersSeen);
throw new IOException(context.getString(R.string.shell_marker_count_error, markersSeen));
if (errnoStdout != errnoStderr)
throw new RootShellException(Reason.SHELL_EXIT_STATUS_READ_ERROR);
throw new IOException(context.getString(R.string.shell_exit_status_read_error));
Log.v(TAG, "exit: " + errnoStdout);
return errnoStdout;
}
}
public void start() throws IOException, RootShellException {
public void start() throws IOException, NoRootException {
if (!isExecutableInPath(SU))
throw new RootShellException(Reason.NO_ROOT_ACCESS);
throw new NoRootException(deviceNotRootedMessage);
synchronized (lock) {
if (isRunning())
return;
if (!localBinaryDir.isDirectory() && !localBinaryDir.mkdirs())
throw new RootShellException(Reason.CREATE_BIN_DIR_ERROR);
throw new FileNotFoundException(context.getString(R.string.create_bin_dir_error));
if (!localTemporaryDir.isDirectory() && !localTemporaryDir.mkdirs())
throw new RootShellException(Reason.CREATE_TEMP_DIR_ERROR);
throw new FileNotFoundException(context.getString(R.string.create_temp_dir_error));
try {
final ProcessBuilder builder = new ProcessBuilder().command(SU);
builder.environment().put("LC_ALL", "C");
@@ -147,9 +147,7 @@ public class RootShell {
process = builder.start();
} catch (final IOException e) {
// A failure at this stage means the device isn't rooted.
final RootShellException rse = new RootShellException(Reason.NO_ROOT_ACCESS);
rse.initCause(e);
throw rse;
throw new NoRootException(deviceNotRootedMessage, e);
}
stdin = new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8);
stdout = new BufferedReader(new InputStreamReader(process.getInputStream(),
@@ -162,18 +160,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 RootShellException(Reason.NO_ROOT_ACCESS);
throw new NoRootException(deviceNotRootedMessage);
}
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 RootShellException(Reason.NO_ROOT_ACCESS);
throw new NoRootException(deviceNotRootedMessage);
}
throw new RootShellException(Reason.SHELL_START_ERROR, process.exitValue());
throw new IOException(context.getString(R.string.shell_start_error, process.exitValue()));
}
} catch (final IOException | RootShellException e) {
} catch (final IOException | NoRootException e) {
stop();
throw e;
}
@@ -189,34 +187,13 @@ public class RootShell {
}
}
public static class RootShellException extends Exception {
private final Object[] format;
private final Reason reason;
public RootShellException(final Reason reason, final Object... format) {
this.reason = reason;
this.format = format;
public static class NoRootException extends Exception {
public NoRootException(final String message, final Throwable cause) {
super(message, cause);
}
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
public NoRootException(final String message) {
super(message);
}
}
}
@@ -1,16 +1,14 @@
/*
* Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package org.amnezia.awg.util;
package com.wireguard.android.util;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import org.amnezia.awg.util.NonNullForAll;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -21,13 +19,8 @@ import java.util.HashSet;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
@NonNullForAll
@RestrictTo(Scope.LIBRARY_GROUP)
public final class SharedLibraryLoader {
private static final String TAG = "AmneziaWG/SharedLibraryLoader";
private static final String TAG = "WireGuard/" + SharedLibraryLoader.class.getSimpleName();
private SharedLibraryLoader() {
}
@@ -41,21 +34,25 @@ public final class SharedLibraryLoader {
for (final String abi : Build.SUPPORTED_ABIS) {
for (final String apk : apks) {
try (final ZipFile zipFile = new ZipFile(new File(apk), ZipFile.OPEN_READ)) {
final String mappedLibName = System.mapLibraryName(libName);
final String libZipPath = "lib" + File.separatorChar + abi + File.separatorChar + mappedLibName;
final ZipEntry zipEntry = zipFile.getEntry(libZipPath);
if (zipEntry == null)
continue;
Log.d(TAG, "Extracting apk:/" + libZipPath + " to " + destination.getAbsolutePath());
try (final FileOutputStream out = new FileOutputStream(destination);
final InputStream in = zipFile.getInputStream(zipEntry)) {
int len;
final byte[] buffer = new byte[1024 * 32];
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
out.getFD().sync();
final ZipFile zipFile;
try {
zipFile = new ZipFile(new File(apk), ZipFile.OPEN_READ);
} catch (final IOException e) {
throw new RuntimeException(e);
}
final String mappedLibName = System.mapLibraryName(libName);
final byte[] buffer = new byte[1024 * 32];
final String libZipPath = "lib" + File.separatorChar + abi + File.separatorChar + mappedLibName;
final ZipEntry zipEntry = zipFile.getEntry(libZipPath);
if (zipEntry == null)
continue;
Log.d(TAG, "Extracting apk:/" + libZipPath + " to " + destination.getAbsolutePath());
try (final FileOutputStream out = new FileOutputStream(destination);
final InputStream in = zipFile.getInputStream(zipEntry)) {
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
}
return true;
@@ -1,16 +1,19 @@
/*
* Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package org.amnezia.awg.util;
package com.wireguard.android.util;
import android.content.Context;
import androidx.annotation.Nullable;
import android.system.OsConstants;
import android.util.Log;
import org.amnezia.awg.util.RootShell.RootShellException;
import org.amnezia.awg.util.NonNullForAll;
import com.wireguard.android.Application;
import com.wireguard.android.BuildConfig;
import com.wireguard.android.R;
import com.wireguard.android.util.RootShell.NoRootException;
import java.io.File;
import java.io.FileNotFoundException;
@@ -18,40 +21,33 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
/**
* Helper to install AmneziaWG tools to the system partition.
* Helper to install WireGuard tools to the system partition.
*/
@NonNullForAll
public final class ToolsInstaller {
public static final int ERROR = 0x0;
public static final int MAGISK = 0x4;
public static final int NO = 0x2;
public static final int SYSTEM = 0x8;
public static final int YES = 0x1;
private static final String[] EXECUTABLES = {"awg", "awg-quick"};
private static final String[] EXECUTABLES = {"wg", "wg-quick"};
private static final File[] INSTALL_DIRS = {
new File("/system/xbin"),
new File("/system/bin"),
};
@Nullable private static final File INSTALL_DIR = getInstallDir();
private static final String TAG = "AmneziaWG/ToolsInstaller";
private static final String TAG = "WireGuard/" + ToolsInstaller.class.getSimpleName();
private final Context context;
private final File localBinaryDir;
private final Object lock = new Object();
private final RootShell rootShell;
@Nullable private Boolean areToolsAvailable;
@Nullable private Boolean installAsMagiskModule;
public ToolsInstaller(final Context context, final RootShell rootShell) {
public ToolsInstaller(final Context context) {
localBinaryDir = new File(context.getCodeCacheDir(), "bin");
this.context = context;
this.rootShell = rootShell;
}
@Nullable
@@ -67,7 +63,7 @@ public final class ToolsInstaller {
return null;
}
public int areInstalled() throws RootShellException {
public int areInstalled() throws NoRootException {
if (INSTALL_DIR == null)
return ERROR;
final StringBuilder script = new StringBuilder();
@@ -78,17 +74,13 @@ public final class ToolsInstaller {
}
script.append("exit ").append(OsConstants.EALREADY).append(';');
try {
final int ret = rootShell.run(null, script.toString());
final int ret = Application.getRootShell().run(null, script.toString());
if (ret == OsConstants.EALREADY)
return willInstallAsMagiskModule() ? YES | MAGISK : YES | SYSTEM;
else
return willInstallAsMagiskModule() ? NO | MAGISK : NO | SYSTEM;
} catch (final IOException ignored) {
return ERROR;
} catch (final RootShellException e) {
if (e.isIORelated())
return ERROR;
throw e;
}
}
@@ -100,72 +92,43 @@ public final class ToolsInstaller {
"Tools were already extracted into our private binary dir");
areToolsAvailable = true;
} catch (final IOException e) {
Log.e(TAG, "The awg and awg-quick tools are not available", e);
Log.e(TAG, "The wg and wg-quick tools are not available", e);
areToolsAvailable = false;
}
}
if (!areToolsAvailable)
throw new FileNotFoundException("Required tools unavailable");
throw new FileNotFoundException(
context.getString(R.string.tools_unavailable_error));
}
}
public boolean extract() throws IOException {
localBinaryDir.mkdirs();
final File[] files = new File[EXECUTABLES.length];
final File[] tempFiles = new File[EXECUTABLES.length];
boolean allExist = true;
for (int i = 0; i < files.length; ++i) {
files[i] = new File(localBinaryDir, EXECUTABLES[i]);
tempFiles[i] = new File(localBinaryDir, EXECUTABLES[i] + ".tmp");
allExist &= files[i].exists();
}
if (allExist)
return false;
for (int i = 0; i < files.length; ++i) {
if (!SharedLibraryLoader.extractLibrary(context, EXECUTABLES[i], tempFiles[i]))
throw new FileNotFoundException("Unable to find " + EXECUTABLES[i]);
if (!tempFiles[i].setExecutable(true, false))
throw new IOException("Unable to mark " + tempFiles[i].getAbsolutePath() + " as executable");
if (!tempFiles[i].renameTo(files[i]))
throw new IOException("Unable to rename " + tempFiles[i].getAbsolutePath() + " to " + files[i].getAbsolutePath());
}
return true;
}
@RestrictTo(Scope.LIBRARY_GROUP)
public int install() throws RootShellException, IOException {
if (!context.getPackageName().startsWith("org.amnezia."))
throw new SecurityException("The tools may only be installed system-wide from the main AmneziaWG app.");
public int install() throws NoRootException, IOException {
return willInstallAsMagiskModule() ? installMagisk() : installSystem();
}
private int installMagisk() throws RootShellException, IOException {
private int installMagisk() throws NoRootException, IOException {
extract();
final StringBuilder script = new StringBuilder("set -ex; ");
script.append("trap 'rm -rf /data/adb/modules/amneziawg' INT TERM EXIT; ");
script.append(String.format("rm -rf /data/adb/modules/amneziawg/; mkdir -p /data/adb/modules/amneziawg%s; ", INSTALL_DIR));
script.append("printf 'id=amneziawg\nname=AmneziaWG Command Line Tools\nversion=1.0\nversionCode=1\nauthor=amnezia\ndescription=Command line tools for AmneziaWG\nminMagisk=1500\n' > /data/adb/modules/amneziawg/module.prop; ");
script.append("touch /data/adb/modules/amneziawg/auto_mount; ");
script.append("trap 'rm -rf /sbin/.magisk/img/wireguard' INT TERM EXIT; ");
script.append(String.format("rm -rf /sbin/.magisk/img/wireguard/; mkdir -p /sbin/.magisk/img/wireguard%s; ", INSTALL_DIR));
script.append(String.format("printf 'name=WireGuard Command Line Tools\nversion=%s\nversionCode=%s\nauthor=zx2c4\ndescription=Command line tools for WireGuard\nminMagisk=1500\n' > /sbin/.magisk/img/wireguard/module.prop; ", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
script.append("touch /sbin/.magisk/img/wireguard/auto_mount; ");
for (final String name : EXECUTABLES) {
final File destination = new File("/data/adb/modules/amneziawg" + INSTALL_DIR, name);
final File destination = new File("/sbin/.magisk/img/wireguard" + INSTALL_DIR, name);
script.append(String.format("cp '%s' '%s'; chmod 755 '%s'; chcon 'u:object_r:system_file:s0' '%s' || true; ",
new File(localBinaryDir, name), destination, destination, destination));
}
script.append("trap - INT TERM EXIT;");
try {
return rootShell.run(null, script.toString()) == 0 ? YES | MAGISK : ERROR;
return Application.getRootShell().run(null, script.toString()) == 0 ? YES | MAGISK : ERROR;
} catch (final IOException ignored) {
return ERROR;
} catch (final RootShellException e) {
if (e.isIORelated())
return ERROR;
throw e;
}
}
private int installSystem() throws RootShellException, IOException {
private int installSystem() throws NoRootException, IOException {
if (INSTALL_DIR == null)
return OsConstants.ENOENT;
extract();
@@ -177,21 +140,36 @@ public final class ToolsInstaller {
new File(localBinaryDir, name), destination, destination, destination));
}
try {
return rootShell.run(null, script.toString()) == 0 ? YES | SYSTEM : ERROR;
return Application.getRootShell().run(null, script.toString()) == 0 ? YES | SYSTEM : ERROR;
} catch (final IOException ignored) {
return ERROR;
} catch (final RootShellException e) {
if (e.isIORelated())
return ERROR;
throw e;
}
}
public boolean extract() throws IOException {
localBinaryDir.mkdirs();
final File files[] = new File[EXECUTABLES.length];
boolean allExist = true;
for (int i = 0; i < files.length; ++i) {
files[i] = new File(localBinaryDir, EXECUTABLES[i]);
allExist &= files[i].exists();
}
if (allExist)
return false;
for (int i = 0; i < files.length; ++i) {
if (!SharedLibraryLoader.extractLibrary(context, EXECUTABLES[i], files[i]))
throw new FileNotFoundException("Unable to find " + EXECUTABLES[i]);
if (!files[i].setExecutable(true, false))
throw new IOException("Unable to mark " + files[i].getAbsolutePath() + " as executable");
}
return true;
}
private boolean willInstallAsMagiskModule() {
synchronized (lock) {
if (installAsMagiskModule == null) {
try {
installAsMagiskModule = rootShell.run(null, "[ -d /data/adb/modules -a ! -f /cache/.disable_magisk ]") == OsConstants.EXIT_SUCCESS;
installAsMagiskModule = Application.getRootShell().run(null, "[ -d /sbin/.magisk/mirror -a -d /sbin/.magisk/img -a ! -f /cache/.disable_magisk ]") == OsConstants.EXIT_SUCCESS;
} catch (final Exception ignored) {
installAsMagiskModule = false;
}
@@ -0,0 +1,93 @@
/*
* 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];
}
}
}
@@ -0,0 +1,190 @@
/*
* 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];
}
}
}
@@ -0,0 +1,380 @@
/*
* 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];
}
}
}
@@ -0,0 +1,54 @@
/*
* 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;
}
}
@@ -0,0 +1,59 @@
/*
* 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);
}
}
@@ -0,0 +1,53 @@
/*
* 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;
}
}
@@ -0,0 +1,216 @@
/*
* 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.annotation.TargetApi;
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 android.util.FloatProperty;
@TargetApi(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();
}
@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;
}
}
@@ -0,0 +1,59 @@
/*
* 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);
}
}
@@ -0,0 +1,66 @@
/*
* 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());
}
}
@@ -0,0 +1,629 @@
/*
* 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.equals("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;
}
}
}
}
@@ -0,0 +1,27 @@
/*
* 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()));
}
}
@@ -0,0 +1,58 @@
/*
* 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);
}
}
@@ -0,0 +1,78 @@
/*
* 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,18 +1,17 @@
/*
* Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package org.amnezia.awg.config;
package com.wireguard.config;
import org.amnezia.awg.util.NonNullForAll;
import android.text.TextUtils;
import java.util.Iterator;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@NonNullForAll
import java9.util.Optional;
public final class Attribute {
private static final Pattern LINE_PATTERN = Pattern.compile("(\\w+)\\s*=\\s*([^\\s#][^#]*)");
private static final Pattern LIST_SEPARATOR = Pattern.compile("\\s*,\\s*");
@@ -26,17 +25,7 @@ public final class Attribute {
}
public static String join(final Iterable<?> values) {
final Iterator<?> it = values.iterator();
if (!it.hasNext()) {
return "";
}
final StringBuilder sb = new StringBuilder();
sb.append(it.next());
while (it.hasNext()) {
sb.append(", ");
sb.append(it.next());
}
return sb.toString();
return TextUtils.join(", ", values);
}
public static Optional<Attribute> parse(final CharSequence line) {
@@ -1,16 +1,14 @@
/*
* Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package org.amnezia.awg.config;
import org.amnezia.awg.crypto.KeyFormatException;
import org.amnezia.awg.util.NonNullForAll;
package com.wireguard.config;
import androidx.annotation.Nullable;
@NonNullForAll
import com.wireguard.crypto.KeyFormatException;
public class BadConfigException extends Exception {
private final Location location;
private final Reason reason;
@@ -72,29 +70,12 @@ public class BadConfigException extends Exception {
DNS("DNS"),
ENDPOINT("Endpoint"),
EXCLUDED_APPLICATIONS("ExcludedApplications"),
INCLUDED_APPLICATIONS("IncludedApplications"),
LISTEN_PORT("ListenPort"),
MTU("MTU"),
PERSISTENT_KEEPALIVE("PersistentKeepalive"),
PRE_SHARED_KEY("PresharedKey"),
PRIVATE_KEY("PrivateKey"),
PUBLIC_KEY("PublicKey"),
JUNK_PACKET_COUNT("JunkPacketCount"),
JUNK_PACKET_MIN_SIZE("JunkPacketMinSize"),
JUNK_PACKET_MAX_SIZE("JunkPacketMaxSize"),
INIT_PACKET_JUNK_SIZE("InitPacketJunkSize"),
RESPONSE_PACKET_JUNK_SIZE("ResponsePacketJunkSize"),
COOKIE_REPLY_PACKET_JUNK_SIZE("CookieReplyPacketJunkSize"),
TRANSPORT_PACKET_JUNK_SIZE("TransportPacketJunkSize"),
INIT_PACKET_MAGIC_HEADER("InitPacketMagicHeader"),
RESPONSE_PACKET_MAGIC_HEADER("ResponsePacketMagicHeader"),
UNDERLOAD_PACKET_MAGIC_HEADER("UnderloadPacketMagicHeader"),
TRANSPORT_PACKET_MAGIC_HEADER("TransportPacketMagicHeader"),
SPECIAL_JUNK_I1("SpecialJunkI1"),
SPECIAL_JUNK_I2("SpecialJunkI2"),
SPECIAL_JUNK_I3("SpecialJunkI3"),
SPECIAL_JUNK_I4("SpecialJunkI4"),
SPECIAL_JUNK_I5("SpecialJunkI5");
PUBLIC_KEY("PublicKey");
private final String name;
@@ -113,6 +94,7 @@ public class BadConfigException extends Exception {
INVALID_VALUE,
MISSING_ATTRIBUTE,
MISSING_SECTION,
MISSING_VALUE,
SYNTAX_ERROR,
UNKNOWN_ATTRIBUTE,
UNKNOWN_SECTION
@@ -1,14 +1,15 @@
/*
* Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package org.amnezia.awg.config;
package com.wireguard.config;
import org.amnezia.awg.config.BadConfigException.Location;
import org.amnezia.awg.config.BadConfigException.Reason;
import org.amnezia.awg.config.BadConfigException.Section;
import org.amnezia.awg.util.NonNullForAll;
import androidx.annotation.Nullable;
import com.wireguard.config.BadConfigException.Location;
import com.wireguard.config.BadConfigException.Reason;
import com.wireguard.config.BadConfigException.Section;
import java.io.BufferedReader;
import java.io.IOException;
@@ -17,18 +18,17 @@ import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import androidx.annotation.Nullable;
import java.util.Set;
/**
* Represents the contents of a awg-quick configuration file, made up of one or more "Interface"
* Represents the contents of a wg-quick configuration file, made up of one or more "Interface"
* sections (combined together), and zero or more "Peer" sections (treated individually).
* <p>
* Instances of this class are immutable.
*/
@NonNullForAll
public final class Config {
private final Interface interfaze;
private final List<Peer> peers;
@@ -44,7 +44,7 @@ public final class Config {
* {@link BadConfigException} if the input is not well-formed or contains data that cannot
* be parsed.
*
* @param stream a stream of UTF-8 text that is interpreted as an AmneziaWG configuration
* @param stream a stream of UTF-8 text that is interpreted as a WireGuard configuration
* @return a {@code Config} instance representing the supplied configuration
*/
public static Config parse(final InputStream stream)
@@ -57,7 +57,7 @@ public final class Config {
* {@link BadConfigException} if the input is not well-formed or contains data that cannot
* be parsed.
*
* @param reader a BufferedReader of UTF-8 text that is interpreted as an AmneziaWG configuration
* @param reader a BufferedReader of UTF-8 text that is interpreted as a WireGuard configuration
* @return a {@code Config} instance representing the supplied configuration
*/
public static Config parse(final BufferedReader reader)
@@ -67,7 +67,6 @@ public final class Config {
final Collection<String> peerLines = new ArrayList<>();
boolean inInterfaceSection = false;
boolean inPeerSection = false;
boolean seenInterfaceSection = false;
@Nullable String line;
while ((line = reader.readLine()) != null) {
final int commentIndex = line.indexOf('#');
@@ -85,7 +84,6 @@ public final class Config {
if ("[Interface]".equalsIgnoreCase(line)) {
inInterfaceSection = true;
inPeerSection = false;
seenInterfaceSection = true;
} else if ("[Peer]".equalsIgnoreCase(line)) {
inInterfaceSection = false;
inPeerSection = true;
@@ -104,7 +102,7 @@ public final class Config {
}
if (inPeerSection)
builder.parsePeer(peerLines);
if (!seenInterfaceSection)
else if (!inInterfaceSection)
throw new BadConfigException(Section.CONFIG, Location.TOP_LEVEL,
Reason.MISSING_SECTION, null);
// Combine all [Interface] sections in the file.
@@ -155,37 +153,37 @@ public final class Config {
}
/**
* Converts the {@code Config} into a string suitable for use as a {@code awg-quick}
* Converts the {@code Config} into a string suitable for use as a {@code wg-quick}
* configuration file.
*
* @return the {@code Config} represented as one [Interface] and zero or more [Peer] sections
*/
public String toAwgQuickString() {
public String toWgQuickString() {
final StringBuilder sb = new StringBuilder();
sb.append("[Interface]\n").append(interfaze.toAwgQuickString());
sb.append("[Interface]\n").append(interfaze.toWgQuickString());
for (final Peer peer : peers)
sb.append("\n[Peer]\n").append(peer.toAwgQuickString());
sb.append("\n[Peer]\n").append(peer.toWgQuickString());
return sb.toString();
}
/**
* Serializes the {@code Config} for use with the AmneziaWG cross-platform userspace API.
* Serializes the {@code Config} for use with the WireGuard cross-platform userspace API.
*
* @return the {@code Config} represented as a series of "key=value" lines
*/
public String toAwgUserspaceString() {
public String toWgUserspaceString() {
final StringBuilder sb = new StringBuilder();
sb.append(interfaze.toAwgUserspaceString());
sb.append(interfaze.toWgUserspaceString());
sb.append("replace_peers=true\n");
for (final Peer peer : peers)
sb.append(peer.toAwgUserspaceString());
sb.append(peer.toWgUserspaceString());
return sb.toString();
}
@SuppressWarnings("UnusedReturnValue")
public static final class Builder {
// Defaults to an empty set.
private final ArrayList<Peer> peers = new ArrayList<>();
private final Set<Peer> peers = new LinkedHashSet<>();
// No default; must be provided before building.
@Nullable private Interface interfaze;
@@ -0,0 +1,64 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.config;
import android.os.Build;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
/**
* Utility methods for creating instances of {@link InetAddress}.
*/
public final class InetAddresses {
private static Method PARSER_METHOD;
private static Method getParserMethod() {
if (PARSER_METHOD != null)
return PARSER_METHOD;
try {
// This method is only present on Android.
// noinspection JavaReflectionMemberAccess
PARSER_METHOD = InetAddress.class.getMethod("parseNumericAddress", String.class);
} catch (final NoSuchMethodException e) {
throw new RuntimeException(e);
}
return PARSER_METHOD;
}
private InetAddresses() {
// Prevent instantiation.
}
/**
* Parses a numeric IPv4 or IPv6 address without performing any DNS lookups.
*
* @param address a string representing the IP address
* @return an instance of {@link Inet4Address} or {@link Inet6Address}, as appropriate
*/
public static InetAddress parse(final String address) throws ParseException {
if (address.isEmpty())
throw new ParseException(InetAddress.class, address, "Empty address");
try {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
return (InetAddress) getParserMethod().invoke(null, address);
else
return android.net.InetAddresses.parseNumericAddress(address);
} catch (final IllegalAccessException | InvocationTargetException e) {
final Throwable cause = e.getCause();
// Re-throw parsing exceptions with the original type, as callers might try to catch
// them. On the other hand, callers cannot be expected to handle reflection failures.
if (cause instanceof IllegalArgumentException)
throw new ParseException(InetAddress.class, address, cause);
throw new RuntimeException(e);
} catch (final IllegalArgumentException e) {
throw new ParseException(InetAddress.class, address, e);
}
}
}
@@ -1,31 +1,30 @@
/*
* Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package org.amnezia.awg.config;
package com.wireguard.config;
import org.amnezia.awg.util.NonNullForAll;
import androidx.annotation.Nullable;
import org.threeten.bp.Duration;
import org.threeten.bp.Instant;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.regex.Pattern;
import androidx.annotation.Nullable;
import java9.util.Optional;
/**
* An external endpoint (host and port) used to connect to an AmneziaWG {@link Peer}.
* An external endpoint (host and port) used to connect to a WireGuard {@link Peer}.
* <p>
* Instances of this class are externally immutable.
*/
@NonNullForAll
public final class InetEndpoint {
private static final Pattern BARE_IPV6 = Pattern.compile("^[^\\[\\]]*:[^\\[\\]]*");
private static final Pattern FORBIDDEN_CHARACTERS = Pattern.compile("[/?#]");
@@ -48,9 +47,9 @@ public final class InetEndpoint {
throw new ParseException(InetEndpoint.class, endpoint, "Forbidden characters");
final URI uri;
try {
uri = new URI("awg://" + endpoint);
uri = new URI("wg://" + endpoint);
} catch (final URISyntaxException e) {
throw new ParseException(InetEndpoint.class, endpoint, e);
throw new IllegalArgumentException(e);
}
if (uri.getPort() < 0 || uri.getPort() > 65535)
throw new ParseException(InetEndpoint.class, endpoint, "Missing/invalid port number");
@@ -1,11 +1,9 @@
/*
* Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package org.amnezia.awg.config;
import org.amnezia.awg.util.NonNullForAll;
package com.wireguard.config;
import java.net.Inet4Address;
import java.net.InetAddress;
@@ -15,7 +13,6 @@ import java.net.InetAddress;
* <p>
* Instances of this class are immutable.
*/
@NonNullForAll
public final class InetNetwork {
private final InetAddress address;
private final int mask;
@@ -47,7 +44,7 @@ public final class InetNetwork {
final int maxMask = (address instanceof Inet4Address) ? 32 : 128;
if (rawMask > maxMask)
throw new ParseException(InetNetwork.class, maskString, "Invalid network mask");
final int mask = rawMask >= 0 ? rawMask : maxMask;
final int mask = rawMask >= 0 && rawMask <= maxMask ? rawMask : maxMask;
return new InetNetwork(address, mask);
}
@@ -0,0 +1,355 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.config;
import androidx.annotation.Nullable;
import com.wireguard.config.BadConfigException.Location;
import com.wireguard.config.BadConfigException.Reason;
import com.wireguard.config.BadConfigException.Section;
import com.wireguard.crypto.Key;
import com.wireguard.crypto.KeyFormatException;
import com.wireguard.crypto.KeyPair;
import java.net.InetAddress;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java9.util.Lists;
import java9.util.Optional;
import java9.util.stream.Collectors;
import java9.util.stream.StreamSupport;
/**
* Represents the configuration for a WireGuard interface (an [Interface] block). Interfaces must
* have a private key (used to initialize a {@code KeyPair}), and may optionally have several other
* attributes.
* <p>
* Instances of this class are immutable.
*/
public final class Interface {
private static final int MAX_UDP_PORT = 65535;
private static final int MIN_UDP_PORT = 0;
private final Set<InetNetwork> addresses;
private final Set<InetAddress> dnsServers;
private final Set<String> excludedApplications;
private final KeyPair keyPair;
private final Optional<Integer> listenPort;
private final Optional<Integer> mtu;
private Interface(final Builder builder) {
// Defensively copy to ensure immutability even if the Builder is reused.
addresses = Collections.unmodifiableSet(new LinkedHashSet<>(builder.addresses));
dnsServers = Collections.unmodifiableSet(new LinkedHashSet<>(builder.dnsServers));
excludedApplications = Collections.unmodifiableSet(new LinkedHashSet<>(builder.excludedApplications));
keyPair = Objects.requireNonNull(builder.keyPair, "Interfaces must have a private key");
listenPort = builder.listenPort;
mtu = builder.mtu;
}
/**
* Parses an series of "KEY = VALUE" lines into an {@code Interface}. Throws
* {@link ParseException} if the input is not well-formed or contains unknown attributes.
*
* @param lines An iterable sequence of lines, containing at least a private key attribute
* @return An {@code Interface} with all of the attributes from {@code lines} set
*/
public static Interface parse(final Iterable<? extends CharSequence> lines)
throws BadConfigException {
final Builder builder = new Builder();
for (final CharSequence line : lines) {
final Attribute attribute = Attribute.parse(line).orElseThrow(() ->
new BadConfigException(Section.INTERFACE, Location.TOP_LEVEL,
Reason.SYNTAX_ERROR, line));
switch (attribute.getKey().toLowerCase(Locale.ENGLISH)) {
case "address":
builder.parseAddresses(attribute.getValue());
break;
case "dns":
builder.parseDnsServers(attribute.getValue());
break;
case "excludedapplications":
builder.parseExcludedApplications(attribute.getValue());
break;
case "listenport":
builder.parseListenPort(attribute.getValue());
break;
case "mtu":
builder.parseMtu(attribute.getValue());
break;
case "privatekey":
builder.parsePrivateKey(attribute.getValue());
break;
default:
throw new BadConfigException(Section.INTERFACE, Location.TOP_LEVEL,
Reason.UNKNOWN_ATTRIBUTE, attribute.getKey());
}
}
return builder.build();
}
@Override
public boolean equals(final Object obj) {
if (!(obj instanceof Interface))
return false;
final Interface other = (Interface) obj;
return addresses.equals(other.addresses)
&& dnsServers.equals(other.dnsServers)
&& excludedApplications.equals(other.excludedApplications)
&& keyPair.equals(other.keyPair)
&& listenPort.equals(other.listenPort)
&& mtu.equals(other.mtu);
}
/**
* Returns the set of IP addresses assigned to the interface.
*
* @return a set of {@link InetNetwork}s
*/
public Set<InetNetwork> getAddresses() {
// The collection is already immutable.
return addresses;
}
/**
* Returns the set of DNS servers associated with the interface.
*
* @return a set of {@link InetAddress}es
*/
public Set<InetAddress> getDnsServers() {
// The collection is already immutable.
return dnsServers;
}
/**
* Returns the set of applications excluded from using the interface.
*
* @return a set of package names
*/
public Set<String> getExcludedApplications() {
// The collection is already immutable.
return excludedApplications;
}
/**
* Returns the public/private key pair used by the interface.
*
* @return a key pair
*/
public KeyPair getKeyPair() {
return keyPair;
}
/**
* Returns the UDP port number that the WireGuard interface will listen on.
*
* @return a UDP port number, or {@code Optional.empty()} if none is configured
*/
public Optional<Integer> getListenPort() {
return listenPort;
}
/**
* Returns the MTU used for the WireGuard interface.
*
* @return the MTU, or {@code Optional.empty()} if none is configured
*/
public Optional<Integer> getMtu() {
return mtu;
}
@Override
public int hashCode() {
int hash = 1;
hash = 31 * hash + addresses.hashCode();
hash = 31 * hash + dnsServers.hashCode();
hash = 31 * hash + excludedApplications.hashCode();
hash = 31 * hash + keyPair.hashCode();
hash = 31 * hash + listenPort.hashCode();
hash = 31 * hash + mtu.hashCode();
return hash;
}
/**
* Converts the {@code Interface} into a string suitable for debugging purposes. The {@code
* Interface} is identified by its public key and (if set) the port used for its UDP socket.
*
* @return A concise single-line identifier for the {@code Interface}
*/
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("(Interface ");
sb.append(keyPair.getPublicKey().toBase64());
listenPort.ifPresent(lp -> sb.append(" @").append(lp));
sb.append(')');
return sb.toString();
}
/**
* Converts the {@code Interface} into a string suitable for inclusion in a {@code wg-quick}
* configuration file.
*
* @return The {@code Interface} represented as a series of "Key = Value" lines
*/
public String toWgQuickString() {
final StringBuilder sb = new StringBuilder();
if (!addresses.isEmpty())
sb.append("Address = ").append(Attribute.join(addresses)).append('\n');
if (!dnsServers.isEmpty()) {
final List<String> dnsServerStrings = StreamSupport.stream(dnsServers)
.map(InetAddress::getHostAddress)
.collect(Collectors.toUnmodifiableList());
sb.append("DNS = ").append(Attribute.join(dnsServerStrings)).append('\n');
}
if (!excludedApplications.isEmpty())
sb.append("ExcludedApplications = ").append(Attribute.join(excludedApplications)).append('\n');
listenPort.ifPresent(lp -> sb.append("ListenPort = ").append(lp).append('\n'));
mtu.ifPresent(m -> sb.append("MTU = ").append(m).append('\n'));
sb.append("PrivateKey = ").append(keyPair.getPrivateKey().toBase64()).append('\n');
return sb.toString();
}
/**
* Serializes the {@code Interface} for use with the WireGuard cross-platform userspace API.
* Note that not all attributes are included in this representation.
*
* @return the {@code Interface} represented as a series of "KEY=VALUE" lines
*/
public String toWgUserspaceString() {
final StringBuilder sb = new StringBuilder();
sb.append("private_key=").append(keyPair.getPrivateKey().toHex()).append('\n');
listenPort.ifPresent(lp -> sb.append("listen_port=").append(lp).append('\n'));
return sb.toString();
}
@SuppressWarnings("UnusedReturnValue")
public static final class Builder {
// Defaults to an empty set.
private final Set<InetNetwork> addresses = new LinkedHashSet<>();
// Defaults to an empty set.
private final Set<InetAddress> dnsServers = new LinkedHashSet<>();
// Defaults to an empty set.
private final Set<String> excludedApplications = new LinkedHashSet<>();
// No default; must be provided before building.
@Nullable private KeyPair keyPair;
// Defaults to not present.
private Optional<Integer> listenPort = Optional.empty();
// Defaults to not present.
private Optional<Integer> mtu = Optional.empty();
public Builder addAddress(final InetNetwork address) {
addresses.add(address);
return this;
}
public Builder addAddresses(final Collection<InetNetwork> addresses) {
this.addresses.addAll(addresses);
return this;
}
public Builder addDnsServer(final InetAddress dnsServer) {
dnsServers.add(dnsServer);
return this;
}
public Builder addDnsServers(final Collection<? extends InetAddress> dnsServers) {
this.dnsServers.addAll(dnsServers);
return this;
}
public Interface build() throws BadConfigException {
if (keyPair == null)
throw new BadConfigException(Section.INTERFACE, Location.PRIVATE_KEY,
Reason.MISSING_ATTRIBUTE, null);
return new Interface(this);
}
public Builder excludeApplication(final String application) {
excludedApplications.add(application);
return this;
}
public Builder excludeApplications(final Collection<String> applications) {
excludedApplications.addAll(applications);
return this;
}
public Builder parseAddresses(final CharSequence addresses) throws BadConfigException {
try {
for (final String address : Attribute.split(addresses))
addAddress(InetNetwork.parse(address));
return this;
} catch (final ParseException e) {
throw new BadConfigException(Section.INTERFACE, Location.ADDRESS, e);
}
}
public Builder parseDnsServers(final CharSequence dnsServers) throws BadConfigException {
try {
for (final String dnsServer : Attribute.split(dnsServers))
addDnsServer(InetAddresses.parse(dnsServer));
return this;
} catch (final ParseException e) {
throw new BadConfigException(Section.INTERFACE, Location.DNS, e);
}
}
public Builder parseExcludedApplications(final CharSequence apps) {
return excludeApplications(Lists.of(Attribute.split(apps)));
}
public Builder parseListenPort(final String listenPort) throws BadConfigException {
try {
return setListenPort(Integer.parseInt(listenPort));
} catch (final NumberFormatException e) {
throw new BadConfigException(Section.INTERFACE, Location.LISTEN_PORT, listenPort, e);
}
}
public Builder parseMtu(final String mtu) throws BadConfigException {
try {
return setMtu(Integer.parseInt(mtu));
} catch (final NumberFormatException e) {
throw new BadConfigException(Section.INTERFACE, Location.MTU, mtu, e);
}
}
public Builder parsePrivateKey(final String privateKey) throws BadConfigException {
try {
return setKeyPair(new KeyPair(Key.fromBase64(privateKey)));
} catch (final KeyFormatException e) {
throw new BadConfigException(Section.INTERFACE, Location.PRIVATE_KEY, e);
}
}
public Builder setKeyPair(final KeyPair keyPair) {
this.keyPair = keyPair;
return this;
}
public Builder setListenPort(final int listenPort) throws BadConfigException {
if (listenPort < MIN_UDP_PORT || listenPort > MAX_UDP_PORT)
throw new BadConfigException(Section.INTERFACE, Location.LISTEN_PORT,
Reason.INVALID_VALUE, String.valueOf(listenPort));
this.listenPort = listenPort == 0 ? Optional.empty() : Optional.of(listenPort);
return this;
}
public Builder setMtu(final int mtu) throws BadConfigException {
if (mtu < 0)
throw new BadConfigException(Section.INTERFACE, Location.LISTEN_PORT,
Reason.INVALID_VALUE, String.valueOf(mtu));
this.mtu = mtu == 0 ? Optional.empty() : Optional.of(mtu);
return this;
}
}
}
@@ -1,18 +1,14 @@
/*
* Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package org.amnezia.awg.config;
import org.amnezia.awg.util.NonNullForAll;
package com.wireguard.config;
import androidx.annotation.Nullable;
/**
*
*/
@NonNullForAll
public class ParseException extends Exception {
private final Class<?> parsingClass;
private final CharSequence text;
@@ -1,34 +1,33 @@
/*
* Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package org.amnezia.awg.config;
package com.wireguard.config;
import org.amnezia.awg.config.BadConfigException.Location;
import org.amnezia.awg.config.BadConfigException.Reason;
import org.amnezia.awg.config.BadConfigException.Section;
import org.amnezia.awg.crypto.Key;
import org.amnezia.awg.crypto.KeyFormatException;
import org.amnezia.awg.util.NonNullForAll;
import androidx.annotation.Nullable;
import com.wireguard.config.BadConfigException.Location;
import com.wireguard.config.BadConfigException.Reason;
import com.wireguard.config.BadConfigException.Section;
import com.wireguard.crypto.Key;
import com.wireguard.crypto.KeyFormatException;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import androidx.annotation.Nullable;
import java9.util.Optional;
/**
* Represents the configuration for an AmneziaWG peer (a [Peer] block). Peers must have a public key,
* Represents the configuration for a WireGuard peer (a [Peer] block). Peers must have a public key,
* and may optionally have several other attributes.
* <p>
* Instances of this class are immutable.
*/
@NonNullForAll
public final class Peer {
private final Set<InetNetwork> allowedIps;
private final Optional<InetEndpoint> endpoint;
@@ -168,12 +167,12 @@ public final class Peer {
}
/**
* Converts the {@code Peer} into a string suitable for inclusion in a {@code awg-quick}
* Converts the {@code Peer} into a string suitable for inclusion in a {@code wg-quick}
* configuration file.
*
* @return the {@code Peer} represented as a series of "Key = Value" lines
*/
public String toAwgQuickString() {
public String toWgQuickString() {
final StringBuilder sb = new StringBuilder();
if (!allowedIps.isEmpty())
sb.append("AllowedIPs = ").append(Attribute.join(allowedIps)).append('\n');
@@ -185,12 +184,12 @@ public final class Peer {
}
/**
* Serializes the {@code Peer} for use with the AmneziaWG cross-platform userspace API. Note
* Serializes the {@code Peer} for use with the WireGuard cross-platform userspace API. Note
* that not all attributes are included in this representation.
*
* @return the {@code Peer} represented as a series of "key=value" lines
*/
public String toAwgUserspaceString() {
public String toWgUserspaceString() {
final StringBuilder sb = new StringBuilder();
// The order here is important: public_key signifies the beginning of a new peer.
sb.append("public_key=").append(publicKey.toHex()).append('\n');
@@ -204,7 +203,7 @@ public final class Peer {
@SuppressWarnings("UnusedReturnValue")
public static final class Builder {
// See awg(8)
// See wg(8)
private static final int MAX_PERSISTENT_KEEPALIVE = 65535;
// Defaults to an empty set.
@@ -1,21 +1,19 @@
/*
* Copyright © 2016 Southern Storm Software, Pty Ltd.
* Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package org.amnezia.awg.crypto;
import org.amnezia.awg.util.NonNullForAll;
import java.util.Arrays;
package com.wireguard.crypto;
import androidx.annotation.Nullable;
import java.util.Arrays;
/**
* Implementation of Curve25519 ECDH.
* Implementation of the Curve25519 elliptic curve algorithm.
* <p>
* This implementation was imported to AmneziaWG from noise-java:
* This implementation was imported to WireGuard from noise-java:
* https://github.com/rweather/noise-java
* <p>
* This implementation is based on that from arduinolibs:
@@ -27,7 +25,6 @@ import androidx.annotation.Nullable;
* References: http://cr.yp.to/ecdh.html, RFC 7748
*/
@SuppressWarnings({"MagicNumber", "NonConstantFieldWithUpperCaseName", "SuspiciousNameCombination"})
@NonNullForAll
public final class Curve25519 {
// Numbers modulo 2^255 - 19 are broken up into ten 26-bit words.
private static final int NUM_LIMBS_255BIT = 10;
@@ -1,25 +1,22 @@
/*
* Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package org.amnezia.awg.crypto;
package com.wireguard.crypto;
import org.amnezia.awg.crypto.KeyFormatException.Type;
import org.amnezia.awg.util.NonNullForAll;
import com.wireguard.crypto.KeyFormatException.Type;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays;
/**
* Represents an AmneziaWG public or private key. This class uses specialized constant-time base64
* Represents a WireGuard public or private key. This class uses specialized constant-time base64
* and hexadecimal codec implementations that resist side-channel attacks.
* <p>
* Instances of this class are immutable.
*/
@SuppressWarnings("MagicNumber")
@NonNullForAll
public final class Key {
private final byte[] key;
@@ -83,10 +80,10 @@ public final class Key {
}
/**
* Decodes an AmneziaWG public or private key from its base64 string representation. This
* Decodes a WireGuard public or private key from its base64 string representation. This
* function throws a {@link KeyFormatException} if the source string is not well-formed.
*
* @param str the base64 string representation of an AmneziaWG key
* @param str the base64 string representation of a WireGuard key
* @return the decoded key encapsulated in an immutable container
*/
public static Key fromBase64(final String str) throws KeyFormatException {
@@ -120,10 +117,10 @@ public final class Key {
}
/**
* Wraps an AmneziaWG public or private key in an immutable container. This function throws a
* Wraps a WireGuard public or private key in an immutable container. This function throws a
* {@link KeyFormatException} if the source data is not the correct length.
*
* @param bytes an array of bytes containing an AmneziaWG key in binary format
* @param bytes an array of bytes containing a WireGuard key in binary format
* @return the key encapsulated in an immutable container
*/
public static Key fromBytes(final byte[] bytes) throws KeyFormatException {
@@ -133,10 +130,10 @@ public final class Key {
}
/**
* Decodes an AmneziaWG public or private key from its hexadecimal string representation. This
* Decodes a WireGuard public or private key from its hexadecimal string representation. This
* function throws a {@link KeyFormatException} if the source string is not well-formed.
*
* @param str the hexadecimal string representation of an AmneziaWG key
* @param str the hexadecimal string representation of a WireGuard key
* @return the decoded key encapsulated in an immutable container
*/
public static Key fromHex(final String str) throws KeyFormatException {
@@ -204,16 +201,6 @@ public final class Key {
return new Key(publicKey);
}
@Override
public boolean equals(final Object obj) {
if (obj == this)
return true;
if (obj == null || obj.getClass() != getClass())
return false;
final Key other = (Key) obj;
return MessageDigest.isEqual(key, other.key);
}
/**
* Returns the key as an array of bytes.
*
@@ -224,14 +211,6 @@ public final class Key {
return Arrays.copyOf(key, key.length);
}
@Override
public int hashCode() {
int ret = 0;
for (int i = 0; i < key.length / 4; ++i)
ret ^= (key[i * 4 + 0] >> 0) + (key[i * 4 + 1] >> 8) + (key[i * 4 + 2] >> 16) + (key[i * 4 + 3] >> 24);
return ret;
}
/**
* Encodes the key to base64.
*
@@ -269,7 +248,7 @@ public final class Key {
}
/**
* The supported formats for encoding an AmneziaWG key.
* The supported formats for encoding a WireGuard key.
*/
public enum Format {
BASE64(44),
@@ -1,18 +1,15 @@
/*
* Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package org.amnezia.awg.crypto;
import org.amnezia.awg.util.NonNullForAll;
package com.wireguard.crypto;
/**
* An exception thrown when attempting to parse an invalid key (too short, too long, or byte
* data inappropriate for the format). The format being parsed can be accessed with the
* {@link #getFormat} method.
*/
@NonNullForAll
public final class KeyFormatException extends Exception {
private final Key.Format format;
private final Type type;
@@ -1,18 +1,15 @@
/*
* Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package org.amnezia.awg.crypto;
import org.amnezia.awg.util.NonNullForAll;
package com.wireguard.crypto;
/**
* Represents a Curve25519 key pair as used by AmneziaWG.
* Represents a Curve25519 key pair as used by WireGuard.
* <p>
* Instances of this class are immutable.
*/
@NonNullForAll
public class KeyPair {
private final Key privateKey;
private final Key publicKey;
@@ -0,0 +1,14 @@
/*
* 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();
}
@@ -0,0 +1,32 @@
/*
* 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,10 +1,11 @@
/*
* Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package org.amnezia.awg.util;
package com.wireguard.util;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -12,18 +13,14 @@ import java.lang.annotation.RetentionPolicy;
import javax.annotation.Nonnull;
import javax.annotation.meta.TypeQualifierDefault;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
/**
* This annotation can be applied to a package, class or method to indicate that all
* class fields and method parameters and return values in that element are nonnull
* by default unless overridden.
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@Documented
@Nonnull
@TypeQualifierDefault({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface NonNullForAll {
}
@@ -0,0 +1,31 @@
/*
* 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();
}
@@ -0,0 +1,10 @@
<?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,10 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FFFFFFFF"
android:fillColor="#ffffff"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</vector>
@@ -1,10 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#FFFFFFFF"
android:fillColor="?android:attr/colorForeground"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
</vector>
@@ -1,10 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#FFFFFFFF"
android:fillColor="?android:attr/colorForeground"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
</vector>
@@ -0,0 +1,9 @@
<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="#ffffff"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
</vector>
@@ -1,10 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#FFFFFFFF"
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>
@@ -0,0 +1,9 @@
<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="#ffffff"
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>
@@ -2,10 +2,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#FFFFFFFF"
android:fillColor="?android:attr/colorForeground"
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z" />
</vector>
@@ -1,10 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#FFFFFFFF"
android:fillColor="#ffffff"
android:pathData="M4,4H10V10H4V4M20,4V10H14V4H20M14,15H16V13H14V11H16V13H18V11H20V13H18V15H20V18H18V20H16V18H13V20H11V16H14V15M16,15V18H18V15H16M4,20V14H10V20H4M6,6V8H8V6H6M16,6V8H18V6H16M6,16V18H8V16H6M4,11H6V13H4V11M9,11H13V15H11V13H9V11M11,6H13V10H11V6M2,2V6H0V2A2,2 0 0,1 2,0H6V2H2M22,0A2,2 0 0,1 24,2V6H22V2H18V0H22M2,18V22H6V24H2A2,2 0 0,1 0,22V18H2M22,22V18H24V22A2,2 0 0,1 22,24H18V22H22Z" />
</vector>
</vector>
@@ -2,10 +2,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#FFFFFFFF"
android:fillColor="?android:attr/colorForeground"
android:pathData="M3 5L5 5 5 3C3.9 3 3 3.9 3 5Zm0 8l2 0 0 -2 -2 0 0 2zm4 8l2 0 0 -2 -2 0 0 2zM3 9L5 9 5 7 3 7 3 9Zm10 -6l-2 0 0 2 2 0 0 -2zm6 0l0 2 2 0C21 3.9 20.1 3 19 3ZM5 21L5 19 3 19c0 1.1 0.9 2 2 2zm-2 -4l2 0 0 -2 -2 0 0 2zM9 3L7 3 7 5 9 5 9 3Zm2 18l2 0 0 -2 -2 0 0 2zm8 -8l2 0 0 -2 -2 0 0 2zm0 8c1.1 0 2 -0.9 2 -2l-2 0 0 2zm0 -12l2 0 0 -2 -2 0 0 2zm0 8l2 0 0 -2 -2 0 0 2zm-4 4l2 0 0 -2 -2 0 0 2zm0 -16l2 0 0 -2 -2 0 0 2zM7 17L17 17 17 7 7 7 7 17Zm2 -8l6 0 0 6 -6 0 0 -6z" />
</vector>
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="2160"
android:viewportWidth="2160">
<group
android:scaleX="1"
android:scaleY="-1"
android:translateX="630"
android:translateY="1750">
<group>
<clip-path android:pathData="M0 1347.452l773.449 0L773.449 0 0 0Z" />
<group
android:translateX="349.0264"
android:translateY="572.616">
<path
android:fillColor="#ffffff"
android:pathData="M0 0c-12.169 -6.44 -21.541 -11.184 -30.71 -16.292 -37.523 -20.902 -69.603 -48.262 -95.162 -82.767 -8.264 -11.156 -13.945 -12.055 -26.528 -4.36 -163.692 100.101 -174.212 351.318 4.549 460.681 139.045 85.064 316.68 33.074 383.242 -94.85 12.614 -24.244 14.218 -61.567 6.228 -87 -27.582 -87.807 -92.71 -137.049 -182.1 -157.968 26.353 22.561 47.329 48.145 54.006 83.494 6.725 35.606 -0.388 67.807 -21.041 97.072 -31.371 44.451 -92.029 62.74 -142.721 43.492 -55.035 -20.896 -85.181 -71.123 -79.747 -132.863C-124.935 51.288 -81.419 14.12 0 0" />
</group>
<group android:translateY="285.9856">
<path
android:fillColor="#ffffff"
android:pathData="M0 0C13.148 88.712 117.033 170.407 204.881 161.087 177.673 124.291 165.104 82.664 162.071 41.145 132.88 35.769 105.368 32.152 78.66 25.373 52.364 18.698 26.882 8.816 0 0" />
</group>
<group
android:translateX="580.2814"
android:translateY="1243.915">
<path
android:fillColor="#ffffff"
android:pathData="M0 0C4.917 3.762 9.98 6.922 16.085 1.891 19.557 -0.97 22.93 -3.933 27.136 -7.523 21.915 -10.28 17.676 -12.599 13.355 -14.779 7.307 -17.83 2.785 -15.792 -0.877 -10.972 -3.847 -7.062 -4.384 -3.354 0 0m71.552 -730.934c-7.403 6.401 -12.094 6.399 -20.775 0.845 -29.454 -18.844 -59.602 -36.696 -90.239 -53.556 -17.562 -9.664 -36.584 -16.675 -58.61 -26.516 7.564 -1.952 11.203 -2.865 14.829 -3.83 82.337 -21.913 126.326 -94.196 106.841 -175.157 -17.329 -71.999 -90.422 -118.033 -161.255 -105.889 -59.053 10.125 -110.601 59.161 -119.21 117.917 -9.382 64.032 22.508 125.618 79.246 151.417 31.472 14.31 63.79 26.766 95.186 41.229 35.697 16.445 74.29 29.435 105.458 52.322 77.355 56.801 125.124 135.006 143.747 229.401 11.155 56.543 10.401 112.837 -15.467 166.524 -19.851 41.202 -52.429 71.133 -87.429 98.447 -36.018 28.108 -74.148 53.518 -110.002 81.821 -9.702 7.659 -16.252 20.865 -20.742 32.84 -1.903 5.075 4.287 18.838 8.426 19.581 21.985 3.946 44.45 5.978 66.818 6.823 25.82 0.974 51.713 0.148 77.571 -0.192 5.606 -0.073 13.217 0.653 16.439 -2.514 13.394 -13.167 23.897 -4.697 33.194 3.965 7.823 7.29 13.399 16.992 19.62 25.168 -3.775 0.555 -11.519 2.505 -19.304 2.689 -26.003 0.616 -52.035 0.221 -78.021 1.176 -4.63 0.17 -9.09 4.935 -13.629 7.579 4.776 1.898 9.537 5.399 14.33 5.444 44.849 0.421 89.703 0.25 134.594 0.25 0.052 23.336 -31.136 55.291 -58.846 63.95 -0.207 -3.158 -0.4 -6.097 -0.606 -9.233C106.184 0.913 79.16 1.426 54.61 14.481 48.141 17.921 43.912 25.57 38.647 31.279 32.019 38.467 26.58 47.709 18.5 52.399 1.934 62.014 -16.148 68.982 -33.431 77.4c-61.418 29.914 -126.278 28.862 -195.946 22.484 41.644 -9.693 79.255 -18.447 116.865 -27.202 -0.428 -2.286 -0.856 -4.573 -1.284 -6.86 -50.308 -6.74 -97.898 11.71 -147.101 18.545 17.831 -10.443 35.897 -20.152 54.561 -28.527 18.965 -8.51 38.52 -15.705 58.076 -23.58 -24.845 -21.229 -49.776 -25.887 -81.008 -18.751 -17.072 3.901 -35.132 5.972 -52.564 5.121 -18.006 -0.88 -36.141 -5.311 -52.491 -16.238 17.507 -8.875 33.643 -16.24 48.864 -25.165 6.277 -3.681 13.473 -9.93 15.223 -16.377 4.191 -15.44 5.402 -31.689 7.817 -47.623 -28.667 -3.247 -79.074 -32.4 -89.261 -51.373 44.059 -8.478 92.031 1.777 134.06 -26.617 -13.844 -10.477 -46.085 -23.507 -57.911 -32.457 14.621 -3.831 48.498 -1.955 61.751 -1.057 11.157 0.756 16.306 1.029 20.881 -2.735l129.701 -101.541c13.638 -10.994 68.719 -63.131 83.098 -95.903 12.241 -27.902 13.739 -51.638 13.736 -57.431 -0.01 -15.541 -1.917 -39.876 -12.605 -67.022 -4.488 -11.399 -17.658 -36.65 -44.826 -66.083 -42.107 -45.617 -96.27 -70.274 -155.501 -82.487 -137.722 -28.395 -252.153 -175.469 -219.85 -337.61 37.714 -189.296 246.646 -291.784 417.386 -201.739 110.359 58.201 168.871 171.751 153.193 295.356 -9.471 74.672 -43.252 135.578 -99.881 184.538" />
</group>
</group>
</group>
</vector>

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