Compare commits

...

174 Commits

Author SHA1 Message Date
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
147 changed files with 5645 additions and 3473 deletions
+1
View File
@@ -15,3 +15,4 @@ build/
*.iml
*.jks
keystore.properties
package-info.java
-3
View File
@@ -4,6 +4,3 @@
[submodule "app/tools/wireguard"]
path = app/tools/wireguard
url = https://git.zx2c4.com/WireGuard
[submodule "app/tools/wireguard-go"]
path = app/tools/wireguard-go
url = https://git.zx2c4.com/wireguard-go
+5
View File
@@ -34,6 +34,9 @@
<option name="USE_CUSTOM_SETTINGS" value="true" />
</AndroidXmlCodeStyleSettings>
<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="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">
@@ -94,6 +97,8 @@
<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>
+1 -1
View File
@@ -1,6 +1,6 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="Copyright © &amp;#36;today.year Firstname Lastname &lt;email@example.org&gt;&#10;SPDX-License-Identifier:" />
<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>
-6
View File
@@ -1,6 +0,0 @@
<component name="CopyrightManager">
<copyright>
<option name="myName" value="GPL-2.0-or-later" />
<option name="notice" value="Copyright © &amp;#36;today.year Firstname Lastname &lt;email@example.com&gt;&#10;SPDX-License-Identifier: GPL-2.0-or-later" />
</copyright>
</component>
+2 -2
View File
@@ -1,3 +1,3 @@
<component name="CopyrightManager">
<settings default="GPL-2.0-or-later" />
</component>
<settings default="Default" />
</component>
-1
View File
@@ -385,7 +385,6 @@
<option name="ignorePrivateMethods" value="true" />
</inspection_tool>
<inspection_tool class="ReturnOfDateField" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ReturnOfInnerClass" enabled="true" level="WARNING" enabled_by_default="true" />
<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" />
+169 -305
View File
@@ -1,338 +1,202 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Preamble
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
1. Definitions.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
The precise terms and conditions for copying, distribution and
modification follow.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
END OF TERMS AND CONDITIONS
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
APPENDIX: How to apply the Apache License to your work.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
Copyright [yyyy] [name of copyright owner]
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
http://www.apache.org/licenses/LICENSE-2.0
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2
as published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-16
View File
@@ -13,19 +13,3 @@ $ git submodule init
$ git submodule update
$ ./gradlew assembleRelease
```
## License
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+42 -14
View File
@@ -1,23 +1,25 @@
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 '28.0.0'
buildToolsVersion '28.0.3'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
compileSdkVersion 27
compileSdkVersion 28
dataBinding.enabled true
defaultConfig {
applicationId 'com.wireguard.android'
minSdkVersion 21
targetSdkVersion 27
versionCode 427
versionName '0.0.20180625'
targetSdkVersion 28
versionCode 450
versionName '0.0.20190605'
buildConfigField 'int', 'MIN_SDK_VERSION', "$minSdkVersion.apiLevel"
}
// If the keystore file exists
if (keystorePropertiesFile.exists()) {
@@ -39,6 +41,22 @@ android {
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 {
@@ -49,18 +67,28 @@ android {
}
ext {
databindingVersion = '3.1.3'
supportLibsVersion = '27.1.1'
streamsupportVersion = '1.6.0'
annotationsVersion = '1.0.1'
appcompatVersion = '1.0.2'
cardviewVersion = '1.0.0'
databindingVersion = '3.3.1'
materialComponentsVersion = '1.0.0'
jsr305Version = '3.0.2'
preferenceVersion = '1.0.0'
streamsupportVersion = '1.7.0'
threetenabpVersion = '1.1.1'
zxingEmbeddedVersion = '3.6.0'
}
dependencies {
implementation "com.android.databinding:library:$databindingVersion"
implementation "com.android.support:appcompat-v7:$supportLibsVersion"
implementation "com.android.support:cardview-v7:$supportLibsVersion"
implementation "com.android.support:design:$supportLibsVersion"
implementation "com.android.support:preference-v14:$supportLibsVersion"
implementation "com.android.support:support-annotations:$supportLibsVersion"
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"
}
+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"
}
+5
View File
@@ -0,0 +1,5 @@
# Squelch all warnings, they're harmless but ProGuard
# escalates them as errors.
-dontwarn sun.misc.Unsafe
# We're OSS anyway and who doesn't love a readable log
-dontobfuscate
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">WireGuard β</string>
</resources>
+11 -6
View File
@@ -4,16 +4,17 @@
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.WRITE_EXTERNAL_STORAGE" />
<permission
android:name="com.wireguard.android.permission.CONTROL_TUNNELS"
android:protectionLevel="dangerous"
android:name="${applicationId}.permission.CONTROL_TUNNELS"
android:description="@string/permission_description"
android:icon="@mipmap/ic_launcher"
android:label="@string/permission_label"
android:description="@string/permission_description" />
android:protectionLevel="dangerous" />
<application
android:name=".Application"
@@ -42,14 +43,18 @@
<activity
android:name=".activity.SettingsActivity"
android:label="@string/settings"
android:parentActivityName=".activity.MainActivity"
android:theme="@style/SettingsTheme" />
android:parentActivityName=".activity.MainActivity" />
<activity
android:name=".activity.TunnelCreatorActivity"
android:label="@string/create_activity_title"
android:parentActivityName=".activity.MainActivity" />
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="fullSensor"
tools:replace="screenOrientation" />
<receiver android:name=".BootShutdownReceiver">
<intent-filter>
<action android:name="android.intent.action.ACTION_SHUTDOWN" />
@@ -59,7 +64,7 @@
<receiver
android:name=".model.TunnelManager$IntentReceiver"
android:permission="com.wireguard.android.permission.CONTROL_TUNNELS">
android:permission="${applicationId}.permission.CONTROL_TUNNELS">
<intent-filter>
<action android:name="com.wireguard.android.action.REFRESH_TUNNEL_STATES" />
<action android:name="com.wireguard.android.action.SET_TUNNEL_UP" />
@@ -1,21 +1,24 @@
/*
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.preference.PreferenceManager;
import android.support.v7.app.AppCompatDelegate;
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.ConfigStore;
import com.wireguard.android.configStore.FileConfigStore;
import com.wireguard.android.model.TunnelManager;
import com.wireguard.android.util.AsyncWorker;
@@ -24,21 +27,18 @@ import com.wireguard.android.util.ToolsInstaller;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java9.util.concurrent.CompletableFuture;
public class Application extends android.app.Application {
private static WeakReference<Application> weakSelf;
private AsyncWorker asyncWorker;
private Backend backend;
private RootShell rootShell;
private SharedPreferences sharedPreferences;
private ToolsInstaller toolsInstaller;
private TunnelManager tunnelManager;
private Handler handler;
private List<BackendCallback> haveBackendCallbacks = new ArrayList<>();
private final Object haveBackendCallbacksLock = new Object();
@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 TunnelManager tunnelManager;
public Application() {
weakSelf = new WeakReference<>(this);
@@ -54,39 +54,26 @@ public class Application extends android.app.Application {
public static Backend getBackend() {
final Application app = get();
synchronized (app) {
synchronized (app.futureBackend) {
if (app.backend == null) {
Backend backend = null;
if (new File("/sys/module/wireguard").exists()) {
try {
app.rootShell.start();
app.backend = new WgQuickBackend(app.getApplicationContext());
} catch (final Exception ignored) { }
}
if (app.backend == null)
app.backend = new GoBackend(app.getApplicationContext());
synchronized (app.haveBackendCallbacksLock) {
for (final BackendCallback callback : app.haveBackendCallbacks)
app.handler.post(() -> callback.callback(app.backend));
app.haveBackendCallbacks = null;
backend = new WgQuickBackend(app.getApplicationContext());
} catch (final Exception ignored) {
}
}
if (backend == null)
backend = new GoBackend(app.getApplicationContext());
app.backend = backend;
}
return app.backend;
}
}
@FunctionalInterface
public interface BackendCallback {
void callback(final Backend backend);
}
public static void onHaveBackend(final BackendCallback callback) {
final Application app = get();
synchronized (app.haveBackendCallbacksLock) {
if (app.haveBackendCallbacks == null)
callback.callback(app.backend);
else
app.haveBackendCallbacks.add(callback);
}
public static CompletableFuture<Backend> getBackendAsync() {
return get().futureBackend;
}
public static RootShell getRootShell() {
@@ -105,15 +92,25 @@ public class Application extends android.app.Application {
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();
handler = new Handler(Looper.getMainLooper());
final Executor executor = AsyncTask.SERIAL_EXECUTOR;
final ConfigStore configStore = new FileConfigStore(getApplicationContext());
asyncWorker = new AsyncWorker(executor, handler);
asyncWorker = new AsyncWorker(AsyncTask.SERIAL_EXECUTOR, new Handler(Looper.getMainLooper()));
rootShell = new RootShell(getApplicationContext());
toolsInstaller = new ToolsInstaller(getApplicationContext());
@@ -122,8 +119,9 @@ public class Application extends android.app.Application {
sharedPreferences.getBoolean("dark_theme", false) ?
AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);
tunnelManager = new TunnelManager(configStore);
asyncWorker.runAsync(Application::getBackend);
tunnelManager = new TunnelManager(new FileConfigStore(getApplicationContext()));
tunnelManager.onCreate();
asyncWorker.supplyAsync(Application::getBackend).thenAccept(futureBackend::complete);
}
}
@@ -1,7 +1,6 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android;
@@ -20,8 +19,8 @@ public class BootShutdownReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context context, final Intent intent) {
Application.onHaveBackend(backend -> {
if (backend.getClass() != WgQuickBackend.class)
Application.getBackendAsync().thenAccept(backend -> {
if (!(backend instanceof WgQuickBackend))
return;
final String action = intent.getAction();
if (action == null)
@@ -1,26 +1,30 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* 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 android.databinding.Observable;
import android.databinding.Observable.OnPropertyChangedCallback;
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.ExceptionLoggers;
import com.wireguard.android.util.ErrorMessages;
import com.wireguard.android.widget.SlashDrawable;
import java.util.Objects;
@@ -36,11 +40,31 @@ public class QuickTileService extends TileService {
private final OnStateChangedCallback onStateChangedCallback = new OnStateChangedCallback();
private final OnTunnelChangedCallback onTunnelChangedCallback = new OnTunnelChangedCallback();
private Tunnel tunnel;
@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);
@@ -49,6 +73,28 @@ public class QuickTileService extends TileService {
}
}
@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);
@@ -65,10 +111,10 @@ public class QuickTileService extends TileService {
}
private void onToggleFinished(@SuppressWarnings("unused") final State state,
final Throwable throwable) {
@Nullable final Throwable throwable) {
if (throwable == null)
return;
final String error = ExceptionLoggers.unwrapMessage(throwable);
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();
@@ -99,10 +145,7 @@ public class QuickTileService extends TileService {
return;
tile.setLabel(label);
if (tile.getState() != state) {
// The icon must be changed every time the state changes, or the shade will not change.
final Integer iconResource = state == Tile.STATE_ACTIVE ? R.drawable.ic_tile
: R.drawable.ic_tile_disabled;
tile.setIcon(Icon.createWithResource(this, iconResource));
tile.setIcon(state == Tile.STATE_ACTIVE ? iconOn : iconOff);
tile.setState(state);
}
tile.updateTile();
@@ -1,20 +1,17 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity;
import android.content.Intent;
import android.databinding.CallbackRegistry;
import android.databinding.CallbackRegistry.NotifierCallback;
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.backend.GoBackend;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.TunnelManager;
import java.util.Objects;
@@ -26,40 +23,34 @@ public abstract class BaseActivity extends ThemeChangeAwareActivity {
private static final String KEY_SELECTED_TUNNEL = "selected_tunnel";
private final SelectionChangeRegistry selectionChangeRegistry = new SelectionChangeRegistry();
private Tunnel selectedTunnel;
@Nullable private Tunnel selectedTunnel;
public void addOnSelectedTunnelChangedListener(
final OnSelectedTunnelChangedListener listener) {
public void addOnSelectedTunnelChangedListener(final OnSelectedTunnelChangedListener listener) {
selectionChangeRegistry.add(listener);
}
@Nullable
public Tunnel getSelectedTunnel() {
return selectedTunnel;
}
@Override
protected void onCreate(final Bundle savedInstanceState) {
protected void onCreate(@Nullable final Bundle savedInstanceState) {
// Restore the saved tunnel if there is one; otherwise grab it from the arguments.
String savedTunnelName = null;
final String savedTunnelName;
if (savedInstanceState != null)
savedTunnelName = savedInstanceState.getString(KEY_SELECTED_TUNNEL);
else if (getIntent() != null)
savedTunnelName = getIntent().getStringExtra(KEY_SELECTED_TUNNEL);
if (savedTunnelName != null) {
final TunnelManager tunnelManager = Application.getTunnelManager();
selectedTunnel = tunnelManager.getTunnels().get(savedTunnelName);
}
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);
Application.onHaveBackend(backend -> {
if (backend.getClass() == GoBackend.class) {
final Intent intent = GoBackend.VpnService.prepare(this);
if (intent != null)
startActivityForResult(intent, 0);
}
});
}
@Override
@@ -69,14 +60,14 @@ public abstract class BaseActivity extends ThemeChangeAwareActivity {
super.onSaveInstanceState(outState);
}
protected abstract void onSelectedTunnelChanged(Tunnel oldTunnel, Tunnel newTunnel);
protected abstract void onSelectedTunnelChanged(@Nullable Tunnel oldTunnel, @Nullable Tunnel newTunnel);
public void removeOnSelectedTunnelChangedListener(
final OnSelectedTunnelChangedListener listener) {
selectionChangeRegistry.remove(listener);
}
public void setSelectedTunnel(final Tunnel tunnel) {
public void setSelectedTunnel(@Nullable final Tunnel tunnel) {
final Tunnel oldTunnel = selectedTunnel;
if (Objects.equals(oldTunnel, tunnel))
return;
@@ -86,7 +77,7 @@ public abstract class BaseActivity extends ThemeChangeAwareActivity {
}
public interface OnSelectedTunnelChangedListener {
void onSelectedTunnelChanged(Tunnel oldTunnel, Tunnel newTunnel);
void onSelectedTunnelChanged(@Nullable Tunnel oldTunnel, @Nullable Tunnel newTunnel);
}
private static final class SelectionChangeNotifier
@@ -1,7 +1,6 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity;
@@ -9,12 +8,14 @@ package com.wireguard.android.activity;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.util.Log;
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;
@@ -22,89 +23,63 @@ import com.wireguard.android.fragment.TunnelEditorFragment;
import com.wireguard.android.fragment.TunnelListFragment;
import com.wireguard.android.model.Tunnel;
import java9.util.stream.Stream;
/**
* 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 {
private static final String KEY_STATE = "fragment_state";
private static final String TAG = "WireGuard/" + MainActivity.class.getSimpleName();
private State state = State.EMPTY;
private boolean moveToState(final State nextState) {
if (state == nextState)
return false;
final FragmentManager fragmentManager = getSupportFragmentManager();
Log.i(TAG, "Moving from " + state.name() + " to " + nextState.name());
if (nextState.layer > state.layer + 1) {
moveToState(State.ofLayer(state.layer + 1));
moveToState(nextState);
return true;
} else if (nextState.layer == state.layer + 1) {
final Fragment fragment = Fragment.instantiate(this, nextState.fragment);
final FragmentTransaction transaction = fragmentManager.beginTransaction()
.replace(R.id.master_fragment, fragment)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
if (state.layer > 0)
transaction.addToBackStack(null);
transaction.commit();
} else if (nextState.layer == state.layer - 1) {
if (fragmentManager.getBackStackEntryCount() == 0)
return false;
fragmentManager.popBackStack();
} else if (nextState.layer < state.layer - 1) {
moveToState(State.ofLayer(state.layer - 1));
moveToState(nextState);
return true;
}
state = nextState;
if (state.layer <= State.LIST.layer)
setSelectedTunnel(null);
updateActionBar();
return true;
}
public class MainActivity extends BaseActivity
implements FragmentManager.OnBackStackChangedListener {
@Nullable private ActionBar actionBar;
private boolean isTwoPaneLayout;
@Nullable private TunnelListFragment listFragment;
@Override
public void onBackPressed() {
TunnelListFragment fragment = null;
try {
fragment = ((TunnelListFragment) getSupportFragmentManager().getFragments().get(0));
} catch (final ClassCastException ignored) { }
if (fragment == null || !fragment.collapseActionMenu()) {
if (!moveToState(State.ofLayer(state.layer - 1)))
super.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(final Bundle savedInstanceState) {
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
if (savedInstanceState != null && savedInstanceState.getString(KEY_STATE) != null)
state = State.valueOf(savedInstanceState.getString(KEY_STATE));
if (state == State.EMPTY) {
State initialState = getSelectedTunnel() != null ? State.DETAIL : State.LIST;
if (getIntent() != null && getIntent().getStringExtra(KEY_STATE) != null)
initialState = State.valueOf(getIntent().getStringExtra(KEY_STATE));
moveToState(initialState);
}
updateActionBar();
final int actionBarId = getResources().getIdentifier("action_bar", "id", getPackageName());
if (actionBarId != 0 && findViewById(actionBarId) != null) {
findViewById(actionBarId).setOnTouchListener((v, e) -> {
try {
((TunnelListFragment) getSupportFragmentManager().getFragments().get(0)).collapseActionMenu();
} catch (final ClassCastException ignored) { }
return false;
});
}
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
@@ -118,11 +93,14 @@ public class MainActivity extends BaseActivity {
switch (item.getItemId()) {
case android.R.id.home:
// The back arrow in the action bar should act the same as the back button.
moveToState(State.ofLayer(state.layer - 1));
onBackPressed();
return true;
case R.id.menu_action_edit:
if (getSelectedTunnel() != null)
moveToState(State.EDITOR);
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.
@@ -136,37 +114,26 @@ public class MainActivity extends BaseActivity {
}
@Override
protected void onSaveInstanceState(final Bundle outState) {
outState.putString(KEY_STATE, state.name());
super.onSaveInstanceState(outState);
}
@Override
protected void onSelectedTunnelChanged(final Tunnel oldTunnel, final Tunnel newTunnel) {
moveToState(newTunnel != null ? State.DETAIL : State.LIST);
}
private void updateActionBar() {
if (getSupportActionBar() != null)
getSupportActionBar().setDisplayHomeAsUpEnabled(state.layer > State.LIST.layer);
}
private enum State {
EMPTY(null, 0),
LIST(TunnelListFragment.class, 1),
DETAIL(TunnelDetailFragment.class, 2),
EDITOR(TunnelEditorFragment.class, 3);
private final String fragment;
private final int layer;
State(final Class<? extends Fragment> fragment, final int layer) {
this.fragment = fragment != null ? fragment.getName() : null;
this.layer = layer;
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;
}
private static State ofLayer(final int layer) {
return Stream.of(State.values()).filter(s -> s.layer == layer).findFirst().get();
if (backStackEntries == 2) {
// Pop the editor off the back stack to reveal the detail fragment. Use the immediate
// method to avoid the editor picking up the new tunnel while it is still visible.
fragmentManager.popBackStackImmediate();
} else if (backStackEntries == 0) {
// Create and show a new detail fragment.
fragmentManager.beginTransaction()
.add(R.id.detail_container, new TunnelDetailFragment())
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.addToBackStack(null)
.commit();
}
}
}
@@ -1,19 +1,19 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* 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.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceFragmentCompat;
import android.support.v7.preference.PreferenceScreen;
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;
@@ -22,16 +22,14 @@ import com.wireguard.android.backend.WgQuickBackend;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Interface for changing application-global persistent settings.
*/
public class SettingsActivity extends ThemeChangeAwareActivity {
private final Map<Integer, PermissionRequestCallback> permissionRequestCallbacks = new HashMap<>();
private final SparseArray<PermissionRequestCallback> permissionRequestCallbacks = new SparseArray<>();
private int permissionRequestCounter;
public void ensurePermissions(final String[] permissions, final PermissionRequestCallback cb) {
@@ -54,7 +52,7 @@ public class SettingsActivity extends ThemeChangeAwareActivity {
}
@Override
protected void onCreate(final Bundle savedInstanceState) {
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getSupportFragmentManager().findFragmentById(android.R.id.content) == null) {
getSupportFragmentManager().beginTransaction()
@@ -76,8 +74,8 @@ public class SettingsActivity extends ThemeChangeAwareActivity {
@Override
public void onRequestPermissionsResult(final int requestCode,
@NonNull final String[] permissions,
@NonNull final int[] grantResults) {
final String[] permissions,
final int[] grantResults) {
final PermissionRequestCallback f = permissionRequestCallbacks.get(requestCode);
if (f != null) {
permissionRequestCallbacks.remove(requestCode);
@@ -94,15 +92,15 @@ public class SettingsActivity extends ThemeChangeAwareActivity {
public void onCreatePreferences(final Bundle savedInstanceState, final String key) {
addPreferencesFromResource(R.xml.preferences);
final Preference wgQuickOnlyPrefs[] = {
getPreferenceManager().findPreference("tools_installer"),
getPreferenceManager().findPreference("restore_on_boot")
getPreferenceManager().findPreference("tools_installer"),
getPreferenceManager().findPreference("restore_on_boot")
};
for (final Preference pref : wgQuickOnlyPrefs)
pref.setVisible(false);
final PreferenceScreen screen = getPreferenceScreen();
Application.onHaveBackend(backend -> {
Application.getBackendAsync().thenAccept(backend -> {
for (final Preference pref : wgQuickOnlyPrefs) {
if (backend.getClass() == WgQuickBackend.class)
if (backend instanceof WgQuickBackend)
pref.setVisible(true);
else
screen.removePreference(pref);
@@ -1,6 +1,6 @@
/*
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity;
@@ -8,8 +8,9 @@ package com.wireguard.android.activity;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.app.AppCompatDelegate;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
import android.util.Log;
import com.wireguard.android.Application;
@@ -18,9 +19,9 @@ 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 Resources lastResources;
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;
@@ -32,7 +33,8 @@ public abstract class ThemeChangeAwareActivity extends AppCompatActivity impleme
f = o.getClass().getDeclaredField("mResourcesImpl");
f.setAccessible(true);
o = f.get(o);
} catch (final Exception ignored) { }
} catch (final Exception ignored) {
}
f = o.getClass().getDeclaredField("mDrawableCache");
f.setAccessible(true);
o = f.get(o);
@@ -51,7 +53,7 @@ public abstract class ThemeChangeAwareActivity extends AppCompatActivity impleme
@Override
protected void onCreate(final Bundle savedInstanceState) {
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Application.getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
}
@@ -1,12 +1,12 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* 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;
@@ -18,7 +18,7 @@ import com.wireguard.android.model.Tunnel;
public class TunnelCreatorActivity extends BaseActivity {
@Override
@SuppressWarnings("UnnecessaryFullyQualifiedName")
protected void onCreate(final Bundle savedInstanceState) {
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getSupportFragmentManager().findFragmentById(android.R.id.content) == null) {
getSupportFragmentManager().beginTransaction()
@@ -28,7 +28,7 @@ public class TunnelCreatorActivity extends BaseActivity {
}
@Override
protected void onSelectedTunnelChanged(final Tunnel oldTunnel, final Tunnel newTunnel) {
protected void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
finish();
}
}
@@ -1,7 +1,6 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.backend;
@@ -54,14 +53,11 @@ public interface Backend {
Statistics getStatistics(Tunnel tunnel) throws Exception;
/**
* Set the state of a tunnel.
* Determine type name of underlying backend.
*
* @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.
* @return Type name
*/
State setState(Tunnel tunnel, State state) throws Exception;
String getTypePrettyName();
/**
* Determine version of underlying backend.
@@ -72,9 +68,12 @@ public interface Backend {
String getVersion() throws Exception;
/**
* Determine type name of underlying backend.
* Set the state of a tunnel.
*
* @return Type name
* @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.
*/
String getTypeName();
State setState(Tunnel tunnel, State state) throws Exception;
}
@@ -1,7 +1,6 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.backend;
@@ -10,10 +9,12 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.ParcelFileDescriptor;
import android.support.v4.util.ArraySet;
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;
@@ -22,13 +23,11 @@ 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.Interface;
import com.wireguard.config.Peer;
import com.wireguard.crypto.KeyEncoding;
import java.net.InetAddress;
import java.util.Collections;
import java.util.Formatter;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -40,7 +39,7 @@ public final class GoBackend implements Backend {
private static CompletableFuture<VpnService> vpnService = new CompletableFuture<>();
private final Context context;
private Tunnel currentTunnel;
@Nullable private Tunnel currentTunnel;
private int currentTunnelHandle = -1;
public GoBackend(final Context context) {
@@ -58,12 +57,6 @@ public final class GoBackend implements Backend {
private static native String wgVersion();
@Override
public String getVersion() { return wgVersion(); }
@Override
public String getTypeName() { return "Go userspace"; }
@Override
public Config applyConfig(final Tunnel tunnel, final Config config) throws Exception {
if (tunnel.getState() == State.UP) {
@@ -100,6 +93,16 @@ public final class GoBackend implements Backend {
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);
@@ -108,20 +111,22 @@ public final class GoBackend implements Backend {
if (state == originalState)
return originalState;
if (state == State.UP && currentTunnel != null)
throw new IllegalStateException("Only one userspace tunnel can run at a time");
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, final Config config, final State state)
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("VPN service not authorized by user");
throw new Exception(context.getString(R.string.vpn_not_authorized_error));
final VpnService service;
if (!vpnService.isDone())
@@ -130,7 +135,7 @@ public final class GoBackend implements Backend {
try {
service = vpnService.get(2, TimeUnit.SECONDS);
} catch (final TimeoutException e) {
throw new Exception("Unable to start Android VPN service", e);
throw new Exception(context.getString(R.string.vpn_start_error), e);
}
if (currentTunnelHandle != -1) {
@@ -139,29 +144,7 @@ public final class GoBackend implements Backend {
}
// Build config
final Interface iface = config.getInterface();
final String goConfig;
try (final Formatter fmt = new Formatter(new StringBuilder())) {
fmt.format("replace_peers=true\n");
if (iface.getPrivateKey() != null)
fmt.format("private_key=%s\n", KeyEncoding.keyToHex(KeyEncoding.keyFromBase64(iface.getPrivateKey())));
if (iface.getListenPort() != 0)
fmt.format("listen_port=%d\n", config.getInterface().getListenPort());
for (final Peer peer : config.getPeers()) {
if (peer.getPublicKey() != null)
fmt.format("public_key=%s\n", KeyEncoding.keyToHex(KeyEncoding.keyFromBase64(peer.getPublicKey())));
if (peer.getPreSharedKey() != null)
fmt.format("preshared_key=%s\n", KeyEncoding.keyToHex(KeyEncoding.keyFromBase64(peer.getPreSharedKey())));
if (peer.getEndpoint() != null)
fmt.format("endpoint=%s\n", peer.getResolvedEndpointString());
if (peer.getPersistentKeepalive() != 0)
fmt.format("persistent_keepalive_interval=%d\n", peer.getPersistentKeepalive());
for (final InetNetwork addr : peer.getAllowedIPs()) {
fmt.format("allowed_ip=%s\n", addr.toString());
}
}
goConfig = fmt.toString();
}
final String goConfig = config.toWgUserspaceString();
// Create the vpn tunnel with android API
final VpnService.Builder builder = service.getBuilder();
@@ -171,31 +154,31 @@ public final class GoBackend implements Backend {
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().getDnses())
for (final InetAddress addr : config.getInterface().getDnsServers())
builder.addDnsServer(addr.getHostAddress());
for (final Peer peer : config.getPeers()) {
for (final InetNetwork addr : peer.getAllowedIPs())
for (final InetNetwork addr : peer.getAllowedIps())
builder.addRoute(addr.getAddress(), addr.getMask());
}
int mtu = config.getInterface().getMtu();
if (mtu == 0)
mtu = 1280;
builder.setMtu(mtu);
builder.setMtu(config.getInterface().getMtu().orElse(1280));
builder.setBlocking(true);
try (final ParcelFileDescriptor tun = builder.establish()) {
if (tun == null)
throw new Exception("Unable to create tun device");
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("Unable to turn tunnel on (wgTurnOn return " + currentTunnelHandle + ')');
throw new Exception(context.getString(R.string.tunnel_on_error, currentTunnelHandle));
currentTunnel = tunnel;
@@ -233,16 +216,19 @@ public final class GoBackend implements Backend {
@Override
public void onDestroy() {
for (final Tunnel tunnel : Application.getTunnelManager().getTunnels()) {
if (tunnel != null && tunnel.getState() != State.DOWN)
tunnel.setState(State.DOWN);
}
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(final Intent intent, final int flags, final int startId) {
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");
@@ -250,5 +236,6 @@ public final class GoBackend implements Backend {
}
return super.onStartCommand(intent, flags, startId);
}
}
}
@@ -1,15 +1,16 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* 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;
@@ -21,6 +22,8 @@ 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;
@@ -34,23 +37,13 @@ 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 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("Unable to determine kernel module version");
return output.get(0);
}
@Override
public String getTypeName() { return "Kernel module"; }
@Override
public Config applyConfig(final Tunnel tunnel, final Config config) throws Exception {
if (tunnel.getState() == State.UP) {
@@ -93,6 +86,20 @@ public final class WgQuickBackend implements Backend {
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);
@@ -106,20 +113,21 @@ public final class WgQuickBackend implements Backend {
return getState(tunnel);
}
private void setStateInternal(final Tunnel tunnel, final Config config, final State state)
throws Exception {
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.toString().getBytes(StandardCharsets.UTF_8));
stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8));
}
String command = String.format("wg-quick %s '%s'",
state.toString().toLowerCase(), tempFile.getAbsolutePath());
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("Unable to configure tunnel (wg-quick returned " + result + ')');
throw new Exception(context.getString(R.string.tunnel_config_error, result));
}
}
@@ -1,7 +1,6 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.configStore;
@@ -1,7 +1,6 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.configStore;
@@ -9,6 +8,8 @@ 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;
@@ -40,9 +41,9 @@ public final class FileConfigStore implements ConfigStore {
Log.d(TAG, "Creating configuration for tunnel " + name);
final File file = fileFor(name);
if (!file.createNewFile())
throw new IOException("Configuration file " + file.getName() + " already exists");
throw new IOException(context.getString(R.string.config_file_exists_error, file.getName()));
try (final FileOutputStream stream = new FileOutputStream(file, false)) {
stream.write(config.toString().getBytes(StandardCharsets.UTF_8));
stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8));
}
return config;
}
@@ -52,7 +53,7 @@ public final class FileConfigStore implements ConfigStore {
Log.d(TAG, "Deleting configuration for tunnel " + name);
final File file = fileFor(name);
if (!file.delete())
throw new IOException("Cannot delete configuration file " + file.getName());
throw new IOException(context.getString(R.string.config_delete_error, file.getName()));
}
@Override
@@ -68,9 +69,9 @@ public final class FileConfigStore implements ConfigStore {
}
@Override
public Config load(final String name) throws IOException {
public Config load(final String name) throws BadConfigException, IOException {
try (final FileInputStream stream = new FileInputStream(fileFor(name))) {
return Config.from(stream);
return Config.parse(stream);
}
}
@@ -80,11 +81,11 @@ public final class FileConfigStore implements ConfigStore {
final File file = fileFor(name);
final File replacementFile = fileFor(replacement);
if (!replacementFile.createNewFile())
throw new IOException("Configuration for " + replacement + " already exists");
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("Cannot rename configuration file " + file.getName());
throw new IOException(context.getString(R.string.config_rename_error, file.getName()));
}
}
@@ -93,9 +94,9 @@ public final class FileConfigStore implements ConfigStore {
Log.d(TAG, "Saving configuration for tunnel " + name);
final File file = fileFor(name);
if (!file.isFile())
throw new FileNotFoundException("Configuration file " + file.getName() + " not found");
throw new FileNotFoundException(context.getString(R.string.config_not_found_error, file.getName()));
try (final FileOutputStream stream = new FileOutputStream(file, false)) {
stream.write(config.toString().getBytes(StandardCharsets.UTF_8));
stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8));
}
return config;
}
@@ -1,24 +1,34 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.databinding;
import android.databinding.BindingAdapter;
import android.databinding.ObservableList;
import android.databinding.adapters.ListenerUtil;
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.ListView;
import android.widget.TextView;
import com.wireguard.android.BR;
import com.wireguard.android.R;
import com.wireguard.util.Keyed;
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.
@@ -41,9 +51,10 @@ public final class BindingAdapters {
}
@BindingAdapter({"items", "layout"})
public static <E> void setItems(final LinearLayout view,
final ObservableList<E> oldList, final int oldLayoutId,
final ObservableList<E> newList, final int newLayoutId) {
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);
@@ -66,15 +77,41 @@ public final class BindingAdapters {
}
@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 ListView view,
final ObservableKeyedList<K, E> oldList, final int oldLayoutId,
final ObservableKeyedList<K, E> newList, final int newLayoutId) {
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") ObservableKeyedListAdapter<K, E> adapter =
(ObservableKeyedListAdapter<K, E>) view.getAdapter();
@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);
@@ -84,9 +121,11 @@ public final class BindingAdapters {
if (newList == null || newLayoutId == 0)
return;
if (adapter == null) {
adapter = new ObservableKeyedListAdapter<>(view.getContext(), newLayoutId, newList);
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);
}
@@ -96,4 +135,14 @@ public final class BindingAdapters {
final OnBeforeCheckedChangeListener listener) {
view.setOnBeforeCheckedChangeListener(listener);
}
@BindingAdapter("android:text")
public static void setText(final TextView view, final Optional<?> text) {
view.setText(text.map(Object::toString).orElse(""));
}
@BindingAdapter("android:text")
public static void setText(final TextView view, @Nullable final Iterable<InetNetwork> networks) {
view.setText(networks != null ? Attribute.join(networks) : "");
}
}
@@ -1,14 +1,14 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.databinding;
import android.databinding.DataBindingUtil;
import android.databinding.ObservableList;
import android.databinding.ViewDataBinding;
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;
@@ -16,6 +16,7 @@ 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.
@@ -26,7 +27,7 @@ class ItemChangeListener<T> {
private final ViewGroup container;
private final int layoutId;
private final LayoutInflater layoutInflater;
private ObservableList<T> list;
@Nullable private ObservableList<T> list;
ItemChangeListener(final ViewGroup container, final int layoutId) {
this.container = container;
@@ -34,17 +35,21 @@ class ItemChangeListener<T> {
layoutInflater = LayoutInflater.from(container.getContext());
}
private View getView(final int position, final View convertView) {
ViewDataBinding binding = DataBindingUtil.getBinding(convertView);
if (binding == null)
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(final ObservableList<T> newList) {
void setList(@Nullable final ObservableList<T> newList) {
if (list != null)
list.removeOnListChangedCallback(callback);
list = newList;
@@ -1,84 +1,91 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.databinding;
import android.content.Context;
import android.databinding.DataBindingUtil;
import android.databinding.ObservableList;
import android.databinding.ViewDataBinding;
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.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import com.wireguard.android.BR;
import com.wireguard.util.Keyed;
import com.wireguard.android.util.ObservableKeyedList;
import com.wireguard.util.Keyed;
import java.lang.ref.WeakReference;
/**
* A generic {@code ListAdapter} backed by a {@code ObservableKeyedList}.
* A generic {@code RecyclerView.Adapter} backed by a {@code ObservableKeyedList}.
*/
class ObservableKeyedListAdapter<K, E extends Keyed<? extends K>> extends BaseAdapter {
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;
private ObservableKeyedList<K, E> list;
@Nullable private ObservableKeyedList<K, E> list;
@Nullable private RowConfigurationHandler rowConfigurationHandler;
ObservableKeyedListAdapter(final Context context, final int layoutId,
final ObservableKeyedList<K, E> list) {
ObservableKeyedRecyclerViewAdapter(final Context context, final int layoutId,
final ObservableKeyedList<K, E> list) {
this.layoutId = layoutId;
layoutInflater = LayoutInflater.from(context);
setList(list);
}
@Override
public int getCount() {
return list != null ? list.size() : 0;
}
@Override
public E getItem(final int position) {
@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 View getView(final int position, final View convertView, final ViewGroup parent) {
ViewDataBinding binding = DataBindingUtil.getBinding(convertView);
if (binding == null)
binding = DataBindingUtil.inflate(layoutInflater, layoutId, parent, false);
binding.setVariable(BR.collection, list);
binding.setVariable(BR.key, getKey(position));
binding.setVariable(BR.item, getItem(position));
binding.executePendingBindings();
return binding.getRoot();
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 boolean hasStableIds() {
return true;
public ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
return new ViewHolder(DataBindingUtil.inflate(layoutInflater, layoutId, parent, false));
}
void setList(final ObservableKeyedList<K, E> newList) {
void setList(@Nullable final ObservableKeyedList<K, E> newList) {
if (list != null)
list.removeOnListChangedCallback(callback);
list = newList;
@@ -88,18 +95,26 @@ class ObservableKeyedListAdapter<K, E extends Keyed<? extends K>> extends BaseAd
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<ObservableKeyedListAdapter<?, E>> weakAdapter;
private final WeakReference<ObservableKeyedRecyclerViewAdapter<?, E>> weakAdapter;
private OnListChangedCallback(final ObservableKeyedListAdapter<?, E> adapter) {
private OnListChangedCallback(final ObservableKeyedRecyclerViewAdapter<?, E> adapter) {
weakAdapter = new WeakReference<>(adapter);
}
@Override
public void onChanged(final ObservableList<E> sender) {
final ObservableKeyedListAdapter adapter = weakAdapter.get();
final ObservableKeyedRecyclerViewAdapter adapter = weakAdapter.get();
if (adapter != null)
adapter.notifyDataSetChanged();
else
@@ -130,4 +145,15 @@ class ObservableKeyedListAdapter<K, E extends Keyed<? extends K>> extends BaseAd
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);
}
}
@@ -1,17 +1,31 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* 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.support.v4.app.Fragment;
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
@@ -19,12 +33,29 @@ import com.wireguard.android.model.Tunnel;
*/
public abstract class BaseFragment extends Fragment implements OnSelectedTunnelChangedListener {
private BaseActivity activity;
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);
@@ -44,8 +75,52 @@ public abstract class BaseFragment extends Fragment implements OnSelectedTunnelC
super.onDetach();
}
protected void setSelectedTunnel(final Tunnel tunnel) {
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);
}
}
}
@@ -1,58 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
*/
package com.wireguard.android.fragment;
import android.content.Context;
import android.databinding.DataBindingUtil;
import android.databinding.ViewDataBinding;
import android.support.design.widget.Snackbar;
import android.util.Log;
import android.view.View;
import com.wireguard.android.R;
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.ExceptionLoggers;
/**
* Helper method shared by TunnelListFragment and TunnelDetailFragment.
*/
public final class TunnelController {
private static final String TAG = "WireGuard/" + TunnelController.class.getSimpleName();
private TunnelController() {
// Prevent instantiation.
}
public static 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
tunnel = null;
if (tunnel == null) {
Log.e(TAG, "setChecked() from a null tunnel", new IllegalStateException("No tunnel"));
return;
}
tunnel.setState(State.of(checked)).whenComplete((state, throwable) -> {
if (throwable == null)
return;
final Context context = view.getContext();
final String error = ExceptionLoggers.unwrapMessage(throwable);
final int messageResId = checked ? R.string.error_up : R.string.error_down;
final String message = context.getString(messageResId, error);
Snackbar.make(view, message, Snackbar.LENGTH_LONG).show();
Log.e(TAG, message, throwable);
});
}
}
@@ -1,13 +1,12 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment;
import android.os.Bundle;
import android.support.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -17,21 +16,16 @@ import android.view.ViewGroup;
import com.wireguard.android.R;
import com.wireguard.android.databinding.TunnelDetailFragmentBinding;
import com.wireguard.android.model.Tunnel;
import com.wireguard.config.Config;
/**
* Fragment that shows details about a specific tunnel.
*/
public class TunnelDetailFragment extends BaseFragment {
private TunnelDetailFragmentBinding binding;
private void onConfigLoaded(final String name, final Config config) {
binding.setConfig(new Config.Observable(config, name));
}
@Nullable private TunnelDetailFragmentBinding binding;
@Override
public void onCreate(final Bundle savedInstanceState) {
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@@ -42,8 +36,8 @@ public class TunnelDetailFragment extends BaseFragment {
}
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
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();
@@ -57,19 +51,25 @@ public class TunnelDetailFragment extends BaseFragment {
}
@Override
public void onSelectedTunnelChanged(final Tunnel oldTunnel, final Tunnel newTunnel) {
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(a -> onConfigLoaded(newTunnel.getName(), a));
newTunnel.getConfigAsync().thenAccept(binding::setConfig);
}
@Override
public void onViewStateRestored(final Bundle savedInstanceState) {
public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
if (binding == null) {
return;
}
binding.setFragment(this);
onSelectedTunnelChanged(null, getSelectedTunnel());
super.onViewStateRestored(savedInstanceState);
}
}
@@ -1,16 +1,17 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* 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 android.support.annotation.NonNull;
import android.support.design.widget.Snackbar;
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;
@@ -24,29 +25,37 @@ 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.ExceptionLoggers;
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 {
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();
private TunnelEditorFragmentBinding binding;
private Tunnel tunnel;
@Nullable private TunnelEditorFragmentBinding binding;
@Nullable private Tunnel tunnel;
private void onConfigLoaded(final String name, final Config config) {
binding.setConfig(new Config.Observable(config, name));
private void onConfigLoaded(final Config config) {
if (binding != null) {
binding.setConfig(new ConfigProxy(config));
}
}
private void onConfigSaved(final Tunnel savedTunnel,
final Throwable throwable) {
@Nullable final Throwable throwable) {
final String message;
if (throwable == null) {
message = getString(R.string.config_save_success, savedTunnel.getName());
@@ -54,7 +63,7 @@ public class TunnelEditorFragment extends BaseFragment {
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
onFinished();
} else {
final String error = ExceptionLoggers.unwrapMessage(throwable);
final String error = ErrorMessages.get(throwable);
message = getString(R.string.config_save_error, savedTunnel.getName(), error);
Log.e(TAG, message, throwable);
if (binding != null) {
@@ -64,7 +73,7 @@ public class TunnelEditorFragment extends BaseFragment {
}
@Override
public void onCreate(final Bundle savedInstanceState) {
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@@ -75,8 +84,8 @@ public class TunnelEditorFragment extends BaseFragment {
}
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
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();
@@ -89,6 +98,15 @@ public class TunnelEditorFragment extends BaseFragment {
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();
@@ -103,6 +121,7 @@ public class TunnelEditorFragment extends BaseFragment {
}
// 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())
@@ -115,25 +134,27 @@ public class TunnelEditorFragment extends BaseFragment {
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_action_save:
final Config newConfig = new Config();
if (binding == null)
return false;
final Config newConfig;
try {
binding.getConfig().commitData(newConfig);
newConfig = binding.getConfig().resolve();
} catch (final Exception e) {
final String error = ExceptionLoggers.unwrapMessage(e);
final String tunnelName = tunnel == null ? binding.getConfig().getName() : tunnel.getName();
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.getConfig().getName());
Log.d(TAG, "Attempting to create new tunnel " + binding.getName());
final TunnelManager manager = Application.getTunnelManager();
manager.create(binding.getConfig().getName(), newConfig)
manager.create(binding.getName(), newConfig)
.whenComplete(this::onTunnelCreated);
} else if (!tunnel.getName().equals(binding.getConfig().getName())) {
Log.d(TAG, "Attempting to rename tunnel to " + binding.getConfig().getName());
tunnel.setName(binding.getConfig().getName())
} 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());
@@ -146,24 +167,39 @@ public class TunnelEditorFragment extends BaseFragment {
}
}
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(@NonNull final Bundle outState) {
outState.putParcelable(KEY_LOCAL_CONFIG, binding.getConfig());
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(final Tunnel oldTunnel, final Tunnel newTunnel) {
public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel,
@Nullable final Tunnel newTunnel) {
tunnel = newTunnel;
if (binding == null)
return;
binding.setConfig(new Config.Observable(null, null));
if (tunnel != null)
tunnel.getConfigAsync().thenAccept(a -> onConfigLoaded(tunnel.getName(), a));
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, final Throwable throwable) {
private void onTunnelCreated(final Tunnel newTunnel, @Nullable final Throwable throwable) {
final String message;
if (throwable == null) {
tunnel = newTunnel;
@@ -172,7 +208,7 @@ public class TunnelEditorFragment extends BaseFragment {
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
onFinished();
} else {
final String error = ExceptionLoggers.unwrapMessage(throwable);
final String error = ErrorMessages.get(throwable);
message = getString(R.string.tunnel_create_error, error);
Log.e(TAG, message, throwable);
if (binding != null) {
@@ -182,7 +218,7 @@ public class TunnelEditorFragment extends BaseFragment {
}
private void onTunnelRenamed(final Tunnel renamedTunnel, final Config newConfig,
final Throwable throwable) {
@Nullable final Throwable throwable) {
final String message;
if (throwable == null) {
message = getString(R.string.tunnel_rename_success, renamedTunnel.getName());
@@ -191,7 +227,7 @@ public class TunnelEditorFragment extends BaseFragment {
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 = ExceptionLoggers.unwrapMessage(throwable);
final String error = ErrorMessages.get(throwable);
message = getString(R.string.tunnel_rename_error, error);
Log.e(TAG, message, throwable);
if (binding != null) {
@@ -201,12 +237,18 @@ public class TunnelEditorFragment extends BaseFragment {
}
@Override
public void onViewStateRestored(final Bundle savedInstanceState) {
public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
if (binding == null) {
return;
}
binding.setFragment(this);
if (savedInstanceState == null) {
onSelectedTunnelChanged(null, getSelectedTunnel());
} else {
tunnel = getSelectedTunnel();
final Config.Observable config = savedInstanceState.getParcelable(KEY_LOCAL_CONFIG);
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);
@@ -216,4 +258,5 @@ public class TunnelEditorFragment extends BaseFragment {
super.onViewStateRestored(savedInstanceState);
}
}
@@ -1,7 +1,6 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment;
@@ -15,45 +14,50 @@ import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.OpenableColumns;
import android.support.annotation.NonNull;
import android.support.design.widget.Snackbar;
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.util.SparseBooleanArray;
import android.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AbsListView.MultiChoiceModeListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemLongClickListener;
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.ExceptionLoggers;
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.Collectors;
import java9.util.stream.IntStream;
import java9.util.stream.StreamSupport;
/**
@@ -64,22 +68,35 @@ public class TunnelListFragment extends BaseFragment {
private static final int REQUEST_IMPORT = 1;
private static final String TAG = "WireGuard/" + TunnelListFragment.class.getSimpleName();
private final MultiChoiceModeListener actionModeListener = new ActionModeListener();
private final ListViewCallbacks listViewCallbacks = new ListViewCallbacks();
private ActionMode actionMode;
private TunnelListFragmentBinding binding;
private final ActionModeListener actionModeListener = new ActionModeListener();
@Nullable private ActionMode actionMode;
@Nullable private TunnelListFragmentBinding binding;
public boolean collapseActionMenu() {
if (binding.createMenu.isExpanded()) {
if (binding != null && binding.createMenu.isExpanded()) {
binding.createMenu.collapse();
return true;
}
return false;
}
private void importTunnel(final Uri uri) {
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)
if (activity == null || uri == null)
return;
final ContentResolver contentResolver = activity.getContentResolver();
@@ -98,18 +115,18 @@ public class TunnelListFragment extends BaseFragment {
int idx = name.lastIndexOf('/');
if (idx >= 0) {
if (idx >= name.length() - 1)
throw new IllegalArgumentException("Illegal file name: " + name);
throw new IllegalArgumentException(getResources().getString(R.string.illegal_filename_error, name));
name = name.substring(idx + 1);
}
boolean isZip = name.toLowerCase().endsWith(".zip");
if (name.toLowerCase().endsWith(".conf"))
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("File must be .conf or .zip");
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, StandardCharsets.UTF_8));
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())
@@ -121,13 +138,13 @@ public class TunnelListFragment extends BaseFragment {
continue;
name = name.substring(name.lastIndexOf('/') + 1);
}
if (name.toLowerCase().endsWith(".conf"))
if (name.toLowerCase(Locale.ENGLISH).endsWith(".conf"))
name = name.substring(0, name.length() - ".conf".length());
else
continue;
Config config = null;
try {
config = Config.from(reader);
config = Config.parse(reader);
} catch (Exception e) {
throwables.add(e);
}
@@ -137,14 +154,14 @@ public class TunnelListFragment extends BaseFragment {
}
} else {
futureTunnels.add(Application.getTunnelManager().create(name,
Config.from(contentResolver.openInputStream(uri))).toCompletableFuture());
Config.parse(contentResolver.openInputStream(uri))).toCompletableFuture());
}
if (futureTunnels.isEmpty()) {
if (throwables.size() == 1)
throw throwables.get(0);
else if (throwables.isEmpty())
throw new IllegalArgumentException("No configurations found");
throw new IllegalArgumentException(getResources().getString(R.string.no_configs_error));
}
return CompletableFuture.allOf(futureTunnels.toArray(new CompletableFuture[futureTunnels.size()]));
@@ -171,31 +188,51 @@ public class TunnelListFragment extends BaseFragment {
}
@Override
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
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)
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 void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
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.setMultiChoiceModeListener(actionModeListener);
binding.tunnelList.setOnItemClickListener(listViewCallbacks);
binding.tunnelList.setOnItemLongClickListener(listViewCallbacks);
binding.tunnelList.setOnTouchListener(listViewCallbacks);
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();
}
@@ -206,6 +243,14 @@ public class TunnelListFragment extends BaseFragment {
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)
@@ -221,23 +266,42 @@ public class TunnelListFragment extends BaseFragment {
binding.createMenu.collapse();
}
@Override
public void onPause() {
binding.createMenu.collapse();
super.onPause();
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 onSelectedTunnelChanged(final Tunnel oldTunnel, final Tunnel newTunnel) {
// Do nothing.
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putIntegerArrayList("CHECKED_ITEMS", actionModeListener.getCheckedItems());
}
private void onTunnelDeletionFinished(final Integer count, final Throwable throwable) {
@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 = ExceptionLoggers.unwrapMessage(throwable);
final String error = ErrorMessages.get(throwable);
message = getResources().getQuantityString(R.plurals.delete_error, count, count, error);
Log.e(TAG, message, throwable);
}
@@ -250,7 +314,7 @@ public class TunnelListFragment extends BaseFragment {
String message = null;
for (final Throwable throwable : throwables) {
final String error = ExceptionLoggers.unwrapMessage(throwable);
final String error = ErrorMessages.get(throwable);
message = getString(R.string.import_error, error);
Log.e(TAG, message, throwable);
}
@@ -272,39 +336,77 @@ public class TunnelListFragment extends BaseFragment {
}
@Override
public void onViewStateRestored(final Bundle savedInstanceState) {
public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if (binding == null) {
return;
}
binding.setFragment(this);
binding.setTunnels(Application.getTunnelManager().getTunnels());
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 final class ActionModeListener implements MultiChoiceModeListener {
private Resources resources;
private AbsListView tunnelList;
private MultiselectableRelativeLayout viewForTunnel(final Tunnel tunnel, final List tunnels) {
return (MultiselectableRelativeLayout) binding.tunnelList.findViewHolderForAdapterPosition(tunnels.indexOf(tunnel)).itemView;
}
private IntStream getCheckedPositions() {
final SparseBooleanArray checkedItemPositions = tunnelList.getCheckedItemPositions();
return IntStream.range(0, checkedItemPositions.size())
.filter(checkedItemPositions::valueAt)
.map(checkedItemPositions::keyAt);
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:
// Must operate in two steps: positions change once we start deleting things.
final List<Tunnel> tunnelsToDelete = getCheckedPositions()
.mapToObj(pos -> (Tunnel) tunnelList.getItemAtPosition(pos))
.collect(Collectors.toList());
final CompletableFuture[] futures = StreamSupport.stream(tunnelsToDelete)
.map(Tunnel::delete)
.toArray(CompletableFuture[]::new);
CompletableFuture.allOf(futures)
.thenApply(x -> futures.length)
.whenComplete(TunnelListFragment.this::onTunnelDeletionFinished);
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;
}
@@ -313,10 +415,11 @@ public class TunnelListFragment extends BaseFragment {
@Override
public boolean onCreateActionMode(final ActionMode mode, final Menu menu) {
actionMode = mode;
if (getActivity() != null)
if (getActivity() != null) {
resources = getActivity().getResources();
tunnelList = binding.tunnelList;
}
mode.getMenuInflater().inflate(R.menu.tunnel_list_action_mode, menu);
binding.tunnelList.getAdapter().notifyDataSetChanged();
return true;
}
@@ -324,12 +427,8 @@ public class TunnelListFragment extends BaseFragment {
public void onDestroyActionMode(final ActionMode mode) {
actionMode = null;
resources = null;
}
@Override
public void onItemCheckedStateChanged(final ActionMode mode, final int position,
final long id, final boolean checked) {
updateTitle(mode);
checkedItems.clear();
binding.tunnelList.getAdapter().notifyDataSetChanged();
}
@Override
@@ -338,8 +437,37 @@ public class TunnelListFragment extends BaseFragment {
return false;
}
private void updateTitle(final ActionMode mode) {
final int count = (int) getCheckedPositions().count();
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 {
@@ -348,30 +476,4 @@ public class TunnelListFragment extends BaseFragment {
}
}
private final class ListViewCallbacks
implements OnItemClickListener, OnItemLongClickListener, OnTouchListener {
@Override
public void onItemClick(final AdapterView<?> parent, final View view,
final int position, final long id) {
setSelectedTunnel((Tunnel) parent.getItemAtPosition(position));
}
@Override
public boolean onItemLongClick(final AdapterView<?> parent, final View view,
final int position, final long id) {
if (actionMode != null)
return false;
if (binding != null)
binding.tunnelList.setItemChecked(position, true);
return true;
}
@Override
@SuppressLint("ClickableViewAccessibility")
public boolean onTouch(final View view, final MotionEvent motionEvent) {
if (binding != null)
binding.createMenu.collapse();
return false;
}
}
}
@@ -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);
}
}
@@ -1,20 +1,18 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.model;
import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
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.util.Keyed;
import com.wireguard.config.Config;
import com.wireguard.util.Keyed;
import java.util.regex.Pattern;
@@ -30,20 +28,20 @@ public class Tunnel extends BaseObservable implements Keyed<String> {
private static final Pattern NAME_PATTERN = Pattern.compile("[a-zA-Z0-9_=+.-]{1,15}");
private final TunnelManager manager;
private Config config;
@Nullable private Config config;
private String name;
private State state;
private Statistics statistics;
@Nullable private Statistics statistics;
Tunnel(@NonNull final TunnelManager manager, @NonNull final String name,
@Nullable final Config config, @NonNull final State state) {
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(@NonNull final CharSequence name) {
public static boolean isNameInvalid(final CharSequence name) {
return !NAME_PATTERN.matcher(name).matches();
}
@@ -52,6 +50,7 @@ public class Tunnel extends BaseObservable implements Keyed<String> {
}
@Bindable
@Nullable
public Config getConfig() {
if (config == null)
manager.getTunnelConfig(this).whenComplete(ExceptionLoggers.E);
@@ -80,21 +79,22 @@ public class Tunnel extends BaseObservable implements Keyed<String> {
}
public CompletionStage<State> getStateAsync() {
return manager.getTunnelState(this);
return TunnelManager.getTunnelState(this);
}
@Bindable
@Nullable
public Statistics getStatistics() {
// FIXME: Check age of statistics.
if (statistics == null)
manager.getTunnelStatistics(this).whenComplete(ExceptionLoggers.E);
TunnelManager.getTunnelStatistics(this).whenComplete(ExceptionLoggers.E);
return statistics;
}
public CompletionStage<Statistics> getStatisticsAsync() {
// FIXME: Check age of statistics.
if (statistics == null)
return manager.getTunnelStatistics(this);
return TunnelManager.getTunnelStatistics(this);
return CompletableFuture.completedFuture(statistics);
}
@@ -118,25 +118,26 @@ public class Tunnel extends BaseObservable implements Keyed<String> {
return state;
}
Statistics onStatisticsChanged(final Statistics statistics) {
@Nullable
Statistics onStatisticsChanged(@Nullable final Statistics statistics) {
this.statistics = statistics;
notifyPropertyChanged(BR.statistics);
return statistics;
}
public CompletionStage<Config> setConfig(@NonNull final Config config) {
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(@NonNull final String name) {
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(@NonNull final State state) {
public CompletionStage<State> setState(final State state) {
if (state != this.state)
return manager.setTunnelState(this, state);
return CompletableFuture.completedFuture(this.state);
@@ -1,7 +1,6 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.model;
@@ -9,17 +8,17 @@ package com.wireguard.android.model;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.support.annotation.NonNull;
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.ObservableKeyedList;
import com.wireguard.android.util.ObservableSortedKeyedArrayList;
import com.wireguard.android.util.ObservableSortedKeyedList;
import com.wireguard.config.Config;
@@ -46,28 +45,39 @@ public final class TunnelManager extends BaseObservable {
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 ObservableSortedKeyedList<String, Tunnel> tunnels =
new ObservableSortedKeyedArrayList<>(COMPARATOR);
private Tunnel lastUsedTunnel;
private boolean haveLoaded;
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;
}
private Tunnel addToList(final String name, final Config config, final State state) {
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(@NonNull final String name, final Config config) {
public CompletionStage<Tunnel> create(final String name, @Nullable final Config config) {
if (Tunnel.isNameInvalid(name))
return CompletableFuture.failedFuture(new IllegalArgumentException("Invalid name"));
return CompletableFuture.failedFuture(new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)));
if (tunnels.containsKey(name)) {
final String message = "Tunnel " + name + " already exists";
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))
@@ -103,6 +113,7 @@ public final class TunnelManager extends BaseObservable {
}
@Bindable
@Nullable
public Tunnel getLastUsedTunnel() {
return lastUsedTunnel;
}
@@ -112,18 +123,8 @@ public final class TunnelManager extends BaseObservable {
.thenApply(tunnel::onConfigChanged);
}
CompletionStage<State> getTunnelState(final Tunnel tunnel) {
return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getState(tunnel))
.thenApply(tunnel::onStateChanged);
}
CompletionStage<Statistics> getTunnelStatistics(final Tunnel tunnel) {
return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getStatistics(tunnel))
.thenApply(tunnel::onStatisticsChanged);
}
public ObservableKeyedList<String, Tunnel> getTunnels() {
return tunnels;
public CompletableFuture<ObservableSortedKeyedList<String, Tunnel>> getTunnels() {
return completableTunnels;
}
public void onCreate() {
@@ -153,6 +154,8 @@ public final class TunnelManager extends BaseObservable {
f.completeExceptionally(t);
}
});
completableTunnels.complete(tunnels);
}
public void refreshTunnelStates() {
@@ -191,7 +194,7 @@ public final class TunnelManager extends BaseObservable {
Application.getSharedPreferences().edit().putStringSet(KEY_RUNNING_TUNNELS, runningTunnels).apply();
}
private void setLastUsedTunnel(final Tunnel tunnel) {
private void setLastUsedTunnel(@Nullable final Tunnel tunnel) {
if (tunnel == lastUsedTunnel)
return;
lastUsedTunnel = tunnel;
@@ -211,9 +214,9 @@ public final class TunnelManager extends BaseObservable {
CompletionStage<String> setTunnelName(final Tunnel tunnel, final String name) {
if (Tunnel.isNameInvalid(name))
return CompletableFuture.failedFuture(new IllegalArgumentException("Invalid name"));
return CompletableFuture.failedFuture(new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)));
if (tunnels.containsKey(name)) {
final String message = "Tunnel " + name + " already exists";
final String message = context.getString(R.string.tunnel_error_already_exists, name);
return CompletableFuture.failedFuture(new IllegalArgumentException(message));
}
final State originalState = tunnel.getState();
@@ -256,7 +259,7 @@ public final class TunnelManager extends BaseObservable {
public static final class IntentReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context context, final Intent intent) {
public void onReceive(final Context context, @Nullable final Intent intent) {
final TunnelManager manager = Application.getTunnelManager();
if (intent == null)
return;
@@ -286,10 +289,12 @@ public final class TunnelManager extends BaseObservable {
final String tunnelName = intent.getStringExtra("tunnel");
if (tunnelName == null)
return;
final Tunnel tunnel = manager.getTunnels().get(tunnelName);
if (tunnel == null)
return;
manager.setTunnelState(tunnel, state);
manager.getTunnels().thenAccept(tunnels -> {
final Tunnel tunnel = tunnels.get(tunnelName);
if (tunnel == null)
return;
manager.setTunnelState(tunnel, state);
});
}
}
}
@@ -1,7 +1,6 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference;
@@ -10,14 +9,15 @@ import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Environment;
import android.support.design.widget.Snackbar;
import android.support.v7.preference.Preference;
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.ExceptionLoggers;
import com.wireguard.android.util.ErrorMessages;
import com.wireguard.android.util.FragmentUtils;
import java.io.BufferedReader;
@@ -33,7 +33,7 @@ import java.io.InputStreamReader;
public class LogExporterPreference extends Preference {
private static final String TAG = "WireGuard/" + LogExporterPreference.class.getSimpleName();
private String exportedFilePath;
@Nullable private String exportedFilePath;
public LogExporterPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
@@ -44,7 +44,8 @@ public class LogExporterPreference extends Preference {
final File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
final File file = new File(path, "wireguard-log.txt");
if (!path.isDirectory() && !path.mkdirs())
throw new IOException("Cannot create output directory");
throw new IOException(
getContext().getString(R.string.create_output_dir_error));
/* We would like to simply run `builder.redirectOutput(file);`, but this is API 26.
* Instead we have to do this dance, since logcat appends.
@@ -73,9 +74,9 @@ public class LogExporterPreference extends Preference {
}).whenComplete(this::exportLogComplete);
}
private void exportLogComplete(final String filePath, final Throwable throwable) {
private void exportLogComplete(final String filePath, @Nullable final Throwable throwable) {
if (throwable != null) {
final String error = ExceptionLoggers.unwrapMessage(throwable);
final String error = ErrorMessages.get(throwable);
final String message = getContext().getString(R.string.log_export_error, error);
Log.e(TAG, message, throwable);
Snackbar.make(
@@ -97,7 +98,7 @@ public class LogExporterPreference extends Preference {
@Override
public CharSequence getTitle() {
return getContext().getString(R.string.log_exporter_title);
return getContext().getString(R.string.log_export_title);
}
@Override
@@ -1,14 +1,13 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.v7.preference.Preference;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import android.util.AttributeSet;
import com.wireguard.android.Application;
@@ -43,7 +42,7 @@ public class ToolsInstallerPreference extends Preference {
Application.getAsyncWorker().supplyAsync(Application.getToolsInstaller()::areInstalled).whenComplete(this::onCheckResult);
}
private void onCheckResult(final int state, final Throwable throwable) {
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)
@@ -62,7 +61,7 @@ public class ToolsInstallerPreference extends Preference {
Application.getAsyncWorker().supplyAsync(Application.getToolsInstaller()::install).whenComplete(this::onInstallResult);
}
private void onInstallResult(final Integer result, final Throwable throwable) {
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))
@@ -73,7 +72,7 @@ public class ToolsInstallerPreference extends Preference {
setState(State.FAILURE);
}
private void setState(@NonNull final State state) {
private void setState(final State state) {
if (this.state == state)
return;
this.state = state;
@@ -1,37 +1,42 @@
/*
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* 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 android.support.v7.preference.Preference;
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 {
private String versionSummary;
@Nullable private String versionSummary;
public VersionPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
Application.onHaveBackend(backend -> {
versionSummary = getContext().getString(R.string.version_summary_checking, backend.getTypeName().toLowerCase());
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.getTypeName(), version)
: getContext().getString(R.string.version_summary_unknown, backend.getTypeName().toLowerCase());
? 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;
@@ -46,7 +51,10 @@ public class VersionPreference extends Preference {
protected void onClick() {
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://www.wireguard.com/"));
getContext().startActivity(intent);
try {
getContext().startActivity(intent);
} catch (final ActivityNotFoundException ignored) {
}
}
}
@@ -1,7 +1,6 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference;
@@ -10,15 +9,16 @@ import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Environment;
import android.support.design.widget.Snackbar;
import android.support.v7.preference.Preference;
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.ExceptionLoggers;
import com.wireguard.android.util.ErrorMessages;
import com.wireguard.android.util.FragmentUtils;
import com.wireguard.config.Config;
@@ -40,19 +40,23 @@ import java9.util.concurrent.CompletableFuture;
public class ZipExporterPreference extends Preference {
private static final String TAG = "WireGuard/" + ZipExporterPreference.class.getSimpleName();
private String exportedFilePath;
@Nullable private String exportedFilePath;
public ZipExporterPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
private void exportZip() {
final List<Tunnel> tunnels = new ArrayList<>(Application.getTunnelManager().getTunnels());
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("No tunnels exist"));
exportZipComplete(null, new IllegalArgumentException(
getContext().getString(R.string.no_tunnels_error)));
return;
}
CompletableFuture.allOf(futureConfigs.toArray(new CompletableFuture[futureConfigs.size()]))
@@ -62,12 +66,13 @@ public class ZipExporterPreference extends Preference {
final File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
final File file = new File(path, "wireguard-export.zip");
if (!path.isDirectory() && !path.mkdirs())
throw new IOException("Cannot create output directory");
throw new IOException(
getContext().getString(R.string.create_output_dir_error));
try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(file))) {
for (int i = 0; i < futureConfigs.size(); ++i) {
zip.putNextEntry(new ZipEntry(tunnels.get(i).getName() + ".conf"));
zip.write(futureConfigs.get(i).getNow(null).
toString().getBytes(StandardCharsets.UTF_8));
toWgQuickString().getBytes(StandardCharsets.UTF_8));
}
zip.closeEntry();
} catch (final Exception e) {
@@ -79,9 +84,9 @@ public class ZipExporterPreference extends Preference {
}).whenComplete(this::exportZipComplete));
}
private void exportZipComplete(final String filePath, final Throwable throwable) {
private void exportZipComplete(@Nullable final String filePath, @Nullable final Throwable throwable) {
if (throwable != null) {
final String error = ExceptionLoggers.unwrapMessage(throwable);
final String error = ErrorMessages.get(throwable);
final String message = getContext().getString(R.string.zip_export_error, error);
Log.e(TAG, message, throwable);
Snackbar.make(
@@ -103,7 +108,7 @@ public class ZipExporterPreference extends Preference {
@Override
public CharSequence getTitle() {
return getContext().getString(R.string.zip_exporter_title);
return getContext().getString(R.string.zip_export_title);
}
@Override
@@ -1,7 +1,6 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
@@ -1,7 +1,6 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
@@ -9,7 +8,7 @@ package com.wireguard.android.util;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.support.design.widget.Snackbar;
import com.google.android.material.snackbar.Snackbar;
import android.view.View;
import android.widget.TextView;
@@ -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;
}
}
@@ -1,14 +1,13 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.support.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.Log;
import java9.util.concurrent.CompletionException;
import java9.util.function.BiConsumer;
/**
@@ -21,30 +20,14 @@ public enum ExceptionLoggers implements BiConsumer<Object, Throwable> {
E(Log.ERROR);
private static final String TAG = "WireGuard/" + ExceptionLoggers.class.getSimpleName();
private final int priority;
ExceptionLoggers(final int priority) {
this.priority = priority;
}
public static Throwable unwrap(final Throwable throwable) {
if (throwable instanceof CompletionException && throwable.getCause() != null)
return throwable.getCause();
return throwable;
}
@NonNull
public static String unwrapMessage(Throwable throwable) {
throwable = unwrap(throwable);
final String message = throwable.getMessage();
if (message != null)
return message;
return throwable.getClass().getSimpleName();
}
@Override
public void accept(final Object result, final Throwable throwable) {
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)
@@ -1,17 +1,19 @@
/*
* Copyright © 2018 Harsh Shandilya <msfjarvis@gmail.com>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.content.Context;
import android.support.v7.preference.Preference;
import androidx.preference.Preference;
import android.view.ContextThemeWrapper;
import com.wireguard.android.activity.SettingsActivity;
public class FragmentUtils {
public final class FragmentUtils {
private FragmentUtils() {
// Prevent instantiation
}
public static SettingsActivity getPrefActivity(final Preference preference) {
final Context context = preference.getContext();
@@ -1,13 +1,12 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.databinding.ObservableArrayList;
import android.support.annotation.NonNull;
import androidx.databinding.ObservableArrayList;
import androidx.annotation.Nullable;
import com.wireguard.util.Keyed;
@@ -25,28 +24,28 @@ import java.util.Objects;
public class ObservableKeyedArrayList<K, E extends Keyed<? extends K>>
extends ObservableArrayList<E> implements ObservableKeyedList<K, E> {
@Override
public boolean add(final E e) {
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, final E e) {
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(@NonNull final Collection<? extends E> c) {
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, @NonNull final Collection<? extends E> c) {
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);
@@ -65,12 +64,14 @@ public class ObservableKeyedArrayList<K, E extends Keyed<? extends K>>
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);
@@ -100,7 +101,7 @@ public class ObservableKeyedArrayList<K, E extends Keyed<? extends K>>
}
@Override
public E set(final int index, final E e) {
public E set(final int index, @Nullable final E e) {
if (e == null)
throw new NullPointerException("Trying to set a null key");
return super.set(index, e);
@@ -1,12 +1,11 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.databinding.ObservableList;
import androidx.databinding.ObservableList;
import com.wireguard.util.Keyed;
import com.wireguard.util.KeyedList;
@@ -1,12 +1,11 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.support.annotation.NonNull;
import androidx.annotation.Nullable;
import com.wireguard.util.Keyed;
import com.wireguard.util.SortedKeyedList;
@@ -29,7 +28,7 @@ import java.util.Spliterator;
public class ObservableSortedKeyedArrayList<K, E extends Keyed<? extends K>>
extends ObservableKeyedArrayList<K, E> implements ObservableSortedKeyedList<K, E> {
private final Comparator<? super K> comparator;
@Nullable private final Comparator<? super K> comparator;
private final transient KeyList<K, E> keyList = new KeyList<>(this);
@SuppressWarnings("WeakerAccess")
@@ -75,7 +74,7 @@ public class ObservableSortedKeyedArrayList<K, E extends Keyed<? extends K>>
}
@Override
public boolean addAll(@NonNull final Collection<? extends E> c) {
public boolean addAll(final Collection<? extends E> c) {
boolean didChange = false;
for (final E e : c)
if (add(e))
@@ -84,12 +83,13 @@ public class ObservableSortedKeyedArrayList<K, E extends Keyed<? extends K>>
}
@Override
public boolean addAll(int index, @NonNull final Collection<? extends E> c) {
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;
@@ -128,7 +128,6 @@ public class ObservableSortedKeyedArrayList<K, E extends Keyed<? extends K>>
}
@Override
@NonNull
public Set<K> keySet() {
return keyList;
}
@@ -168,7 +167,6 @@ public class ObservableSortedKeyedArrayList<K, E extends Keyed<? extends K>>
}
@Override
@NonNull
public Collection<E> values() {
return this;
}
@@ -1,7 +1,6 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
@@ -1,14 +1,15 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* 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.annotation.Nullable;
import android.util.Log;
import com.wireguard.android.BuildConfig;
import com.wireguard.android.R;
import java.io.BufferedReader;
@@ -29,23 +30,25 @@ public class RootShell {
private static final String SU = "su";
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();
private final String preamble;
private Process process;
private BufferedReader stderr;
private OutputStreamWriter stdin;
private BufferedReader stdout;
@Nullable private Process process;
@Nullable private BufferedReader stderr;
@Nullable private OutputStreamWriter stdin;
@Nullable private BufferedReader stdout;
public RootShell(final Context context) {
deviceNotRootedMessage = context.getString(R.string.error_root);
final File cacheDir = context.getCacheDir();
localBinaryDir = new File(cacheDir, "bin");
localTemporaryDir = new File(cacheDir, "tmp");
preamble = String.format("export CALLING_PACKAGE=com.wireguard.android PATH=\"%s:$PATH\" TMPDIR='%s'; id -u\n",
localBinaryDir, localTemporaryDir);
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) {
@@ -80,7 +83,7 @@ public class RootShell {
* @param command Command to run as root.
* @return The exit value of the command.
*/
public int run(final Collection<String> output, final String command)
public int run(@Nullable final Collection<String> output, final String command)
throws IOException, NoRootException {
synchronized (lock) {
/* Start inside synchronized block to prevent a concurrent call to stop(). */
@@ -120,9 +123,9 @@ public class RootShell {
}
}
if (markersSeen != 4)
throw new IOException("Expected 4 markers, received " + markersSeen);
throw new IOException(context.getString(R.string.shell_marker_count_error, markersSeen));
if (errnoStdout != errnoStderr)
throw new IOException("Unable to read exit status");
throw new IOException(context.getString(R.string.shell_exit_status_read_error));
Log.v(TAG, "exit: " + errnoStdout);
return errnoStdout;
}
@@ -135,9 +138,9 @@ public class RootShell {
if (isRunning())
return;
if (!localBinaryDir.isDirectory() && !localBinaryDir.mkdirs())
throw new FileNotFoundException("Could not create local binary directory");
throw new FileNotFoundException(context.getString(R.string.create_bin_dir_error));
if (!localTemporaryDir.isDirectory() && !localTemporaryDir.mkdirs())
throw new FileNotFoundException("Could not create local temporary directory");
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");
@@ -167,7 +170,7 @@ public class RootShell {
if (line.contains("Permission denied"))
throw new NoRootException(deviceNotRootedMessage);
}
throw new IOException("Shell failed to start: " + process.exitValue());
throw new IOException(context.getString(R.string.shell_start_error, process.exitValue()));
}
} catch (final IOException | NoRootException e) {
stop();
@@ -1,6 +1,6 @@
/*
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
@@ -1,17 +1,18 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* 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.annotation.Nullable;
import android.system.OsConstants;
import android.util.Log;
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;
@@ -26,11 +27,10 @@ import java.util.List;
public final class ToolsInstaller {
public static final int ERROR = 0x0;
public static final int YES = 0x1;
public static final int NO = 0x2;
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 = {
{"libwg.so", "wg"},
{"libwg-quick.so", "wg-quick"},
@@ -39,20 +39,23 @@ public final class ToolsInstaller {
new File("/system/xbin"),
new File("/system/bin"),
};
private static final File INSTALL_DIR = getInstallDir();
@Nullable private static final File INSTALL_DIR = getInstallDir();
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 File nativeLibraryDir;
private Boolean areToolsAvailable;
private Boolean installAsMagiskModule;
@Nullable private Boolean areToolsAvailable;
@Nullable private Boolean installAsMagiskModule;
public ToolsInstaller(final Context context) {
localBinaryDir = new File(context.getCacheDir(), "bin");
nativeLibraryDir = new File(context.getApplicationInfo().nativeLibraryDir);
this.context = context;
}
@Nullable
private static File getInstallDir() {
final String path = System.getenv("PATH");
if (path == null)
@@ -102,20 +105,33 @@ public final class ToolsInstaller {
}
}
if (!areToolsAvailable)
throw new FileNotFoundException("Required tools unavailable");
throw new FileNotFoundException(
context.getString(R.string.tools_unavailable_error));
}
}
private boolean willInstallAsMagiskModule() {
synchronized (lock) {
if (installAsMagiskModule == null) {
try {
installAsMagiskModule = Application.getRootShell().run(null, "[ -d /sbin/.core/mirror -a -d /sbin/.core/img -a ! -f /cache/.disable_magisk ]") == OsConstants.EXIT_SUCCESS;
} catch (final Exception ignored) {
installAsMagiskModule = false;
}
}
return installAsMagiskModule;
public int install() throws NoRootException {
return willInstallAsMagiskModule() ? installMagisk() : installSystem();
}
private int installMagisk() throws NoRootException {
final StringBuilder script = new StringBuilder("set -ex; ");
script.append("trap 'rm -rf /sbin/.magisk/img/wireguard' INT TERM EXIT; ");
script.append(String.format("rm -rf /sbin/.magisk/img/wireguard/; mkdir -p /sbin/.magisk/img/wireguard%s; ", INSTALL_DIR));
script.append(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[] names : EXECUTABLES) {
final File destination = new File("/sbin/.magisk/img/wireguard" + INSTALL_DIR, names[1]);
script.append(String.format("cp '%s' '%s'; chmod 755 '%s'; chcon 'u:object_r:system_file:s0' '%s' || true; ",
new File(nativeLibraryDir, names[0]), destination, destination, destination));
}
script.append("trap - INT TERM EXIT;");
try {
return Application.getRootShell().run(null, script.toString()) == 0 ? YES | MAGISK : ERROR;
} catch (final IOException ignored) {
return ERROR;
}
}
@@ -136,31 +152,6 @@ public final class ToolsInstaller {
}
}
private int installMagisk() throws NoRootException {
final StringBuilder script = new StringBuilder("set -ex; ");
script.append("trap 'rm -rf /sbin/.core/img/wireguard' INT TERM EXIT; ");
script.append(String.format("rm -rf /sbin/.core/img/wireguard/; mkdir -p /sbin/.core/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/.core/img/wireguard/module.prop; ", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
script.append("touch /sbin/.core/img/wireguard/auto_mount; ");
for (final String[] names : EXECUTABLES) {
final File destination = new File("/sbin/.core/img/wireguard" + INSTALL_DIR, names[1]);
script.append(String.format("cp '%s' '%s'; chmod 755 '%s'; restorecon '%s' || true; ",
new File(nativeLibraryDir, names[0]), destination, destination, destination));
}
script.append("trap - INT TERM EXIT;");
try {
return Application.getRootShell().run(null, script.toString()) == 0 ? YES | MAGISK : ERROR;
} catch (final IOException ignored) {
return ERROR;
}
}
public int install() throws NoRootException {
return willInstallAsMagiskModule() ? installMagisk() : installSystem();
}
public int symlink() throws NoRootException {
final StringBuilder script = new StringBuilder("set -x; ");
for (final String[] names : EXECUTABLES) {
@@ -183,4 +174,17 @@ public final class ToolsInstaller {
return OsConstants.EXIT_FAILURE;
}
}
private boolean willInstallAsMagiskModule() {
synchronized (lock) {
if (installAsMagiskModule == null) {
try {
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;
}
}
return installAsMagiskModule;
}
}
}
@@ -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];
}
}
}
@@ -1,16 +1,16 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* 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.KeyEncoding;
import com.wireguard.crypto.Key;
/**
* InputFilter for entering WireGuard private/public keys encoded with base64.
@@ -25,6 +25,7 @@ public class KeyInputFilter implements InputFilter {
return new KeyInputFilter();
}
@Nullable
@Override
public CharSequence filter(final CharSequence source,
final int sStart, final int sEnd,
@@ -38,9 +39,9 @@ public class KeyInputFilter implements InputFilter {
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 < KeyEncoding.KEY_LENGTH_BASE64 && isAllowed(c)) ||
(dIndex + 1 == KeyEncoding.KEY_LENGTH_BASE64 && c == '=')) &&
dLength + (sIndex - sStart) < KeyEncoding.KEY_LENGTH_BASE64) {
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)
@@ -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);
}
}
@@ -1,11 +1,11 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* 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;
@@ -25,6 +25,7 @@ public class NameInputFilter implements InputFilter {
return new NameInputFilter();
}
@Nullable
@Override
public CharSequence filter(final CharSequence source,
final int sStart, final int sEnd,
@@ -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;
}
}
@@ -1,5 +1,6 @@
/*
* Copyright © 2013 The Android Open Source Project
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
@@ -7,22 +8,23 @@ 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;
private OnBeforeCheckedChangeListener listener;
@SuppressWarnings({"SameParameterValue", "WeakerAccess"})
public ToggleSwitch(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
@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;
@@ -1,97 +0,0 @@
/*
* Copyright © 2014 Jerzy Chalupski
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
*/
package com.wireguard.android.widget.fab;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.Shape;
import android.support.annotation.ColorRes;
import android.support.annotation.DrawableRes;
import android.support.v4.content.ContextCompat;
import android.util.AttributeSet;
import com.wireguard.android.R;
public class AddFloatingActionButton extends FloatingActionButton {
int mPlusColor;
public AddFloatingActionButton(final Context context) {
this(context, null);
}
public AddFloatingActionButton(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
public AddFloatingActionButton(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
}
@Override
void init(final Context context, final AttributeSet attributeSet) {
final TypedArray attr = context.obtainStyledAttributes(attributeSet, R.styleable.AddFloatingActionButton, 0, 0);
mPlusColor = attr.getColor(R.styleable.AddFloatingActionButton_fab_plusIconColor, FloatingActionButton.getColorFromTheme(context, android.R.attr.colorBackground, android.R.color.white));
attr.recycle();
super.init(context, attributeSet);
}
/**
* @return the current Color of plus icon.
*/
public int getPlusColor() {
return mPlusColor;
}
public void setPlusColor(final int color) {
if (mPlusColor != color) {
mPlusColor = color;
updateBackground();
}
}
public void setPlusColorResId(@ColorRes final int plusColor) {
setPlusColor(ContextCompat.getColor(getContext(), plusColor));
}
@Override
public void setIcon(@DrawableRes final int icon) {
throw new UnsupportedOperationException("Use FloatingActionButton if you want to use custom icon");
}
@Override
Drawable getIconDrawable() {
final float iconSize = getDimension(R.dimen.fab_icon_size);
final float iconHalfSize = iconSize / 2f;
final float plusSize = getDimension(R.dimen.fab_plus_icon_size);
final float plusHalfStroke = getDimension(R.dimen.fab_plus_icon_stroke) / 2f;
final float plusOffset = (iconSize - plusSize) / 2f;
final Shape shape = new Shape() {
@Override
public void draw(final Canvas canvas, final Paint paint) {
canvas.drawRect(plusOffset, iconHalfSize - plusHalfStroke, iconSize - plusOffset, iconHalfSize + plusHalfStroke, paint);
canvas.drawRect(iconHalfSize - plusHalfStroke, plusOffset, iconHalfSize + plusHalfStroke, iconSize - plusOffset, paint);
}
};
final ShapeDrawable drawable = new ShapeDrawable(shape);
final Paint paint = drawable.getPaint();
paint.setColor(mPlusColor);
paint.setStyle(Style.FILL);
paint.setAntiAlias(true);
return drawable;
}
}
@@ -1,446 +0,0 @@
/*
* Copyright © 2014 Jerzy Chalupski
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
*/
package com.wireguard.android.widget.fab;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.graphics.Shader;
import android.graphics.Shader.TileMode;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.ShapeDrawable.ShaderFactory;
import android.graphics.drawable.StateListDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.support.annotation.ColorRes;
import android.support.annotation.DimenRes;
import android.support.annotation.DrawableRes;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.AppCompatImageButton;
import android.util.AttributeSet;
import android.widget.TextView;
import com.wireguard.android.R;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class FloatingActionButton extends AppCompatImageButton {
public static final int SIZE_NORMAL = 0;
public static final int SIZE_MINI = 1;
int mColorNormal;
int mColorPressed;
int mColorDisabled;
String mTitle;
boolean mStrokeVisible;
@DrawableRes
private int mIcon;
private Drawable mIconDrawable;
private int mSize;
private float mCircleSize;
private float mShadowRadius;
private float mShadowOffset;
private int mDrawableSize;
public FloatingActionButton(final Context context) {
this(context, null);
}
public FloatingActionButton(final Context context, final AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public FloatingActionButton(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
public static int getColorFromTheme(final Context context, final int themeResource, @ColorRes final int fallback) {
final TypedArray a = context.obtainStyledAttributes(new int[]{themeResource});
try {
return a.getColor(0, ContextCompat.getColor(context, fallback));
} finally {
a.recycle();
}
}
void init(final Context context, final AttributeSet attributeSet) {
final TypedArray attr = context.obtainStyledAttributes(attributeSet,
R.styleable.FloatingActionButton, 0, 0);
mColorNormal = attr.getColor(R.styleable.FloatingActionButton_fab_colorNormal,
getColorFromTheme(context, android.R.attr.colorAccent, android.R.color.holo_blue_bright));
mColorPressed = attr.getColor(R.styleable.FloatingActionButton_fab_colorPressed,
darkenOrLightenColor(mColorNormal)); //TODO(msf): use getColorForState on the accent color from theme instead to get darker states
mColorDisabled = attr.getColor(R.styleable.FloatingActionButton_fab_colorDisabled,
ContextCompat.getColor(context, android.R.color.darker_gray)); //TODO(msf): load from theme
mSize = attr.getInt(R.styleable.FloatingActionButton_fab_size, SIZE_NORMAL);
mIcon = attr.getResourceId(R.styleable.FloatingActionButton_fab_icon, 0);
mTitle = attr.getString(R.styleable.FloatingActionButton_fab_title);
mStrokeVisible = attr.getBoolean(R.styleable.FloatingActionButton_fab_stroke_visible, true);
attr.recycle();
updateCircleSize();
mShadowRadius = getDimension(R.dimen.fab_shadow_radius);
mShadowOffset = getDimension(R.dimen.fab_shadow_offset);
updateDrawableSize();
updateBackground();
}
private void updateDrawableSize() {
mDrawableSize = (int) (mCircleSize + 2 * mShadowRadius);
}
private void updateCircleSize() {
mCircleSize = getDimension(mSize == SIZE_NORMAL ? R.dimen.fab_size_normal : R.dimen.fab_size_mini);
}
@FAB_SIZE
public int getSize() {
return mSize;
}
public void setSize(@FAB_SIZE final int size) {
if (size != SIZE_MINI && size != SIZE_NORMAL) {
throw new IllegalArgumentException("Use @FAB_SIZE constants only!");
}
if (mSize != size) {
mSize = size;
updateCircleSize();
updateDrawableSize();
updateBackground();
}
}
public void setIcon(@DrawableRes final int icon) {
if (mIcon != icon) {
mIcon = icon;
mIconDrawable = null;
updateBackground();
}
}
/**
* @return the current Color for normal state.
*/
public int getColorNormal() {
return mColorNormal;
}
public void setColorNormal(final int color) {
if (mColorNormal != color) {
mColorNormal = color;
updateBackground();
}
}
public void setColorNormalResId(@ColorRes final int colorNormal) {
setColorNormal(ContextCompat.getColor(getContext(), colorNormal));
}
/**
* @return the current color for pressed state.
*/
public int getColorPressed() {
return mColorPressed;
}
public void setColorPressed(final int color) {
if (mColorPressed != color) {
mColorPressed = color;
updateBackground();
}
}
public void setColorPressedResId(@ColorRes final int colorPressed) {
setColorPressed(ContextCompat.getColor(getContext(), colorPressed));
}
/**
* @return the current color for disabled state.
*/
public int getColorDisabled() {
return mColorDisabled;
}
public void setColorDisabled(final int color) {
if (mColorDisabled != color) {
mColorDisabled = color;
updateBackground();
}
}
public void setColorDisabledResId(@ColorRes final int colorDisabled) {
setColorDisabled(ContextCompat.getColor(getContext(), colorDisabled));
}
public boolean isStrokeVisible() {
return mStrokeVisible;
}
public void setStrokeVisible(final boolean visible) {
if (mStrokeVisible != visible) {
mStrokeVisible = visible;
updateBackground();
}
}
float getDimension(@DimenRes final int id) {
return getResources().getDimension(id);
}
TextView getLabelView() {
return (TextView) getTag(R.id.fab_label);
}
public String getTitle() {
return mTitle;
}
public void setTitle(final String title) {
mTitle = title;
final TextView label = getLabelView();
if (label != null) {
label.setText(title);
}
}
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(mDrawableSize, mDrawableSize);
}
void updateBackground() {
final float strokeWidth = getDimension(R.dimen.fab_stroke_width);
final float halfStrokeWidth = strokeWidth / 2f;
final LayerDrawable layerDrawable = new LayerDrawable(
new Drawable[]{
//TODO(msf); replace these pngs with programatic elevation
getResources().getDrawable(mSize == SIZE_NORMAL ? R.drawable.fab_bg_normal : R.drawable.fab_bg_mini, null),
createFillDrawable(strokeWidth),
createOuterStrokeDrawable(strokeWidth),
getIconDrawable()
});
final int iconOffset = (int) (mCircleSize - getDimension(R.dimen.fab_icon_size)) / 2;
final int circleInsetHorizontal = (int) (mShadowRadius);
final int circleInsetTop = (int) (mShadowRadius - mShadowOffset);
final int circleInsetBottom = (int) (mShadowRadius + mShadowOffset);
layerDrawable.setLayerInset(1,
circleInsetHorizontal,
circleInsetTop,
circleInsetHorizontal,
circleInsetBottom);
layerDrawable.setLayerInset(2,
(int) (circleInsetHorizontal - halfStrokeWidth),
(int) (circleInsetTop - halfStrokeWidth),
(int) (circleInsetHorizontal - halfStrokeWidth),
(int) (circleInsetBottom - halfStrokeWidth));
layerDrawable.setLayerInset(3,
circleInsetHorizontal + iconOffset,
circleInsetTop + iconOffset,
circleInsetHorizontal + iconOffset,
circleInsetBottom + iconOffset);
setBackground(layerDrawable);
}
Drawable getIconDrawable() {
if (mIconDrawable != null) {
return mIconDrawable;
} else if (mIcon != 0) {
return ContextCompat.getDrawable(getContext(), mIcon);
} else {
return new ColorDrawable(Color.TRANSPARENT);
}
}
public void setIconDrawable(@NonNull final Drawable iconDrawable) {
if (mIconDrawable != iconDrawable) {
mIcon = 0;
mIconDrawable = iconDrawable;
updateBackground();
}
}
private StateListDrawable createFillDrawable(final float strokeWidth) {
final StateListDrawable drawable = new StateListDrawable();
drawable.addState(new int[]{-android.R.attr.state_enabled}, createCircleDrawable(mColorDisabled, strokeWidth));
drawable.addState(new int[]{android.R.attr.state_pressed}, createCircleDrawable(mColorPressed, strokeWidth));
drawable.addState(new int[]{}, createCircleDrawable(mColorNormal, strokeWidth));
return drawable;
}
private Drawable createCircleDrawable(final int color, final float strokeWidth) {
final int alpha = Color.alpha(color);
final int opaqueColor = opaque(color);
final ShapeDrawable fillDrawable = new ShapeDrawable(new OvalShape());
final Paint paint = fillDrawable.getPaint();
paint.setAntiAlias(true);
paint.setColor(opaqueColor);
final Drawable[] layers = {
fillDrawable,
createInnerStrokesDrawable(opaqueColor, strokeWidth)
};
final LayerDrawable drawable = alpha == 255 || !mStrokeVisible
? new LayerDrawable(layers)
: new TranslucentLayerDrawable(alpha, layers);
final int halfStrokeWidth = (int) (strokeWidth / 2f);
drawable.setLayerInset(1, halfStrokeWidth, halfStrokeWidth, halfStrokeWidth, halfStrokeWidth);
return drawable;
}
private static Drawable createOuterStrokeDrawable(final float strokeWidth) {
final ShapeDrawable shapeDrawable = new ShapeDrawable(new OvalShape());
final Paint paint = shapeDrawable.getPaint();
paint.setAntiAlias(true);
paint.setStrokeWidth(strokeWidth);
paint.setStyle(Style.STROKE);
paint.setColor(Color.BLACK);
paint.setAlpha(opacityToAlpha(0.02f));
return shapeDrawable;
}
private static int opacityToAlpha(final float opacity) {
return (int) (255f * opacity);
}
private static int darkenColor(final int argb) {
return adjustColorBrightness(argb, 0.9f);
}
private static int lightenColor(final int argb) {
return adjustColorBrightness(argb, 1.1f);
}
public static int darkenOrLightenColor(final int argb) {
final float[] hsv = new float[3];
Color.colorToHSV(argb, hsv);
final float factor;
if (hsv[2] < 0.2)
factor = 1.2f;
else
factor = 0.8f;
hsv[2] = Math.min(hsv[2] * factor, 1f);
return Color.HSVToColor(Color.alpha(argb), hsv);
}
private static int adjustColorBrightness(final int argb, final float factor) {
final float[] hsv = new float[3];
Color.colorToHSV(argb, hsv);
hsv[2] = Math.min(hsv[2] * factor, 1f);
return Color.HSVToColor(Color.alpha(argb), hsv);
}
private static int halfTransparent(final int argb) {
return Color.argb(
Color.alpha(argb) / 2,
Color.red(argb),
Color.green(argb),
Color.blue(argb)
);
}
private static int opaque(final int argb) {
return Color.rgb(
Color.red(argb),
Color.green(argb),
Color.blue(argb)
);
}
private Drawable createInnerStrokesDrawable(final int color, final float strokeWidth) {
if (!mStrokeVisible) {
return new ColorDrawable(Color.TRANSPARENT);
}
final ShapeDrawable shapeDrawable = new ShapeDrawable(new OvalShape());
final int bottomStrokeColor = darkenColor(color);
final int bottomStrokeColorHalfTransparent = halfTransparent(bottomStrokeColor);
final int topStrokeColor = lightenColor(color);
final int topStrokeColorHalfTransparent = halfTransparent(topStrokeColor);
final Paint paint = shapeDrawable.getPaint();
paint.setAntiAlias(true);
paint.setStrokeWidth(strokeWidth);
paint.setStyle(Style.STROKE);
shapeDrawable.setShaderFactory(new ShaderFactory() {
@Override
public Shader resize(int width, int height) {
return new LinearGradient(width / 2, 0, width / 2, height,
new int[]{topStrokeColor, topStrokeColorHalfTransparent, color, bottomStrokeColorHalfTransparent, bottomStrokeColor},
new float[]{0f, 0.2f, 0.5f, 0.8f, 1f},
TileMode.CLAMP
);
}
});
return shapeDrawable;
}
@Override
public void setVisibility(final int visibility) {
final TextView label = getLabelView();
if (label != null) {
label.setVisibility(visibility);
}
super.setVisibility(visibility);
}
@Retention(RetentionPolicy.SOURCE)
@IntDef({SIZE_NORMAL, SIZE_MINI})
public @interface FAB_SIZE {
}
private static final class TranslucentLayerDrawable extends LayerDrawable {
private final int mAlpha;
private TranslucentLayerDrawable(final int alpha, final Drawable... layers) {
super(layers);
mAlpha = alpha;
}
@Override
public void draw(final Canvas canvas) {
final Rect bounds = getBounds();
canvas.saveLayerAlpha(bounds.left, bounds.top, bounds.right, bounds.bottom, mAlpha);
super.draw(canvas);
canvas.restore();
}
}
}
@@ -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());
}
}
@@ -1,7 +1,7 @@
/*
* Copyright © 2014 Jerzy Chalupski
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget.fab;
@@ -17,11 +17,14 @@ 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 android.support.annotation.Keep;
import android.support.annotation.NonNull;
import android.support.v7.widget.AppCompatTextView;
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;
@@ -33,136 +36,59 @@ import android.widget.TextView;
import com.wireguard.android.R;
@SuppressWarnings("ReturnOfInnerClass")
public class FloatingActionsMenu extends ViewGroup {
public static final int EXPAND_UP = 0;
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 static final TimeInterpolator COLLAPSE_INTERPOLATOR = new DecelerateInterpolator(3f);
private static final TimeInterpolator ALPHA_EXPAND_INTERPOLATOR = new DecelerateInterpolator();
private int mAddButtonPlusColor;
private int mAddButtonColorNormal;
private int mAddButtonColorPressed;
private int mAddButtonSize;
private boolean mAddButtonStrokeVisible;
private int mExpandDirection;
private int mButtonSpacing;
private int mLabelsMargin;
private int mLabelsVerticalOffset;
private boolean mExpanded;
private final AnimatorSet mExpandAnimation = new AnimatorSet().setDuration(ANIMATION_DURATION);
private final AnimatorSet mCollapseAnimation = new AnimatorSet().setDuration(ANIMATION_DURATION);
private AddFloatingActionButton mAddButton;
private RotatingDrawable mRotatingDrawable;
private int mMaxButtonWidth;
private int mMaxButtonHeight;
private int mLabelsStyle;
private int mLabelsPosition;
private int mButtonsCount;
private TouchDelegateGroup mTouchDelegateGroup;
private OnFloatingActionsMenuUpdateListener mListener;
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, final AttributeSet attrs) {
public FloatingActionsMenu(final Context context, @Nullable final AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public FloatingActionsMenu(final Context context, final AttributeSet attrs, final int defStyle) {
public FloatingActionsMenu(final Context context, @Nullable final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
private void init(final Context context, final AttributeSet attributeSet) {
mButtonSpacing = (int) (getResources().getDimension(R.dimen.fab_actions_spacing) - getResources().getDimension(R.dimen.fab_shadow_radius) - getResources().getDimension(R.dimen.fab_shadow_offset));
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);
mAddButtonPlusColor = attr.getColor(R.styleable.FloatingActionsMenu_fab_addButtonPlusIconColor,
FloatingActionButton.getColorFromTheme(context, android.R.attr.colorBackground, android.R.color.white));
mAddButtonColorNormal = attr.getColor(R.styleable.FloatingActionsMenu_fab_addButtonColorNormal,
FloatingActionButton.getColorFromTheme(context, android.R.attr.colorAccent, android.R.color.holo_blue_bright));
mAddButtonColorPressed = attr.getColor(R.styleable.FloatingActionsMenu_fab_addButtonColorPressed,
FloatingActionButton.darkenOrLightenColor(mAddButtonColorNormal)); //TODO(msf): use getColorForState on the accent color from theme instead to get darker states
mAddButtonSize = attr.getInt(R.styleable.FloatingActionsMenu_fab_addButtonSize, FloatingActionButton.SIZE_NORMAL);
mAddButtonStrokeVisible = attr.getBoolean(R.styleable.FloatingActionsMenu_fab_addButtonStrokeVisible, true);
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 is not supported.");
}
createAddButton(context);
private static int adjustForOvershoot(final int dimension) {
return dimension * 12 / 10;
}
public void setOnFloatingActionsMenuUpdateListener(final OnFloatingActionsMenuUpdateListener listener) {
mListener = listener;
}
private boolean expandsHorizontally() {
return mExpandDirection == EXPAND_LEFT || mExpandDirection == EXPAND_RIGHT;
}
private void createAddButton(final Context context) {
mAddButton = new AddFloatingActionButton(context) {
@Override
void updateBackground() {
mPlusColor = mAddButtonPlusColor;
mColorNormal = mAddButtonColorNormal;
mColorPressed = mAddButtonColorPressed;
mStrokeVisible = mAddButtonStrokeVisible;
super.updateBackground();
}
@Override
Drawable getIconDrawable() {
final RotatingDrawable rotatingDrawable = new RotatingDrawable(super.getIconDrawable());
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);
return rotatingDrawable;
}
};
mAddButton.setId(R.id.fab_expand_menu_button);
mAddButton.setSize(mAddButtonSize);
mAddButton.setOnClickListener(v -> toggle());
addView(mAddButton, super.generateDefaultLayoutParams());
mButtonsCount++;
}
public void addButton(final FloatingActionButton button) {
public void addButton(final LabeledFloatingActionButton button) {
addView(button, mButtonsCount - 1);
mButtonsCount++;
@@ -171,11 +97,276 @@ public class FloatingActionsMenu extends ViewGroup {
}
}
public void removeButton(final FloatingActionButton button) {
removeView(button.getLabelView());
removeView(button);
button.setTag(R.id.fab_label, null);
mButtonsCount--;
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
@@ -239,248 +430,6 @@ public class FloatingActionsMenu extends ViewGroup {
setMeasuredDimension(width, height);
}
private static int adjustForOvershoot(final int dimension) {
return dimension * 12 / 10;
}
@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(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 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));
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
bringChildToFront(mAddButton);
mButtonsCount = getChildCount();
if (mLabelsStyle != 0) {
createLabels();
}
}
private void createLabels() {
final Context context = new ContextThemeWrapper(getContext(), mLabelsStyle);
for (int i = 0; i < mButtonsCount; i++) {
final FloatingActionButton button = (FloatingActionButton) getChildAt(i);
final String title = button.getTitle();
if (button == mAddButton || title == null ||
button.getTag(R.id.fab_label) != null) continue;
final AppCompatTextView label = new AppCompatTextView(context);
label.setTextAppearance(context, mLabelsStyle);
label.setText(button.getTitle());
addView(label);
button.setTag(R.id.fab_label, label);
}
}
public void collapse() {
collapse(false);
}
public void collapseImmediately() {
collapse(true);
}
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 toggle() {
if (mExpanded) {
collapse();
} else {
expand();
}
}
public void expand() {
if (!mExpanded) {
mExpanded = true;
mTouchDelegateGroup.setEnabled(true);
mCollapseAnimation.cancel();
mExpandAnimation.start();
if (mListener != null) {
mListener.onMenuExpanded();
}
}
}
public boolean isExpanded() {
return mExpanded;
}
@Override
public void setEnabled(final boolean enabled) {
super.setEnabled(enabled);
mAddButton.setEnabled(enabled);
}
@Override
public Parcelable onSaveInstanceState() {
final Parcelable superState = super.onSaveInstanceState();
final SavedState savedState = new SavedState(superState);
savedState.mExpanded = mExpanded;
return savedState;
}
@Override
public void onRestoreInstanceState(final Parcelable state) {
if (state instanceof SavedState) {
@@ -498,10 +447,55 @@ public class FloatingActionsMenu extends ViewGroup {
}
}
public interface OnFloatingActionsMenuUpdateListener {
void onMenuExpanded();
@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 {
@@ -511,6 +505,14 @@ public class FloatingActionsMenu extends ViewGroup {
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;
@@ -522,14 +524,6 @@ public class FloatingActionsMenu extends ViewGroup {
mRotation = rotation;
invalidateSelf();
}
@Override
public void draw(final Canvas canvas) {
canvas.save();
canvas.rotate(mRotation, getBounds().centerX(), getBounds().centerY());
super.draw(canvas);
canvas.restore();
}
}
public static class SavedState extends BaseSavedState {
@@ -545,7 +539,7 @@ public class FloatingActionsMenu extends ViewGroup {
return new SavedState[size];
}
};
public boolean mExpanded;
private boolean mExpanded;
public SavedState(final Parcelable parcel) {
super(parcel);
@@ -557,7 +551,7 @@ public class FloatingActionsMenu extends ViewGroup {
}
@Override
public void writeToParcel(@NonNull final Parcel out, final int flags) {
public void writeToParcel(final Parcel out, final int flags) {
super.writeToParcel(out, flags);
out.writeInt(mExpanded ? 1 : 0);
}
@@ -565,10 +559,10 @@ public class FloatingActionsMenu extends ViewGroup {
private class LayoutParams extends ViewGroup.LayoutParams {
private final ObjectAnimator mExpandDir = new ObjectAnimator();
private final ObjectAnimator mExpandAlpha = new ObjectAnimator();
private final ObjectAnimator mCollapseDir = new ObjectAnimator();
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) {
@@ -599,6 +593,20 @@ public class FloatingActionsMenu extends ViewGroup {
}
}
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);
@@ -617,19 +625,5 @@ public class FloatingActionsMenu extends ViewGroup {
animationsSetToPlay = true;
}
}
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);
}
});
}
}
}
@@ -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);
}
}
@@ -1,13 +1,13 @@
/*
* Copyright © 2014 Jerzy Chalupski
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* 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 android.support.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.MotionEvent;
import android.view.TouchDelegate;
import android.view.View;
@@ -18,38 +18,32 @@ 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<>();
private TouchDelegate mCurrentTouchDelegate;
@Nullable private TouchDelegate mCurrentTouchDelegate;
private boolean mEnabled;
public TouchDelegateGroup(final View uselessHackyView) {
super(USELESS_HACKY_RECT, uselessHackyView);
}
public void addTouchDelegate(@NonNull final TouchDelegate touchDelegate) {
public void addTouchDelegate(final TouchDelegate touchDelegate) {
mTouchDelegates.add(touchDelegate);
}
public void removeTouchDelegate(final TouchDelegate touchDelegate) {
mTouchDelegates.remove(touchDelegate);
if (mCurrentTouchDelegate == touchDelegate) {
mCurrentTouchDelegate = null;
}
}
public void clearTouchDelegates() {
mTouchDelegates.clear();
mCurrentTouchDelegate = null;
}
@Override
public boolean onTouchEvent(@NonNull final MotionEvent event) {
if (!mEnabled) return false;
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) {
for (final TouchDelegate touchDelegate : mTouchDelegates) {
if (touchDelegate.onTouchEvent(event)) {
mCurrentTouchDelegate = touchDelegate;
return true;
@@ -71,6 +65,13 @@ public class TouchDelegateGroup extends TouchDelegate {
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,88 +1,49 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.config;
import android.text.TextUtils;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* The set of valid attributes for an interface or peer in a WireGuard configuration file.
*/
import java9.util.Optional;
enum Attribute {
ADDRESS("Address"),
ALLOWED_IPS("AllowedIPs"),
DNS("DNS"),
ENDPOINT("Endpoint"),
LISTEN_PORT("ListenPort"),
MTU("MTU"),
PERSISTENT_KEEPALIVE("PersistentKeepalive"),
PRESHARED_KEY("PresharedKey"),
PRIVATE_KEY("PrivateKey"),
PUBLIC_KEY("PublicKey");
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*");
private static final String[] EMPTY_LIST = new String[0];
private static final Map<String, Attribute> KEY_MAP;
private static final Pattern LIST_SEPARATOR_PATTERN = Pattern.compile("\\s*,\\s*");
private static final Pattern SEPARATOR_PATTERN = Pattern.compile("\\s|=");
private final String key;
private final String value;
static {
KEY_MAP = new HashMap<>(Attribute.values().length);
for (final Attribute key : Attribute.values()) {
KEY_MAP.put(key.token.toLowerCase(), key);
}
private Attribute(final String key, final String value) {
this.key = key;
this.value = value;
}
private final Pattern pattern;
private final String token;
Attribute(final String token) {
pattern = Pattern.compile(token + "\\s*=\\s*(\\S.*)");
this.token = token;
public static String join(final Iterable<?> values) {
return TextUtils.join(", ", values);
}
public static <T> String iterableToString(final Iterable<T> iterable) {
return TextUtils.join(", ", iterable);
public static Optional<Attribute> parse(final CharSequence line) {
final Matcher matcher = LINE_PATTERN.matcher(line);
if (!matcher.matches())
return Optional.empty();
return Optional.of(new Attribute(matcher.group(1), matcher.group(2)));
}
public static Attribute match(final CharSequence line) {
return KEY_MAP.get(SEPARATOR_PATTERN.split(line)[0].toLowerCase());
public static String[] split(final CharSequence value) {
return LIST_SEPARATOR.split(value);
}
public static String[] stringToList(final String string) {
if (string == null)
return EMPTY_LIST;
return LIST_SEPARATOR_PATTERN.split(string.trim());
public String getKey() {
return key;
}
public String composeWith(final Object value) {
return String.format("%s = %s%n", token, value);
}
public String composeWith(final int value) {
return String.format(Locale.getDefault(), "%s = %d%n", token, value);
}
public <T> String composeWith(final Iterable<T> value) {
return String.format("%s = %s%n", token, iterableToString(value));
}
public String parse(final CharSequence line) {
final Matcher matcher = pattern.matcher(line);
return matcher.matches() ? matcher.group(1) : null;
}
public String[] parseList(final CharSequence line) {
final Matcher matcher = pattern.matcher(line);
return matcher.matches() ? stringToList(matcher.group(1)) : null;
public String getValue() {
return value;
}
}
@@ -0,0 +1,118 @@
/*
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.config;
import androidx.annotation.Nullable;
import com.wireguard.crypto.KeyFormatException;
public class BadConfigException extends Exception {
private final Location location;
private final Reason reason;
private final Section section;
@Nullable private final CharSequence text;
private BadConfigException(final Section section, final Location location,
final Reason reason, @Nullable final CharSequence text,
@Nullable final Throwable cause) {
super(cause);
this.section = section;
this.location = location;
this.reason = reason;
this.text = text;
}
public BadConfigException(final Section section, final Location location,
final Reason reason, @Nullable final CharSequence text) {
this(section, location, reason, text, null);
}
public BadConfigException(final Section section, final Location location,
final KeyFormatException cause) {
this(section, location, Reason.INVALID_KEY, null, cause);
}
public BadConfigException(final Section section, final Location location,
@Nullable final CharSequence text,
final NumberFormatException cause) {
this(section, location, Reason.INVALID_NUMBER, text, cause);
}
public BadConfigException(final Section section, final Location location,
final ParseException cause) {
this(section, location, Reason.INVALID_VALUE, cause.getText(), cause);
}
public Location getLocation() {
return location;
}
public Reason getReason() {
return reason;
}
public Section getSection() {
return section;
}
@Nullable
public CharSequence getText() {
return text;
}
public enum Location {
TOP_LEVEL(""),
ADDRESS("Address"),
ALLOWED_IPS("AllowedIPs"),
DNS("DNS"),
ENDPOINT("Endpoint"),
EXCLUDED_APPLICATIONS("ExcludedApplications"),
LISTEN_PORT("ListenPort"),
MTU("MTU"),
PERSISTENT_KEEPALIVE("PersistentKeepalive"),
PRE_SHARED_KEY("PresharedKey"),
PRIVATE_KEY("PrivateKey"),
PUBLIC_KEY("PublicKey");
private final String name;
Location(final String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public enum Reason {
INVALID_KEY,
INVALID_NUMBER,
INVALID_VALUE,
MISSING_ATTRIBUTE,
MISSING_SECTION,
MISSING_VALUE,
SYNTAX_ERROR,
UNKNOWN_ATTRIBUTE,
UNKNOWN_SECTION
}
public enum Section {
CONFIG("Config"),
INTERFACE("Interface"),
PEER("Peer");
private final String name;
Section(final String name) {
this.name = name;
}
public String getName() {
return name;
}
}
}
+160 -107
View File
@@ -1,45 +1,73 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.config;
import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.databinding.ObservableArrayList;
import android.databinding.ObservableList;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import com.android.databinding.library.baseAdapters.BR;
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;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* Represents a wg-quick configuration file, its name, and its connection state.
* 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.
*/
public final class Config {
private final Interface interfaze;
private final List<Peer> peers;
public class Config {
private final Interface interfaceSection = new Interface();
private List<Peer> peers = new ArrayList<>();
public static Config from(final InputStream stream) throws IOException {
return from(new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)));
private Config(final Builder builder) {
interfaze = Objects.requireNonNull(builder.interfaze, "An [Interface] section is required");
// Defensively copy to ensure immutability even if the Builder is reused.
peers = Collections.unmodifiableList(new ArrayList<>(builder.peers));
}
public static Config from(final BufferedReader reader) throws IOException {
final Config config = new Config();
Peer currentPeer = null;
String line;
/**
* Parses an series of "Interface" and "Peer" sections into a {@code Config}. Throws
* {@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 a WireGuard configuration
* @return a {@code Config} instance representing the supplied configuration
*/
public static Config parse(final InputStream stream)
throws IOException, BadConfigException {
return parse(new BufferedReader(new InputStreamReader(stream)));
}
/**
* Parses an series of "Interface" and "Peer" sections into a {@code Config}. Throws
* {@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 a WireGuard configuration
* @return a {@code Config} instance representing the supplied configuration
*/
public static Config parse(final BufferedReader reader)
throws IOException, BadConfigException {
final Builder builder = new Builder();
final Collection<String> interfaceLines = new ArrayList<>();
final Collection<String> peerLines = new ArrayList<>();
boolean inInterfaceSection = false;
boolean inPeerSection = false;
@Nullable String line;
while ((line = reader.readLine()) != null) {
final int commentIndex = line.indexOf('#');
if (commentIndex != -1)
@@ -47,122 +75,147 @@ public class Config {
line = line.trim();
if (line.isEmpty())
continue;
if ("[Interface]".toLowerCase().equals(line.toLowerCase())) {
currentPeer = null;
inInterfaceSection = true;
} else if ("[Peer]".toLowerCase().equals(line.toLowerCase())) {
currentPeer = new Peer();
config.peers.add(currentPeer);
inInterfaceSection = false;
if (line.startsWith("[")) {
// Consume all [Peer] lines read so far.
if (inPeerSection) {
builder.parsePeer(peerLines);
peerLines.clear();
}
if ("[Interface]".equalsIgnoreCase(line)) {
inInterfaceSection = true;
inPeerSection = false;
} else if ("[Peer]".equalsIgnoreCase(line)) {
inInterfaceSection = false;
inPeerSection = true;
} else {
throw new BadConfigException(Section.CONFIG, Location.TOP_LEVEL,
Reason.UNKNOWN_SECTION, line);
}
} else if (inInterfaceSection) {
config.interfaceSection.parse(line);
} else if (currentPeer != null) {
currentPeer.parse(line);
interfaceLines.add(line);
} else if (inPeerSection) {
peerLines.add(line);
} else {
throw new IllegalArgumentException("Invalid configuration line: " + line);
throw new BadConfigException(Section.CONFIG, Location.TOP_LEVEL,
Reason.UNKNOWN_SECTION, line);
}
}
if (!inInterfaceSection && currentPeer == null) {
throw new IllegalArgumentException("Could not find any config information");
}
return config;
if (inPeerSection)
builder.parsePeer(peerLines);
else if (!inInterfaceSection)
throw new BadConfigException(Section.CONFIG, Location.TOP_LEVEL,
Reason.MISSING_SECTION, null);
// Combine all [Interface] sections in the file.
builder.parseInterface(interfaceLines);
return builder.build();
}
@Override
public boolean equals(final Object obj) {
if (!(obj instanceof Config))
return false;
final Config other = (Config) obj;
return interfaze.equals(other.interfaze) && peers.equals(other.peers);
}
/**
* Returns the interface section of the configuration.
*
* @return the interface configuration
*/
public Interface getInterface() {
return interfaceSection;
return interfaze;
}
/**
* Returns a list of the configuration's peer sections.
*
* @return a list of {@link Peer}s
*/
public List<Peer> getPeers() {
return peers;
}
@Override
public int hashCode() {
return 31 * interfaze.hashCode() + peers.hashCode();
}
/**
* Converts the {@code Config} into a string suitable for debugging purposes. The {@code Config}
* is identified by its interface's public key and the number of peers it has.
*
* @return a concise single-line identifier for the {@code Config}
*/
@Override
public String toString() {
final StringBuilder sb = new StringBuilder().append(interfaceSection);
return "(Config " + interfaze + " (" + peers.size() + " peers))";
}
/**
* 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 toWgQuickString() {
final StringBuilder sb = new StringBuilder();
sb.append("[Interface]\n").append(interfaze.toWgQuickString());
for (final Peer peer : peers)
sb.append('\n').append(peer);
sb.append("\n[Peer]\n").append(peer.toWgQuickString());
return sb.toString();
}
public static class Observable extends BaseObservable implements Parcelable {
public static final Creator<Observable> CREATOR = new Creator<Observable>() {
@Override
public Observable createFromParcel(final Parcel in) {
return new Observable(in);
}
/**
* 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 toWgUserspaceString() {
final StringBuilder sb = new StringBuilder();
sb.append(interfaze.toWgUserspaceString());
sb.append("replace_peers=true\n");
for (final Peer peer : peers)
sb.append(peer.toWgUserspaceString());
return sb.toString();
}
@Override
public Observable[] newArray(final int size) {
return new Observable[size];
}
};
private String name;
private Interface.Observable observableInterface;
private ObservableList<Peer.Observable> observablePeers;
@SuppressWarnings("UnusedReturnValue")
public static final class Builder {
// Defaults to an empty set.
private final Set<Peer> peers = new LinkedHashSet<>();
// No default; must be provided before building.
@Nullable private Interface interfaze;
public Observable(final Config parent, final String name) {
this.name = name;
loadData(parent);
public Builder addPeer(final Peer peer) {
peers.add(peer);
return this;
}
private Observable(final Parcel in) {
name = in.readString();
observableInterface = in.readParcelable(Interface.Observable.class.getClassLoader());
observablePeers = new ObservableArrayList<>();
in.readTypedList(observablePeers, Peer.Observable.CREATOR);
public Builder addPeers(final Collection<Peer> peers) {
this.peers.addAll(peers);
return this;
}
public void commitData(final Config parent) {
observableInterface.commitData(parent.interfaceSection);
final List<Peer> newPeers = new ArrayList<>(observablePeers.size());
for (final Peer.Observable observablePeer : observablePeers) {
final Peer peer = new Peer();
observablePeer.commitData(peer);
newPeers.add(peer);
}
parent.peers = newPeers;
notifyChange();
public Config build() {
if (interfaze == null)
throw new IllegalArgumentException("An [Interface] section is required");
return new Config(this);
}
@Override
public int describeContents() {
return 0;
public Builder parseInterface(final Iterable<? extends CharSequence> lines)
throws BadConfigException {
return setInterface(Interface.parse(lines));
}
@Bindable
public Interface.Observable getInterfaceSection() {
return observableInterface;
public Builder parsePeer(final Iterable<? extends CharSequence> lines)
throws BadConfigException {
return addPeer(Peer.parse(lines));
}
@Bindable
public String getName() {
return name == null ? "" : name;
}
@Bindable
public ObservableList<Peer.Observable> getPeers() {
return observablePeers;
}
protected void loadData(final Config parent) {
observableInterface = new Interface.Observable(parent == null ? null : parent.interfaceSection);
observablePeers = new ObservableArrayList<>();
if (parent != null) {
for (final Peer peer : parent.getPeers())
observablePeers.add(new Peer.Observable(peer));
}
}
public void setName(final String name) {
this.name = name;
notifyPropertyChanged(BR.name);
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeString(name);
dest.writeParcelable(observableInterface, flags);
dest.writeTypedList(observablePeers);
public Builder setInterface(final Interface interfaze) {
this.interfaze = interfaze;
return this;
}
}
}
@@ -1,24 +1,26 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.config;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
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 final Method PARSER_METHOD;
static {
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);
@@ -29,13 +31,23 @@ public final class InetAddresses {
// Prevent instantiation.
}
@NonNull
public static InetAddress parse(@Nullable final String address) {
if (address == null || address.isEmpty())
throw new IllegalArgumentException("Empty address");
/**
* 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 {
return (InetAddress) PARSER_METHOD.invoke(null, 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);
}
}
@@ -0,0 +1,125 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.config;
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.util.regex.Pattern;
import java9.util.Optional;
/**
* An external endpoint (host and port) used to connect to a WireGuard {@link Peer}.
* <p>
* Instances of this class are externally immutable.
*/
public final class InetEndpoint {
private static final Pattern BARE_IPV6 = Pattern.compile("^[^\\[\\]]*:[^\\[\\]]*");
private static final Pattern FORBIDDEN_CHARACTERS = Pattern.compile("[/?#]");
private final String host;
private final boolean isResolved;
private final Object lock = new Object();
private final int port;
private Instant lastResolution = Instant.EPOCH;
@Nullable private InetEndpoint resolved;
private InetEndpoint(final String host, final boolean isResolved, final int port) {
this.host = host;
this.isResolved = isResolved;
this.port = port;
}
public static InetEndpoint parse(final String endpoint) throws ParseException {
if (FORBIDDEN_CHARACTERS.matcher(endpoint).find())
throw new ParseException(InetEndpoint.class, endpoint, "Forbidden characters");
final URI uri;
try {
uri = new URI("wg://" + endpoint);
} catch (final URISyntaxException e) {
throw new IllegalArgumentException(e);
}
if (uri.getPort() < 0 || uri.getPort() > 65535)
throw new ParseException(InetEndpoint.class, endpoint, "Missing/invalid port number");
try {
InetAddresses.parse(uri.getHost());
// Parsing ths host as a numeric address worked, so we don't need to do DNS lookups.
return new InetEndpoint(uri.getHost(), true, uri.getPort());
} catch (final ParseException ignored) {
// Failed to parse the host as a numeric address, so it must be a DNS hostname/FQDN.
return new InetEndpoint(uri.getHost(), false, uri.getPort());
}
}
@Override
public boolean equals(final Object obj) {
if (!(obj instanceof InetEndpoint))
return false;
final InetEndpoint other = (InetEndpoint) obj;
return host.equals(other.host) && port == other.port;
}
public String getHost() {
return host;
}
public int getPort() {
return port;
}
/**
* Generate an {@code InetEndpoint} instance with the same port and the host resolved using DNS
* to a numeric address. If the host is already numeric, the existing instance may be returned.
* Because this function may perform network I/O, it must not be called from the main thread.
*
* @return the resolved endpoint, or {@link Optional#empty()}
*/
public Optional<InetEndpoint> getResolved() {
if (isResolved)
return Optional.of(this);
synchronized (lock) {
//TODO(zx2c4): Implement a real timeout mechanism using DNS TTL
if (Duration.between(lastResolution, Instant.now()).toMinutes() > 1) {
try {
// Prefer v4 endpoints over v6 to work around DNS64 and IPv6 NAT issues.
final InetAddress[] candidates = InetAddress.getAllByName(host);
InetAddress address = candidates[0];
for (final InetAddress candidate : candidates) {
if (candidate instanceof Inet4Address) {
address = candidate;
break;
}
}
resolved = new InetEndpoint(address.getHostAddress(), true, port);
lastResolution = Instant.now();
} catch (final UnknownHostException e) {
resolved = null;
}
}
return Optional.ofNullable(resolved);
}
}
@Override
public int hashCode() {
return host.hashCode() ^ port;
}
@Override
public String toString() {
final boolean isBareIpv6 = isResolved && BARE_IPV6.matcher(host).matches();
return (isBareIpv6 ? '[' + host + ']' : host) + ':' + port;
}
}
@@ -1,35 +1,51 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.config;
import android.support.annotation.NonNull;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.util.Objects;
public class InetNetwork {
/**
* An Internet network, denoted by its address and netmask
* <p>
* Instances of this class are immutable.
*/
public final class InetNetwork {
private final InetAddress address;
private final int mask;
public InetNetwork(@NonNull final String input) {
final int slash = input.lastIndexOf('/');
private InetNetwork(final InetAddress address, final int mask) {
this.address = address;
this.mask = mask;
}
public static InetNetwork parse(final String network) throws ParseException {
final int slash = network.lastIndexOf('/');
final String maskString;
final int rawMask;
final String rawAddress;
if (slash >= 0) {
rawMask = Integer.parseInt(input.substring(slash + 1), 10);
rawAddress = input.substring(0, slash);
maskString = network.substring(slash + 1);
try {
rawMask = Integer.parseInt(maskString, 10);
} catch (final NumberFormatException ignored) {
throw new ParseException(Integer.class, maskString);
}
rawAddress = network.substring(0, slash);
} else {
maskString = "";
rawMask = -1;
rawAddress = input;
rawAddress = network;
}
address = InetAddresses.parse(rawAddress);
final InetAddress address = InetAddresses.parse(rawAddress);
final int maxMask = (address instanceof Inet4Address) ? 32 : 128;
mask = rawMask >= 0 && rawMask <= maxMask ? rawMask : maxMask;
if (rawMask > maxMask)
throw new ParseException(InetNetwork.class, maskString, "Invalid network mask");
final int mask = rawMask >= 0 && rawMask <= maxMask ? rawMask : maxMask;
return new InetNetwork(address, mask);
}
@Override
@@ -37,10 +53,9 @@ public class InetNetwork {
if (!(obj instanceof InetNetwork))
return false;
final InetNetwork other = (InetNetwork) obj;
return Objects.equals(address, other.address) && mask == other.mask;
return address.equals(other.address) && mask == other.mask;
}
@NonNull
public InetAddress getAddress() {
return address;
}
@@ -1,329 +1,355 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.config;
import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import com.wireguard.android.BR;
import com.wireguard.crypto.Keypair;
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.ArrayList;
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).
* 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;
public class Interface {
private final List<InetNetwork> addressList;
private final List<InetAddress> dnsList;
private Keypair keypair;
private int listenPort;
private int mtu;
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;
public Interface() {
addressList = new ArrayList<>();
dnsList = new ArrayList<>();
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;
}
private void addAddresses(final String[] addresses) {
if (addresses != null && addresses.length > 0) {
for (final String addr : addresses) {
if (addr.isEmpty())
throw new IllegalArgumentException("Address is empty");
addressList.add(new InetNetwork(addr));
/**
* 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());
}
}
}
private void addDnses(final String[] dnses) {
if (dnses != null && dnses.length > 0) {
for (final String dns : dnses) {
dnsList.add(InetAddresses.parse(dns));
}
}
}
private String getAddressString() {
if (addressList.isEmpty())
return null;
return Attribute.iterableToString(addressList);
}
public InetNetwork[] getAddresses() {
return addressList.toArray(new InetNetwork[addressList.size()]);
}
private String getDnsString() {
if (dnsList.isEmpty())
return null;
return Attribute.iterableToString(getDnsStrings());
}
private List<String> getDnsStrings() {
final List<String> strings = new ArrayList<>();
for (final InetAddress addr : dnsList)
strings.add(addr.getHostAddress());
return strings;
}
public InetAddress[] getDnses() {
return dnsList.toArray(new InetAddress[dnsList.size()]);
}
public int getListenPort() {
return listenPort;
}
private String getListenPortString() {
if (listenPort == 0)
return null;
return Integer.valueOf(listenPort).toString();
}
public int getMtu() {
return mtu;
}
private String getMtuString() {
if (mtu == 0)
return null;
return Integer.toString(mtu);
}
public String getPrivateKey() {
if (keypair == null)
return null;
return keypair.getPrivateKey();
}
public String getPublicKey() {
if (keypair == null)
return null;
return keypair.getPublicKey();
}
public void parse(final String line) {
final Attribute key = Attribute.match(line);
switch (key) {
case ADDRESS:
addAddresses(key.parseList(line));
break;
case DNS:
addDnses(key.parseList(line));
break;
case LISTEN_PORT:
setListenPortString(key.parse(line));
break;
case MTU:
setMtuString(key.parse(line));
break;
case PRIVATE_KEY:
setPrivateKey(key.parse(line));
break;
default:
throw new IllegalArgumentException(line);
}
}
private void setAddressString(final String addressString) {
addressList.clear();
addAddresses(Attribute.stringToList(addressString));
}
private void setDnsString(final String dnsString) {
dnsList.clear();
addDnses(Attribute.stringToList(dnsString));
}
private void setListenPort(final int listenPort) {
this.listenPort = listenPort;
}
private void setListenPortString(final String port) {
if (port != null && !port.isEmpty())
setListenPort(Integer.parseInt(port, 10));
else
setListenPort(0);
}
private void setMtu(final int mtu) {
this.mtu = mtu;
}
private void setMtuString(final String mtu) {
if (mtu != null && !mtu.isEmpty())
setMtu(Integer.parseInt(mtu, 10));
else
setMtu(0);
}
private void setPrivateKey(String privateKey) {
if (privateKey != null && privateKey.isEmpty())
privateKey = null;
keypair = privateKey == null ? null : new Keypair(privateKey);
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().append("[Interface]\n");
if (!addressList.isEmpty())
sb.append(Attribute.ADDRESS.composeWith(addressList));
if (!dnsList.isEmpty())
sb.append(Attribute.DNS.composeWith(getDnsStrings()));
if (listenPort != 0)
sb.append(Attribute.LISTEN_PORT.composeWith(listenPort));
if (mtu != 0)
sb.append(Attribute.MTU.composeWith(mtu));
if (keypair != null)
sb.append(Attribute.PRIVATE_KEY.composeWith(keypair.getPrivateKey()));
final StringBuilder sb = new StringBuilder("(Interface ");
sb.append(keyPair.getPublicKey().toBase64());
listenPort.ifPresent(lp -> sb.append(" @").append(lp));
sb.append(')');
return sb.toString();
}
public static class Observable extends BaseObservable implements Parcelable {
public static final Creator<Observable> CREATOR = new Creator<Observable>() {
@Override
public Observable createFromParcel(final Parcel in) {
return new Observable(in);
}
/**
* 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();
}
@Override
public Observable[] newArray(final int size) {
return new Observable[size];
}
};
private String addresses;
private String dnses;
private String listenPort;
private String mtu;
private String privateKey;
private String publicKey;
/**
* 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();
}
public Observable(final Interface parent) {
if (parent != null)
loadData(parent);
@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;
}
private Observable(final Parcel in) {
addresses = in.readString();
dnses = in.readString();
publicKey = in.readString();
privateKey = in.readString();
listenPort = in.readString();
mtu = in.readString();
public Builder addAddresses(final Collection<InetNetwork> addresses) {
this.addresses.addAll(addresses);
return this;
}
public void commitData(final Interface parent) {
parent.setAddressString(addresses);
parent.setDnsString(dnses);
parent.setPrivateKey(privateKey);
parent.setListenPortString(listenPort);
parent.setMtuString(mtu);
loadData(parent);
notifyChange();
public Builder addDnsServer(final InetAddress dnsServer) {
dnsServers.add(dnsServer);
return this;
}
@Override
public int describeContents() {
return 0;
public Builder addDnsServers(final Collection<? extends InetAddress> dnsServers) {
this.dnsServers.addAll(dnsServers);
return this;
}
public void generateKeypair() {
final Keypair keypair = new Keypair();
privateKey = keypair.getPrivateKey();
publicKey = keypair.getPublicKey();
notifyPropertyChanged(BR.privateKey);
notifyPropertyChanged(BR.publicKey);
public Interface build() throws BadConfigException {
if (keyPair == null)
throw new BadConfigException(Section.INTERFACE, Location.PRIVATE_KEY,
Reason.MISSING_ATTRIBUTE, null);
return new Interface(this);
}
@Bindable
public String getAddresses() {
return addresses;
public Builder excludeApplication(final String application) {
excludedApplications.add(application);
return this;
}
@Bindable
public String getDnses() {
return dnses;
public Builder excludeApplications(final Collection<String> applications) {
excludedApplications.addAll(applications);
return this;
}
@Bindable
public String getListenPort() {
return listenPort;
}
@Bindable
public String getMtu() {
return mtu;
}
@Bindable
public String getPrivateKey() {
return privateKey;
}
@Bindable
public String getPublicKey() {
return publicKey;
}
protected void loadData(final Interface parent) {
addresses = parent.getAddressString();
dnses = parent.getDnsString();
publicKey = parent.getPublicKey();
privateKey = parent.getPrivateKey();
listenPort = parent.getListenPortString();
mtu = parent.getMtuString();
}
public void setAddresses(final String addresses) {
this.addresses = addresses;
notifyPropertyChanged(BR.addresses);
}
public void setDnses(final String dnses) {
this.dnses = dnses;
notifyPropertyChanged(BR.dnses);
}
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;
public Builder parseAddresses(final CharSequence addresses) throws BadConfigException {
try {
publicKey = new Keypair(privateKey).getPublicKey();
} catch (final IllegalArgumentException ignored) {
publicKey = "";
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);
}
notifyPropertyChanged(BR.privateKey);
notifyPropertyChanged(BR.publicKey);
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeString(addresses);
dest.writeString(dnses);
dest.writeString(publicKey);
dest.writeString(privateKey);
dest.writeString(listenPort);
dest.writeString(mtu);
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;
}
}
}
@@ -0,0 +1,44 @@
/*
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.config;
import androidx.annotation.Nullable;
/**
*/
public class ParseException extends Exception {
private final Class<?> parsingClass;
private final CharSequence text;
public ParseException(final Class<?> parsingClass, final CharSequence text,
@Nullable final String message, @Nullable final Throwable cause) {
super(message, cause);
this.parsingClass = parsingClass;
this.text = text;
}
public ParseException(final Class<?> parsingClass, final CharSequence text,
@Nullable final String message) {
this(parsingClass, text, message, null);
}
public ParseException(final Class<?> parsingClass, final CharSequence text,
@Nullable final Throwable cause) {
this(parsingClass, text, null, cause);
}
public ParseException(final Class<?> parsingClass, final CharSequence text) {
this(parsingClass, text, null, null);
}
public Class<?> getParsingClass() {
return parsingClass;
}
public CharSequence getText() {
return text;
}
}
+262 -271
View File
@@ -1,315 +1,306 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.config;
import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import com.android.databinding.library.baseAdapters.BR;
import com.wireguard.crypto.KeyEncoding;
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.net.Inet6Address;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java9.util.Optional;
/**
* Represents the configuration for a WireGuard peer (a [Peer] block).
* 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.
*/
public final class Peer {
private final Set<InetNetwork> allowedIps;
private final Optional<InetEndpoint> endpoint;
private final Optional<Integer> persistentKeepalive;
private final Optional<Key> preSharedKey;
private final Key publicKey;
public class Peer {
private final List<InetNetwork> allowedIPsList;
private InetSocketAddress endpoint;
private int persistentKeepalive;
private String preSharedKey;
private String publicKey;
public Peer() {
allowedIPsList = new ArrayList<>();
private Peer(final Builder builder) {
// Defensively copy to ensure immutability even if the Builder is reused.
allowedIps = Collections.unmodifiableSet(new LinkedHashSet<>(builder.allowedIps));
endpoint = builder.endpoint;
persistentKeepalive = builder.persistentKeepalive;
preSharedKey = builder.preSharedKey;
publicKey = Objects.requireNonNull(builder.publicKey, "Peers must have a public key");
}
private void addAllowedIPs(final String[] allowedIPs) {
if (allowedIPs != null && allowedIPs.length > 0) {
for (final String allowedIP : allowedIPs) {
allowedIPsList.add(new InetNetwork(allowedIP));
/**
* Parses an series of "KEY = VALUE" lines into a {@code Peer}. 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 public key attribute
* @return a {@code Peer} with all of its attributes set from {@code lines}
*/
public static Peer 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.PEER, Location.TOP_LEVEL,
Reason.SYNTAX_ERROR, line));
switch (attribute.getKey().toLowerCase(Locale.ENGLISH)) {
case "allowedips":
builder.parseAllowedIPs(attribute.getValue());
break;
case "endpoint":
builder.parseEndpoint(attribute.getValue());
break;
case "persistentkeepalive":
builder.parsePersistentKeepalive(attribute.getValue());
break;
case "presharedkey":
builder.parsePreSharedKey(attribute.getValue());
break;
case "publickey":
builder.parsePublicKey(attribute.getValue());
break;
default:
throw new BadConfigException(Section.PEER, Location.TOP_LEVEL,
Reason.UNKNOWN_ATTRIBUTE, attribute.getKey());
}
}
}
public InetNetwork[] getAllowedIPs() {
return allowedIPsList.toArray(new InetNetwork[allowedIPsList.size()]);
}
private String getAllowedIPsString() {
if (allowedIPsList.isEmpty())
return null;
return Attribute.iterableToString(allowedIPsList);
}
public InetSocketAddress getEndpoint() {
return endpoint;
}
private String getEndpointString() {
if (endpoint == null)
return null;
return String.format(Locale.getDefault(), "%s:%d", endpoint.getHostString(), endpoint.getPort());
}
public int getPersistentKeepalive() {
return persistentKeepalive;
}
private String getPersistentKeepaliveString() {
if (persistentKeepalive == 0)
return null;
return Integer.valueOf(persistentKeepalive).toString();
}
public String getPreSharedKey() {
return preSharedKey;
}
public String getPublicKey() {
return publicKey;
}
public String getResolvedEndpointString() throws UnknownHostException {
if (endpoint == null)
throw new UnknownHostException("{empty}");
if (endpoint.isUnresolved())
endpoint = new InetSocketAddress(endpoint.getHostString(), endpoint.getPort());
if (endpoint.isUnresolved())
throw new UnknownHostException(endpoint.getHostString());
if (endpoint.getAddress() instanceof Inet6Address)
return String.format(Locale.getDefault(),
"[%s]:%d",
endpoint.getAddress().getHostAddress(),
endpoint.getPort());
return String.format(Locale.getDefault(),
"%s:%d",
endpoint.getAddress().getHostAddress(),
endpoint.getPort());
}
public void parse(final String line) {
final Attribute key = Attribute.match(line);
switch (key) {
case ALLOWED_IPS:
addAllowedIPs(key.parseList(line));
break;
case ENDPOINT:
setEndpointString(key.parse(line));
break;
case PERSISTENT_KEEPALIVE:
setPersistentKeepaliveString(key.parse(line));
break;
case PRESHARED_KEY:
setPreSharedKey(key.parse(line));
break;
case PUBLIC_KEY:
setPublicKey(key.parse(line));
break;
default:
throw new IllegalArgumentException(line);
}
}
private void setAllowedIPsString(final String allowedIPsString) {
allowedIPsList.clear();
addAllowedIPs(Attribute.stringToList(allowedIPsString));
}
private void setEndpoint(final InetSocketAddress endpoint) {
this.endpoint = endpoint;
}
private void setEndpointString(final String endpoint) {
if (endpoint != null && !endpoint.isEmpty()) {
final InetSocketAddress constructedEndpoint;
if (endpoint.indexOf('/') != -1 || endpoint.indexOf('?') != -1 || endpoint.indexOf('#') != -1)
throw new IllegalArgumentException("Forbidden characters in endpoint");
final URI uri;
try {
uri = new URI("wg://" + endpoint);
} catch (final URISyntaxException e) {
throw new IllegalArgumentException(e);
}
constructedEndpoint = InetSocketAddress.createUnresolved(uri.getHost(), uri.getPort());
setEndpoint(constructedEndpoint);
} else
setEndpoint(null);
}
private void setPersistentKeepalive(final int persistentKeepalive) {
this.persistentKeepalive = persistentKeepalive;
}
private void setPersistentKeepaliveString(final String persistentKeepalive) {
if (persistentKeepalive != null && !persistentKeepalive.isEmpty())
setPersistentKeepalive(Integer.parseInt(persistentKeepalive, 10));
else
setPersistentKeepalive(0);
}
private void setPreSharedKey(String preSharedKey) {
if (preSharedKey != null && preSharedKey.isEmpty())
preSharedKey = null;
if (preSharedKey != null)
KeyEncoding.keyFromBase64(preSharedKey);
this.preSharedKey = preSharedKey;
}
private void setPublicKey(String publicKey) {
if (publicKey != null && publicKey.isEmpty())
publicKey = null;
if (publicKey != null)
KeyEncoding.keyFromBase64(publicKey);
this.publicKey = publicKey;
return builder.build();
}
@Override
public boolean equals(final Object obj) {
if (!(obj instanceof Peer))
return false;
final Peer other = (Peer) obj;
return allowedIps.equals(other.allowedIps)
&& endpoint.equals(other.endpoint)
&& persistentKeepalive.equals(other.persistentKeepalive)
&& preSharedKey.equals(other.preSharedKey)
&& publicKey.equals(other.publicKey);
}
/**
* Returns the peer's set of allowed IPs.
*
* @return the set of allowed IPs
*/
public Set<InetNetwork> getAllowedIps() {
// The collection is already immutable.
return allowedIps;
}
/**
* Returns the peer's endpoint.
*
* @return the endpoint, or {@code Optional.empty()} if none is configured
*/
public Optional<InetEndpoint> getEndpoint() {
return endpoint;
}
/**
* Returns the peer's persistent keepalive.
*
* @return the persistent keepalive, or {@code Optional.empty()} if none is configured
*/
public Optional<Integer> getPersistentKeepalive() {
return persistentKeepalive;
}
/**
* Returns the peer's pre-shared key.
*
* @return the pre-shared key, or {@code Optional.empty()} if none is configured
*/
public Optional<Key> getPreSharedKey() {
return preSharedKey;
}
/**
* Returns the peer's public key.
*
* @return the public key
*/
public Key getPublicKey() {
return publicKey;
}
@Override
public int hashCode() {
int hash = 1;
hash = 31 * hash + allowedIps.hashCode();
hash = 31 * hash + endpoint.hashCode();
hash = 31 * hash + persistentKeepalive.hashCode();
hash = 31 * hash + preSharedKey.hashCode();
hash = 31 * hash + publicKey.hashCode();
return hash;
}
/**
* Converts the {@code Peer} into a string suitable for debugging purposes. The {@code Peer} is
* identified by its public key and (if known) its endpoint.
*
* @return a concise single-line identifier for the {@code Peer}
*/
@Override
public String toString() {
final StringBuilder sb = new StringBuilder().append("[Peer]\n");
if (!allowedIPsList.isEmpty())
sb.append(Attribute.ALLOWED_IPS.composeWith(allowedIPsList));
if (endpoint != null)
sb.append(Attribute.ENDPOINT.composeWith(getEndpointString()));
if (persistentKeepalive != 0)
sb.append(Attribute.PERSISTENT_KEEPALIVE.composeWith(persistentKeepalive));
if (preSharedKey != null)
sb.append(Attribute.PRESHARED_KEY.composeWith(preSharedKey));
if (publicKey != null)
sb.append(Attribute.PUBLIC_KEY.composeWith(publicKey));
final StringBuilder sb = new StringBuilder("(Peer ");
sb.append(publicKey.toBase64());
endpoint.ifPresent(ep -> sb.append(" @").append(ep));
sb.append(')');
return sb.toString();
}
public static class Observable extends BaseObservable implements Parcelable {
public static final Creator<Observable> CREATOR = new Creator<Observable>() {
@Override
public Observable createFromParcel(final Parcel in) {
return new Observable(in);
/**
* 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 toWgQuickString() {
final StringBuilder sb = new StringBuilder();
if (!allowedIps.isEmpty())
sb.append("AllowedIPs = ").append(Attribute.join(allowedIps)).append('\n');
endpoint.ifPresent(ep -> sb.append("Endpoint = ").append(ep).append('\n'));
persistentKeepalive.ifPresent(pk -> sb.append("PersistentKeepalive = ").append(pk).append('\n'));
preSharedKey.ifPresent(psk -> sb.append("PreSharedKey = ").append(psk.toBase64()).append('\n'));
sb.append("PublicKey = ").append(publicKey.toBase64()).append('\n');
return sb.toString();
}
/**
* 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 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');
for (final InetNetwork allowedIp : allowedIps)
sb.append("allowed_ip=").append(allowedIp).append('\n');
endpoint.flatMap(InetEndpoint::getResolved).ifPresent(ep -> sb.append("endpoint=").append(ep).append('\n'));
persistentKeepalive.ifPresent(pk -> sb.append("persistent_keepalive_interval=").append(pk).append('\n'));
preSharedKey.ifPresent(psk -> sb.append("preshared_key=").append(psk.toHex()).append('\n'));
return sb.toString();
}
@SuppressWarnings("UnusedReturnValue")
public static final class Builder {
// See wg(8)
private static final int MAX_PERSISTENT_KEEPALIVE = 65535;
// Defaults to an empty set.
private final Set<InetNetwork> allowedIps = new LinkedHashSet<>();
// Defaults to not present.
private Optional<InetEndpoint> endpoint = Optional.empty();
// Defaults to not present.
private Optional<Integer> persistentKeepalive = Optional.empty();
// Defaults to not present.
private Optional<Key> preSharedKey = Optional.empty();
// No default; must be provided before building.
@Nullable private Key publicKey;
public Builder addAllowedIp(final InetNetwork allowedIp) {
allowedIps.add(allowedIp);
return this;
}
public Builder addAllowedIps(final Collection<InetNetwork> allowedIps) {
this.allowedIps.addAll(allowedIps);
return this;
}
public Peer build() throws BadConfigException {
if (publicKey == null)
throw new BadConfigException(Section.PEER, Location.PUBLIC_KEY,
Reason.MISSING_ATTRIBUTE, null);
return new Peer(this);
}
public Builder parseAllowedIPs(final CharSequence allowedIps) throws BadConfigException {
try {
for (final String allowedIp : Attribute.split(allowedIps))
addAllowedIp(InetNetwork.parse(allowedIp));
return this;
} catch (final ParseException e) {
throw new BadConfigException(Section.PEER, Location.ALLOWED_IPS, e);
}
}
@Override
public Observable[] newArray(final int size) {
return new Observable[size];
public Builder parseEndpoint(final String endpoint) throws BadConfigException {
try {
return setEndpoint(InetEndpoint.parse(endpoint));
} catch (final ParseException e) {
throw new BadConfigException(Section.PEER, Location.ENDPOINT, e);
}
};
private String allowedIPs;
private String endpoint;
private String persistentKeepalive;
private String preSharedKey;
private String publicKey;
public Observable(final Peer parent) {
loadData(parent);
}
private Observable(final Parcel in) {
allowedIPs = in.readString();
endpoint = in.readString();
persistentKeepalive = in.readString();
preSharedKey = in.readString();
publicKey = in.readString();
public Builder parsePersistentKeepalive(final String persistentKeepalive)
throws BadConfigException {
try {
return setPersistentKeepalive(Integer.parseInt(persistentKeepalive));
} catch (final NumberFormatException e) {
throw new BadConfigException(Section.PEER, Location.PERSISTENT_KEEPALIVE,
persistentKeepalive, e);
}
}
public static Observable newInstance() {
return new Observable(new Peer());
public Builder parsePreSharedKey(final String preSharedKey) throws BadConfigException {
try {
return setPreSharedKey(Key.fromBase64(preSharedKey));
} catch (final KeyFormatException e) {
throw new BadConfigException(Section.PEER, Location.PRE_SHARED_KEY, e);
}
}
public void commitData(final Peer parent) {
parent.setAllowedIPsString(allowedIPs);
parent.setEndpointString(endpoint);
parent.setPersistentKeepaliveString(persistentKeepalive);
parent.setPreSharedKey(preSharedKey);
parent.setPublicKey(publicKey);
if (parent.getPublicKey() == null)
throw new IllegalArgumentException("Peer public key may not be empty");
loadData(parent);
notifyChange();
public Builder parsePublicKey(final String publicKey) throws BadConfigException {
try {
return setPublicKey(Key.fromBase64(publicKey));
} catch (final KeyFormatException e) {
throw new BadConfigException(Section.PEER, Location.PUBLIC_KEY, e);
}
}
@Override
public int describeContents() {
return 0;
public Builder setEndpoint(final InetEndpoint endpoint) {
this.endpoint = Optional.of(endpoint);
return this;
}
@Bindable
public String getAllowedIPs() {
return allowedIPs;
public Builder setPersistentKeepalive(final int persistentKeepalive)
throws BadConfigException {
if (persistentKeepalive < 0 || persistentKeepalive > MAX_PERSISTENT_KEEPALIVE)
throw new BadConfigException(Section.PEER, Location.PERSISTENT_KEEPALIVE,
Reason.INVALID_VALUE, String.valueOf(persistentKeepalive));
this.persistentKeepalive = persistentKeepalive == 0 ?
Optional.empty() : Optional.of(persistentKeepalive);
return this;
}
@Bindable
public String getEndpoint() {
return endpoint;
public Builder setPreSharedKey(final Key preSharedKey) {
this.preSharedKey = Optional.of(preSharedKey);
return this;
}
@Bindable
public String getPersistentKeepalive() {
return persistentKeepalive;
}
@Bindable
public String getPreSharedKey() {
return preSharedKey;
}
@Bindable
public String getPublicKey() {
return publicKey;
}
protected void loadData(final Peer parent) {
allowedIPs = parent.getAllowedIPsString();
endpoint = parent.getEndpointString();
persistentKeepalive = parent.getPersistentKeepaliveString();
preSharedKey = parent.getPreSharedKey();
publicKey = parent.getPublicKey();
}
public void setAllowedIPs(final String allowedIPs) {
this.allowedIPs = allowedIPs;
notifyPropertyChanged(BR.allowedIPs);
}
public void setEndpoint(final String endpoint) {
this.endpoint = endpoint;
notifyPropertyChanged(BR.endpoint);
}
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) {
public Builder setPublicKey(final Key publicKey) {
this.publicKey = publicKey;
notifyPropertyChanged(BR.publicKey);
}
@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);
return this;
}
}
}
@@ -1,10 +1,13 @@
/*
* Copyright © 2016 Southern Storm Software, Pty Ltd.
* SPDX-License-Identifier: MIT
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.crypto;
import androidx.annotation.Nullable;
import java.util.Arrays;
/**
@@ -93,7 +96,7 @@ public final class Curve25519 {
* if the base point of the curve should be used.
*/
public static void eval(final byte[] result, final int offset,
final byte[] privateKey, final byte[] publicKey) {
final byte[] privateKey, @Nullable final byte[] publicKey) {
final Curve25519 state = new Curve25519();
try {
// Unpack the public key value. If null, use 9 as the base point.
@@ -0,0 +1,269 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.crypto;
import com.wireguard.crypto.KeyFormatException.Type;
import java.security.SecureRandom;
import java.util.Arrays;
/**
* 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")
public final class Key {
private final byte[] key;
/**
* Constructs an object encapsulating the supplied key.
*
* @param key an array of bytes containing a binary key. Callers of this constructor are
* responsible for ensuring that the array is of the correct length.
*/
private Key(final byte[] key) {
// Defensively copy to ensure immutability.
this.key = Arrays.copyOf(key, key.length);
}
/**
* Decodes a single 4-character base64 chunk to an integer in constant time.
*
* @param src an array of at least 4 characters in base64 format
* @param srcOffset the offset of the beginning of the chunk in {@code src}
* @return the decoded 3-byte integer, or some arbitrary integer value if the input was not
* valid base64
*/
private static int decodeBase64(final char[] src, final int srcOffset) {
int val = 0;
for (int i = 0; i < 4; ++i) {
final char c = src[i + srcOffset];
val |= (-1
+ ((((('A' - 1) - c) & (c - ('Z' + 1))) >>> 8) & (c - 64))
+ ((((('a' - 1) - c) & (c - ('z' + 1))) >>> 8) & (c - 70))
+ ((((('0' - 1) - c) & (c - ('9' + 1))) >>> 8) & (c + 5))
+ ((((('+' - 1) - c) & (c - ('+' + 1))) >>> 8) & 63)
+ ((((('/' - 1) - c) & (c - ('/' + 1))) >>> 8) & 64)
) << (18 - 6 * i);
}
return val;
}
/**
* Encodes a single 4-character base64 chunk from 3 consecutive bytes in constant time.
*
* @param src an array of at least 3 bytes
* @param srcOffset the offset of the beginning of the chunk in {@code src}
* @param dest an array of at least 4 characters
* @param destOffset the offset of the beginning of the chunk in {@code dest}
*/
private static void encodeBase64(final byte[] src, final int srcOffset,
final char[] dest, final int destOffset) {
final byte[] input = {
(byte) ((src[srcOffset] >>> 2) & 63),
(byte) ((src[srcOffset] << 4 | ((src[1 + srcOffset] & 0xff) >>> 4)) & 63),
(byte) ((src[1 + srcOffset] << 2 | ((src[2 + srcOffset] & 0xff) >>> 6)) & 63),
(byte) ((src[2 + srcOffset]) & 63),
};
for (int i = 0; i < 4; ++i) {
dest[i + destOffset] = (char) (input[i] + 'A'
+ (((25 - input[i]) >>> 8) & 6)
- (((51 - input[i]) >>> 8) & 75)
- (((61 - input[i]) >>> 8) & 15)
+ (((62 - input[i]) >>> 8) & 3));
}
}
/**
* 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 a WireGuard key
* @return the decoded key encapsulated in an immutable container
*/
public static Key fromBase64(final String str) throws KeyFormatException {
final char[] input = str.toCharArray();
if (input.length != Format.BASE64.length || input[Format.BASE64.length - 1] != '=')
throw new KeyFormatException(Format.BASE64, Type.LENGTH);
final byte[] key = new byte[Format.BINARY.length];
int i;
int ret = 0;
for (i = 0; i < key.length / 3; ++i) {
final int val = decodeBase64(input, i * 4);
ret |= val >>> 31;
key[i * 3] = (byte) ((val >>> 16) & 0xff);
key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);
key[i * 3 + 2] = (byte) (val & 0xff);
}
final char[] endSegment = {
input[i * 4],
input[i * 4 + 1],
input[i * 4 + 2],
'A',
};
final int val = decodeBase64(endSegment, 0);
ret |= (val >>> 31) | (val & 0xff);
key[i * 3] = (byte) ((val >>> 16) & 0xff);
key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);
if (ret != 0)
throw new KeyFormatException(Format.BASE64, Type.CONTENTS);
return new Key(key);
}
/**
* 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 a WireGuard key in binary format
* @return the key encapsulated in an immutable container
*/
public static Key fromBytes(final byte[] bytes) throws KeyFormatException {
if (bytes.length != Format.BINARY.length)
throw new KeyFormatException(Format.BINARY, Type.LENGTH);
return new Key(bytes);
}
/**
* 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 a WireGuard key
* @return the decoded key encapsulated in an immutable container
*/
public static Key fromHex(final String str) throws KeyFormatException {
final char[] input = str.toCharArray();
if (input.length != Format.HEX.length)
throw new KeyFormatException(Format.HEX, Type.LENGTH);
final byte[] key = new byte[Format.BINARY.length];
int ret = 0;
for (int i = 0; i < key.length; ++i) {
int c;
int cNum;
int cNum0;
int cAlpha;
int cAlpha0;
int cVal;
final int cAcc;
c = input[i * 2];
cNum = c ^ 48;
cNum0 = ((cNum - 10) >>> 8) & 0xff;
cAlpha = (c & ~32) - 55;
cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff;
ret |= ((cNum0 | cAlpha0) - 1) >>> 8;
cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha);
cAcc = cVal * 16;
c = input[i * 2 + 1];
cNum = c ^ 48;
cNum0 = ((cNum - 10) >>> 8) & 0xff;
cAlpha = (c & ~32) - 55;
cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff;
ret |= ((cNum0 | cAlpha0) - 1) >>> 8;
cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha);
key[i] = (byte) (cAcc | cVal);
}
if (ret != 0)
throw new KeyFormatException(Format.HEX, Type.CONTENTS);
return new Key(key);
}
/**
* Generates a private key using the system's {@link SecureRandom} number generator.
*
* @return a well-formed random private key
*/
static Key generatePrivateKey() {
final SecureRandom secureRandom = new SecureRandom();
final byte[] privateKey = new byte[Format.BINARY.getLength()];
secureRandom.nextBytes(privateKey);
privateKey[0] &= 248;
privateKey[31] &= 127;
privateKey[31] |= 64;
return new Key(privateKey);
}
/**
* Generates a public key from an existing private key.
*
* @param privateKey a private key
* @return a well-formed public key that corresponds to the supplied private key
*/
static Key generatePublicKey(final Key privateKey) {
final byte[] publicKey = new byte[Format.BINARY.getLength()];
Curve25519.eval(publicKey, 0, privateKey.getBytes(), null);
return new Key(publicKey);
}
/**
* Returns the key as an array of bytes.
*
* @return an array of bytes containing the raw binary key
*/
public byte[] getBytes() {
// Defensively copy to ensure immutability.
return Arrays.copyOf(key, key.length);
}
/**
* Encodes the key to base64.
*
* @return a string containing the encoded key
*/
public String toBase64() {
final char[] output = new char[Format.BASE64.length];
int i;
for (i = 0; i < key.length / 3; ++i)
encodeBase64(key, i * 3, output, i * 4);
final byte[] endSegment = {
key[i * 3],
key[i * 3 + 1],
0,
};
encodeBase64(endSegment, 0, output, i * 4);
output[Format.BASE64.length - 1] = '=';
return new String(output);
}
/**
* Encodes the key to hexadecimal ASCII characters.
*
* @return a string containing the encoded key
*/
public String toHex() {
final char[] output = new char[Format.HEX.length];
for (int i = 0; i < key.length; ++i) {
output[i * 2] = (char) (87 + (key[i] >> 4 & 0xf)
+ ((((key[i] >> 4 & 0xf) - 10) >> 8) & ~38));
output[i * 2 + 1] = (char) (87 + (key[i] & 0xf)
+ ((((key[i] & 0xf) - 10) >> 8) & ~38));
}
return new String(output);
}
/**
* The supported formats for encoding a WireGuard key.
*/
public enum Format {
BASE64(44),
BINARY(32),
HEX(64);
private final int length;
Format(final int length) {
this.length = length;
}
public int getLength() {
return length;
}
}
}
@@ -1,158 +0,0 @@
/*
* Copyright © 2015-2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
*/
package com.wireguard.crypto;
/**
* This is a specialized constant-time base64 and hex implementation that resists side-channel attacks.
*/
@SuppressWarnings("MagicNumber")
public final class KeyEncoding {
public static final int KEY_LENGTH = 32;
public static final int KEY_LENGTH_BASE64 = 44;
public static final int KEY_LENGTH_HEX = 64;
private static final String KEY_LENGTH_BASE64_EXCEPTION_MESSAGE =
"WireGuard base64 keys must be 44 characters encoding 32 bytes";
private static final String KEY_LENGTH_EXCEPTION_MESSAGE =
"WireGuard keys must be 32 bytes";
private static final String KEY_LENGTH_HEX_EXCEPTION_MESSAGE =
"WireGuard hex keys must be 64 characters encoding 32 bytes";
private KeyEncoding() {
// Prevent instantiation.
}
private static int decodeBase64(final char[] src, final int srcOffset) {
int val = 0;
for (int i = 0; i < 4; ++i) {
final char c = src[i + srcOffset];
val |= (-1
+ ((((('A' - 1) - c) & (c - ('Z' + 1))) >>> 8) & (c - 64))
+ ((((('a' - 1) - c) & (c - ('z' + 1))) >>> 8) & (c - 70))
+ ((((('0' - 1) - c) & (c - ('9' + 1))) >>> 8) & (c + 5))
+ ((((('+' - 1) - c) & (c - ('+' + 1))) >>> 8) & 63)
+ ((((('/' - 1) - c) & (c - ('/' + 1))) >>> 8) & 64)
) << (18 - 6 * i);
}
return val;
}
private static void encodeBase64(final byte[] src, final int srcOffset,
final char[] dest, final int destOffset) {
final byte[] input = {
(byte) ((src[srcOffset] >>> 2) & 63),
(byte) ((src[srcOffset] << 4 | ((src[1 + srcOffset] & 0xff) >>> 4)) & 63),
(byte) ((src[1 + srcOffset] << 2 | ((src[2 + srcOffset] & 0xff) >>> 6)) & 63),
(byte) ((src[2 + srcOffset]) & 63),
};
for (int i = 0; i < 4; ++i) {
dest[i + destOffset] = (char) (input[i] + 'A'
+ (((25 - input[i]) >>> 8) & 6)
- (((51 - input[i]) >>> 8) & 75)
- (((61 - input[i]) >>> 8) & 15)
+ (((62 - input[i]) >>> 8) & 3));
}
}
public static byte[] keyFromBase64(final String str) {
final char[] input = str.toCharArray();
final byte[] key = new byte[KEY_LENGTH];
if (input.length != KEY_LENGTH_BASE64 || input[KEY_LENGTH_BASE64 - 1] != '=')
throw new IllegalArgumentException(KEY_LENGTH_BASE64_EXCEPTION_MESSAGE);
int i;
int ret = 0;
for (i = 0; i < KEY_LENGTH / 3; ++i) {
final int val = decodeBase64(input, i * 4);
ret |= val >>> 31;
key[i * 3] = (byte) ((val >>> 16) & 0xff);
key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);
key[i * 3 + 2] = (byte) (val & 0xff);
}
final char[] endSegment = {
input[i * 4],
input[i * 4 + 1],
input[i * 4 + 2],
'A',
};
final int val = decodeBase64(endSegment, 0);
ret |= (val >>> 31) | (val & 0xff);
key[i * 3] = (byte) ((val >>> 16) & 0xff);
key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);
if (ret != 0)
throw new IllegalArgumentException(KEY_LENGTH_BASE64_EXCEPTION_MESSAGE);
return key;
}
public static byte[] keyFromHex(final String str) {
final char[] input = str.toCharArray();
final byte[] key = new byte[KEY_LENGTH];
if (input.length != KEY_LENGTH_HEX)
throw new IllegalArgumentException(KEY_LENGTH_HEX_EXCEPTION_MESSAGE);
int ret = 0;
for (int i = 0; i < KEY_LENGTH_HEX; i += 2) {
int c;
int cNum;
int cNum0;
int cAlpha;
int cAlpha0;
int cVal;
final int cAcc;
c = input[i];
cNum = c ^ 48;
cNum0 = ((cNum - 10) >>> 8) & 0xff;
cAlpha = (c & ~32) - 55;
cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff;
ret |= ((cNum0 | cAlpha0) - 1) >>> 8;
cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha);
cAcc = cVal * 16;
c = input[i + 1];
cNum = c ^ 48;
cNum0 = ((cNum - 10) >>> 8) & 0xff;
cAlpha = (c & ~32) - 55;
cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff;
ret |= ((cNum0 | cAlpha0) - 1) >>> 8;
cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha);
key[i / 2] = (byte) (cAcc | cVal);
}
if (ret != 0)
throw new IllegalArgumentException(KEY_LENGTH_HEX_EXCEPTION_MESSAGE);
return key;
}
public static String keyToBase64(final byte[] key) {
final char[] output = new char[KEY_LENGTH_BASE64];
if (key.length != KEY_LENGTH)
throw new IllegalArgumentException(KEY_LENGTH_EXCEPTION_MESSAGE);
int i;
for (i = 0; i < KEY_LENGTH / 3; ++i)
encodeBase64(key, i * 3, output, i * 4);
final byte[] endSegment = {
key[i * 3],
key[i * 3 + 1],
0,
};
encodeBase64(endSegment, 0, output, i * 4);
output[KEY_LENGTH_BASE64 - 1] = '=';
return new String(output);
}
public static String keyToHex(final byte[] key) {
final char[] output = new char[KEY_LENGTH_HEX];
if (key.length != KEY_LENGTH)
throw new IllegalArgumentException(KEY_LENGTH_EXCEPTION_MESSAGE);
for (int i = 0; i < KEY_LENGTH; ++i) {
output[i * 2] = (char) (87 + (key[i] >> 4 & 0xf)
+ ((((key[i] >> 4 & 0xf) - 10) >> 8) & ~38));
output[i * 2 + 1] = (char) (87 + (key[i] & 0xf)
+ ((((key[i] & 0xf) - 10) >> 8) & ~38));
}
return new String(output);
}
}
@@ -0,0 +1,34 @@
/*
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
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.
*/
public final class KeyFormatException extends Exception {
private final Key.Format format;
private final Type type;
KeyFormatException(final Key.Format format, final Type type) {
this.format = format;
this.type = type;
}
public Key.Format getFormat() {
return format;
}
public Type getType() {
return type;
}
public enum Type {
CONTENTS,
LENGTH
}
}
@@ -0,0 +1,51 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.crypto;
/**
* Represents a Curve25519 key pair as used by WireGuard.
* <p>
* Instances of this class are immutable.
*/
public class KeyPair {
private final Key privateKey;
private final Key publicKey;
/**
* Creates a key pair using a newly-generated private key.
*/
public KeyPair() {
this(Key.generatePrivateKey());
}
/**
* Creates a key pair using an existing private key.
*
* @param privateKey a private key, used to derive the public key
*/
public KeyPair(final Key privateKey) {
this.privateKey = privateKey;
publicKey = Key.generatePublicKey(privateKey);
}
/**
* Returns the private key from the key pair.
*
* @return the private key
*/
public Key getPrivateKey() {
return privateKey;
}
/**
* Returns the public key from the key pair.
*
* @return the public key
*/
public Key getPublicKey() {
return publicKey;
}
}
@@ -1,56 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: GPL-2.0-or-later
*/
package com.wireguard.crypto;
import java.security.SecureRandom;
/**
* Represents a Curve25519 keypair as used by WireGuard.
*/
public class Keypair {
private final byte[] privateKey;
private final byte[] publicKey;
public Keypair() {
this(generatePrivateKey());
}
private Keypair(final byte[] privateKey) {
this.privateKey = privateKey;
publicKey = generatePublicKey(privateKey);
}
public Keypair(final String privateKey) {
this(KeyEncoding.keyFromBase64(privateKey));
}
@SuppressWarnings("MagicNumber")
private static byte[] generatePrivateKey() {
final SecureRandom secureRandom = new SecureRandom();
final byte[] privateKey = new byte[KeyEncoding.KEY_LENGTH];
secureRandom.nextBytes(privateKey);
privateKey[0] &= 248;
privateKey[31] &= 127;
privateKey[31] |= 64;
return privateKey;
}
private static byte[] generatePublicKey(final byte[] privateKey) {
final byte[] publicKey = new byte[KeyEncoding.KEY_LENGTH];
Curve25519.eval(publicKey, 0, privateKey, null);
return publicKey;
}
public String getPrivateKey() {
return KeyEncoding.keyToBase64(privateKey);
}
public String getPublicKey() {
return KeyEncoding.keyToBase64(publicKey);
}
}
@@ -1,6 +1,6 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.util;
@@ -1,10 +1,12 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* SPDX-License-Identifier: GPL-2.0-or-later
* 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;
@@ -18,8 +20,10 @@ public interface KeyedList<K, E extends Keyed<? extends K>> extends List<E> {
boolean containsKey(K key);
@Nullable
E get(K key);
@Nullable
E getLast(K key);
int indexOfKey(K key);
@@ -0,0 +1,26 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.util;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.annotation.Nonnull;
import javax.annotation.meta.TypeQualifierDefault;
/**
* 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.
*/
@Documented
@Nonnull
@TypeQualifierDefault({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface NonNullForAll {
}
@@ -1,10 +1,12 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* SPDX-License-Identifier: GPL-2.0-or-later
* 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;
@@ -17,10 +19,12 @@ import java.util.Set;
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();
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

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