Compare commits

...

553 Commits

Author SHA1 Message Date
Jason A. Donenfeld f670ff22c6 version: bump
A Christmas eve special.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-24 14:30:11 +01:00
Jason A. Donenfeld cb3194f10a tunnel: bump libwg-go
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-23 22:55:56 +01:00
Jason A. Donenfeld f6b2bbf433 strings: sync with crowdin
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-21 17:52:24 +01:00
Harsh Shandilya 56a4862442 build: upgrade to MDC 1.3.0-beta01
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-12-17 14:22:43 +05:30
Jason A. Donenfeld 20390d65c8 version: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-16 19:09:21 +01:00
Jason A. Donenfeld 177457e67b tunnel: bump libwg-go
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-16 19:04:17 +01:00
Harsh Shandilya 8caec4d739 build: downgrade Jetpack Datastore to 1.0.0-alpha02
We're hitting occasional build failures with the newer versions that have not been triaged yet

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-12-16 19:03:43 +01:00
Harsh Shandilya fb819b99a4 build: upgrade AGP, Kotlin, core-ktx and mdc-android
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-12-16 18:55:08 +01:00
Harsh Shandilya fe82037f06 build: upgrade activity and fragment to latest betas
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-12-16 18:55:08 +01:00
Harsh Shandilya c2aa1b21f8 build: upgrade datastore dependency
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-12-03 01:26:48 +05:30
Harsh Shandilya d69415b55a build: upgrade dependencies
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-11-30 22:38:17 +05:30
Harsh Shandilya 4fae2d1255 ui: show all apps with internet permission in exclusions list
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-11-16 15:45:19 +05:30
Jason A. Donenfeld a300f269f1 ui: test for any camera, not just rear one
Some folks use chromebooks, which don't have rear cameras.

Reported-by: Jay Tuley <jay.tuley@ekonbenefits.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-11-10 14:11:48 +01:00
Harsh Shandilya 961cba3f7c build: upgrade runtime dependencies
Updates ConstraintLayout, kotlinx.coroutines, Jetpack DataStore, JUnit and Lifecycle-Runtime-KTX

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-11-09 17:01:54 +05:30
Harsh Shandilya 6bc7386bff strings: sync translations
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-11-07 19:13:43 +05:30
Harsh Shandilya 5fa08f286e build: add task to sync localisations with Crowdin
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-11-07 19:13:42 +05:30
Harsh Shandilya 35f868733c build: switch to Gradle's maven-publish plugin
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-11-07 18:53:06 +05:30
Jason A. Donenfeld e71b3d2583 ToolsInstaller: unbreak cleanup
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-10-27 13:35:07 +01:00
Jason A. Donenfeld 755148242c tunnel: do not constantly raise toasts when process is opportunistically killed
Modern Android likes to kill processes to free ram and resources. When
kernel-mode WireGuard is in use, this is quite alright with us, since
the app doesn't actually need to consume any resources at all in order
for the tunnel to run. So, we want to allow and encourage this resource
frugality. However, when the quick settings tile is being used or when
the app is referenced otherwise, the app will occasionally be restarted,
to, for example, repaint the quick settings tile. This is also fine, as
the process winds up being short-lived again. But, since process
initialization means asking for a new root shell in order to check on
kernel-mode WireGuard, this means that Magisk raises a systemwide toast.
On some phones, this happens each and every time that the notification
shade is pulled down. It's not only annoying but it sometimes obscures
other notifications that users want to see, prompting their pulling down
of the notification shade in the first place. In order to get rid of
this nuisance, just disable these notifications and extraneous logs, so
that we don't clutter the system every time that the process is
opportunistically killed and restarted.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-10-26 22:44:35 +01:00
Jason A. Donenfeld 15fea6f02f tunnel: clean up some docstring wording
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-10-20 16:35:05 +02:00
Harsh Shandilya cb2842e8ef build: upgrade to Gradle 6.7
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-10-15 10:01:53 +05:30
Jason A. Donenfeld 106b67d892 build: add crowdin syncer script and use it
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-10-14 17:16:36 +02:00
Harsh Shandilya 996587f792 build: update to AGP 4.1.0
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-10-13 16:18:49 +02:00
Jason A. Donenfeld 3a4bf35c77 README: mention desugaring
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-10-08 15:03:49 +02:00
Harsh Shandilya 46b37c0c26 build: update AGP and ConstraintLayout
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-10-07 14:14:38 +05:30
Jason A. Donenfeld 5b5ba88a97 tunnel: use more subtle roaming escape hatch
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-10-07 10:21:02 +02:00
Jason A. Donenfeld ceb3095a0a build: update to mdc 1.3.0-alpha03
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-10-05 21:46:03 +02:00
Jason A. Donenfeld a31f0cf788 DownloadsFileSaver: initialize callback in constructor, not on the fly
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-10-02 12:11:48 +02:00
Jason A. Donenfeld 1dc74b171c build: upgrade AndroidX biometric
The BiometricConstants class was removed and these were folded into
BiometricPrompt.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-10-02 11:35:53 +02:00
Harsh Shandilya 9266487fe5 build: update AndroidX activity/fragments and resolve compile failure
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-10-02 04:28:25 +05:30
Harsh Shandilya 5d7ce139bc ui: use commit extension from fragment-ktx
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-09-30 22:42:05 +05:30
Jason A. Donenfeld ddb6c87ebf ui: account for binding disappearing on detail fragment
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-30 12:47:29 +02:00
Jason A. Donenfeld 8a6f8f73cd version: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-27 16:30:30 +02:00
Jason A. Donenfeld f4fc15538d tv: hack gridlayoutmanager to fill columns before row if we're not scrolling
If we're horizontally scrolling, it makes sense to fill rows before
columns. But if it all fits in one page and we don't need to scroll
horizontally, it looks ridiculous. So, in this case, rearrange the tiles
so that it appears to fill columns before rows. But we don't want things
suddenly jumping around, so actually, keep the same ordering as
rows-before-columns, but add invisible spaces after certain items, so
that the fill area makes it look as though it's columns-before-rows.
This winds up being much more visually pleasing.

We do this by figuring out this kind of transformation:

If we convert this matrix:

	0 3 6
	1 4 _
	2 5 _

To this one:

	0 2 4 6
	1 3 5 _
	_ _ _ _

For a given index, how many spaces are under it? This changes depending
on how many total are in a grid. Going from 3x3 to 4x3, for example, we
have:

	count == 12, index =
	count == 11, index = 10
	count == 10, index = 7,9
	count == 9, index = 4,6,8
	count == 8, index = 1,3,5,7
	count == 7, index = 1,3,5,6!
	count == 6, index = 1,3,4!,5!
	count == 5, index = 1,2!,3!,4!
	count == 4, index = 0!,1!,2!,3!
	count == 3, index = 0!,1!,2!
	count == 2, index = 0!,1!
	count == 1, index = 0!
	count == 0, index =

The '!' means two blanks below, no '!' means one blank below, and no
mention means no blanks below.

This commit adds code to compute such a table on the fly.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-27 13:17:56 +02:00
Jason A. Donenfeld 938399d881 ui: queue up tunnel mutating on activity scope instead of fragment scope
Fragment scopes get cancelled when the fragment goes away, but we don't
actually want to cancel an in-flight transition in that case. Also,
before when the fragment would cancel, there'd be an exception, and the
exception handler would call Fragment::getString, which in turn called
requireContext, which caused an exception. Work around this by using the
`activity ?: Application.get()` idiom to always have a context for
strings and toasts.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-26 13:49:14 +02:00
Jason A. Donenfeld 53ca421a85 ui: print proper exception trace from log viewer
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-26 12:01:58 +02:00
Jason A. Donenfeld 32778d1c03 ui: request intent permissions from hidden activity
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-26 11:44:24 +02:00
Jason A. Donenfeld a870bf6e04 version: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-24 14:12:51 +02:00
Jason A. Donenfeld 7a8f708157 tv: handle going up directories better
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-24 14:12:40 +02:00
Jason A. Donenfeld e729c5dc51 tv: show volume descriptions for file picker
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-24 14:12:40 +02:00
Jason A. Donenfeld 4bf34c49b7 ui: account for null data in callback
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-24 14:12:40 +02:00
Jason A. Donenfeld 05511d4900 ui: cleanup code after churn
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-23 17:56:37 +02:00
Jason A. Donenfeld 15da17b595 tv: use system picker for API 29+
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-23 17:56:37 +02:00
Jason A. Donenfeld b3c43e428f tv: use our own file picker
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-23 17:56:37 +02:00
Jason A. Donenfeld 7bec539722 tv: escape deletion view with back button
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-23 17:56:37 +02:00
Jason A. Donenfeld a8dfebb086 tv: select first item after toggling deletion mode
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-22 23:59:40 +02:00
Jason A. Donenfeld e72b4fc144 tv: hook up isFocused as observable property
This is kind of ridiculous, since the items own state should clearly be
queryable, but it doesn't appear to be the case here, so just shuffle it
around into kotlin and back.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-22 23:54:41 +02:00
Jason A. Donenfeld 03189e7b20 tv: add text when there are no tunnels
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-22 23:54:38 +02:00
Jason A. Donenfeld 10bb413187 tv: make cards slightly smaller
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-22 23:54:36 +02:00
Jason A. Donenfeld 1c814310b9 tv: select the right thing on load
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-22 23:54:32 +02:00
Harsh Shandilya 3fe9e3162f tv: tweak TV layout to fit 3 rows better
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-09-22 23:54:30 +02:00
Harsh Shandilya 6da6f7886a tv: set layout manager from XML
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-09-22 23:54:26 +02:00
Jason A. Donenfeld 8c2029870f tv: make logo almost better
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-22 23:54:23 +02:00
Harsh Shandilya a5031a44a0 tv: anchor RV bottom to top of delete button
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-09-22 23:54:20 +02:00
Jason A. Donenfeld 44b27fe472 tv: remove useless attribute
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-22 23:54:17 +02:00
Jason A. Donenfeld 93fb3b345b tv: use plus instead of text for importing
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-22 23:54:14 +02:00
Harsh Shandilya 8b596697b7 tv: do theming
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-09-22 23:54:04 +02:00
Jason A. Donenfeld c536bbb7e9 tv: account for broken TVs with no file picker
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-22 23:54:01 +02:00
Jason A. Donenfeld a978aac129 tv: remove tiny words from tv banner
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-22 23:53:57 +02:00
Jason A. Donenfeld eb8cab4110 tv: do not redisplay stats when deleting
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-22 23:53:54 +02:00
Jason A. Donenfeld 0a36d9a5e9 tv: add tv banner
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-22 23:53:51 +02:00
Jason A. Donenfeld 309571039d tv: use proper icon for button
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-22 23:53:49 +02:00
Jason A. Donenfeld d56f2fb1bb tv: hide deletion button when nothing to delete
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-22 23:53:47 +02:00
Jason A. Donenfeld 9df8e5e239 tv: add ugly deletion mode
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-22 23:53:44 +02:00
Jason A. Donenfeld 444a86cc9f tv: wire in stats
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-22 23:53:40 +02:00
Jason A. Donenfeld 382e10e103 tv: wire up tunnel start/stop
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-22 23:53:37 +02:00
Jason A. Donenfeld dc002d77fa tv: begin to wire up databindings
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-22 23:53:32 +02:00
Jason A. Donenfeld aaa55c0dcc tv: abstract out tunnel importing
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-22 23:53:30 +02:00
Harsh Shandilya 0ad3781ae5 tv: initial draft of Android TV support
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-09-22 23:53:27 +02:00
Jason A. Donenfeld d738161a2e Statistics: only do one hash lookup
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-21 11:16:33 +02:00
Jason A. Donenfeld 52c2e9cd24 TunnelManager: catch exception in intent receiver
java.lang.IllegalStateException:
  at android.app.ContextImpl.startServiceCommon (ContextImpl.java:1720)
  at android.app.ContextImpl.startService (ContextImpl.java:1675)
  at android.content.ContextWrapper.startService (ContextWrapper.java:669)
  at com.wireguard.android.backend.GoBackend.startVpnService (GoBackend.java:4)
  at com.wireguard.android.backend.GoBackend.setStateInternal (GoBackend.java:4)
  at com.wireguard.android.backend.GoBackend.setState (GoBackend.java:2)
  at com.wireguard.android.model.TunnelManager$setTunnelState$2$1.invokeSuspend (TunnelManager.java:6)
  at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (BaseContinuationImpl.java:2)
  at kotlinx.coroutines.DispatchedTask.run (DispatchedTask.java:2)
  at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely (CoroutineScheduler.java)
  at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask (CoroutineScheduler.java:7)
  at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker (CoroutineScheduler.java:7)
  at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run (CoroutineScheduler.java:7)

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-21 10:54:07 +02:00
Jason A. Donenfeld 5fd1a32ae4 TunnelEditorFragment: do not assume a context
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-20 18:08:58 +02:00
Jason A. Donenfeld 655a853857 TunnelListFragment: do not assume binding always exists
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-20 18:05:43 +02:00
Jason A. Donenfeld 847da23300 TunnelDetailFragment: use kotlin coroutine for timer and rework nullability
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-20 18:03:58 +02:00
Jason A. Donenfeld d5c07374ff BaseFragment: avoid using requireContext() in permission result callback
java.lang.IllegalStateException:
  at androidx.fragment.app.Fragment.requireContext (Fragment.java:17)
  at com.wireguard.android.fragment.BaseFragment$setTunnelStateWithPermissionsResult$1.invokeSuspend (BaseFragment.java:4)
  at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (BaseContinuationImpl.java:2)
  at kotlinx.coroutines.UndispatchedCoroutine.afterResume (UndispatchedCoroutine.java:19)
  at kotlinx.coroutines.AbstractCoroutine.resumeWith (AbstractCoroutine.java:13)
  at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (BaseContinuationImpl.java:2)
  at kotlinx.coroutines.UndispatchedCoroutine.afterResume (UndispatchedCoroutine.java:19)
  at kotlinx.coroutines.AbstractCoroutine.resumeWith (AbstractCoroutine.java:13)
  at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (BaseContinuationImpl.java:2)
  at kotlinx.coroutines.DispatchedTask.run (DispatchedTask.java:2)
  at android.os.Handler.handleCallback (Handler.java:790)
  at android.os.Handler.dispatchMessage (Handler.java:99)
  at android.os.Looper.loop (Looper.java:164)
  at android.app.ActivityThread.main (ActivityThread.java:7025)
  at java.lang.reflect.Method.invoke (Method.java)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:441)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1408)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Reported-by: Jason A. Donenfeld <Jason@zx2c4.com>
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-06 01:22:44 +05:30
Harsh Shandilya 065893e31d MultiselectableRelativeLayout: use JvmOverloads constructor
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-06 00:39:22 +05:30
Harsh Shandilya ef70aa88e1 ui: fix scrolling in detail view
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-06 00:17:14 +05:30
Harsh Shandilya 6a11a199ca strings: sync translations
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-04 16:19:21 +05:30
Jason A. Donenfeld 7b5ceac9f7 version: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-04-03 19:07:33 -06:00
Jason A. Donenfeld 9fe0019ec1 tunnel: libwg-go: use deterministic verdef name
From the ld.gold source:

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

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

This commit makes wireguard-android reproducible.

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

This reverts commit cd43444d1f.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

So this string should be explicit indexed.

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

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

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-23 18:14:23 -06:00
Harsh Shandilya 383659fb8a Upgrade to Kotlin 1.3.71
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-03-23 22:50:04 +05:30
Jason A. Donenfeld 4725e55090 libwg-go: update go modules
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-22 14:08:20 -06:00
Jason A. Donenfeld 07c85ee6f5 version: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-22 13:09:09 -06:00
Jason A. Donenfeld c02dd9e040 Preferences: hide advanced settings
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-22 01:50:58 -06:00
Jason A. Donenfeld 7dae94976d SettingsActivity: remove from parent instead of fixed screen
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-22 01:23:23 -06:00
Jason A. Donenfeld 38c360cb74 libwg-go: bump go version
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-03-22 01:07:20 -06:00
Jason A. Donenfeld edba640641 TunnelManager: enable Tasker support
Also fix up wording for the permission prompt that Tasker will show when
initially setting this up.

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

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

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

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

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

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

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

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

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

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

Remember to add this to new classes!

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-02-10 16:55:34 +01:00
Alexey 66b46c8618 Update Russian translation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-02-09 11:27:30 +01:00
Jason A. Donenfeld 04689d37b7 version: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-02-06 22:01:03 +01:00
Jason A. Donenfeld e041cacf4b wg: bump to latest snapshot
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-02-06 22:01:03 +01:00
Alexey f54fc92b14 Add Russian translation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-02-06 22:01:03 +01:00
Jason A. Donenfeld 6e7b6bcc35 wg-go: bump go requirement
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-02-03 17:21:34 +01:00
Jason A. Donenfeld e70d5be535 wg: remove mnl dependency and bump dep
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-02-03 17:09:45 +01:00
Jason A. Donenfeld 2ce51c8c2e version: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-01-24 15:36:50 +01:00
Jason A. Donenfeld c621ec0c50 GoBackend: restrict APIs to proper version
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-01-24 15:36:27 +01:00
Harsh Shandilya 2ed2a1431d Upgrade to Gradle 6.1
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-01-24 09:57:25 +05:30
Harsh Shandilya 3a425394ca GoBackend: Ensure we're unmetered on API 29 as well
VPN apps targetting Android 10 are treated as metered by default.

Source: https://developer.android.com/reference/android/net/VpnService.Builder.html#setMetered(boolean)

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-01-24 09:51:48 +05:30
Jason A. Donenfeld d98ba463ad version: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-01-23 16:12:26 +01:00
Jason A. Donenfeld c3d97acb31 Rework timer in tunnel detail
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-01-23 16:12:26 +01:00
Jason A. Donenfeld 8dbd464fa4 Match lowercase asus phones for fab hack
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-01-23 16:10:34 +01:00
Jason A. Donenfeld 63a5bb1bbf manifest: reorder
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-01-23 16:10:34 +01:00
Jason A. Donenfeld 8c03878808 tools: update deps
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-01-23 13:46:29 +01:00
Jason A. Donenfeld d29e50e50b GoBackend: set empty underlying networks
https://lists.zx2c4.com/pipermail/wireguard/2020-January/004859.html
https://issuetracker.google.com/issues/114309459
https://developer.android.com/about/versions/pie/android-9.0-changes-all#network-capabilities-vpn

Apparently we need to call this at least once.

Reported-by: Andrey Kupreychik <foxel@quickfox.ru>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-01-23 13:45:08 +01:00
Revath S Kumar 687bf8b208 Manifest: make wireguard compatible with android TV
As of now wireguard is not listed in Android TV play store
due to the lack of CATEGORY_LEANBACK_LAUNCHER [1].
Even the app is not listed when we sideload into TV device[2].

[1]: https://developer.android.com/reference/android/content/Intent.html#CATEGORY_LEANBACK_LAUNCHER
[2]: https://developer.android.com/training/tv/start/start.html#tv-activity

Signed-off-by: Revath S Kumar <rsk@revathskumar.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-01-22 20:56:49 +01:00
xalloc 10d0807395 Add Italian translation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-01-22 20:56:06 +01:00
Jason A. Donenfeld 0a89c87190 tools: bump to new wireguard-tools repo
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-12-27 16:49:30 +01:00
Jason A. Donenfeld a7df92a64c Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-12-20 11:31:58 +01:00
Harsh Shandilya 4d3043c041 Introduce TunnelToggleActivity
On Android 10, apps cannot start services when they're in the
background. This means that starting VpnService from within
QuickTileService when the app is not active ends badly. To mitigate this
situation, we introduce a proxy activity of sorts that will handle
starting VpnService for us. The activity is completely transparent and
invisible, and does only four things:

- Toggle the tunnel state
- Request the Tile bound by QuickTileService to refresh its state
- Handle any error that might have been thrown during toggle
- Call finishAffinity() and go away

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2019-12-20 11:26:33 +01:00
Jason A. Donenfeld 8261a18472 Use RequiresApi instead of TargetApi
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-12-20 11:14:38 +01:00
Harsh Shandilya a9f04c0bf4 Update AGP to 3.5.3
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2019-12-15 11:08:00 +05:30
Harsh Shandilya 84334a6bc9 Update Gradle to 6.0.1
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2019-12-15 11:07:59 +05:30
Jason A. Donenfeld 1e5596f977 QuickTileService: require phone be unlocked
Reported-by: Simon <simon@laro.se>
Reported-by: Harsh Shandilya <me@msfjarvis.dev>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-11-27 13:12:12 +01:00
Jason A. Donenfeld b67fa3a38c Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-11-20 12:44:54 +01:00
Jason A. Donenfeld 8b0123042f Implement statistics
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-11-20 12:44:54 +01:00
Jason A. Donenfeld 16890a659e ModuleLoader: sync file before renaming
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-22 15:05:22 +02:00
Jason A. Donenfeld d40ac7f89d ToolsInstaller: write to temporary file, fsync, rename
Reported-by: Andre Christanto <christantoandre@gmail.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-21 18:56:28 +02:00
Jason A. Donenfeld 2694f48b87 libwg-go: version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-18 14:57:12 +02:00
Jason A. Donenfeld 0f91aeb2d3 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-18 13:37:59 +02:00
Jason A. Donenfeld bc0111f895 InetAddresses: cleanup and implement final fallback
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-18 13:37:07 +02:00
Jason A. Donenfeld f8a3e9b332 libwg-go: version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-18 13:10:08 +02:00
Jason A. Donenfeld db9397fd3e Application: put user agent in log to help debugging
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-18 13:06:54 +02:00
Jason A. Donenfeld 20717ff128 Suppress depreciation warnings
We know what we're doing here, and it's not nice.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-18 13:06:54 +02:00
Jason A. Donenfeld a532a88585 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-16 14:46:23 +02:00
Jason A. Donenfeld 0b077bd523 tools: bump wg-quick
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-16 14:46:10 +02:00
Jason A. Donenfeld e008efcf97 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-16 10:03:47 +02:00
Jason A. Donenfeld 7cf676f9bd Google doesn't want to enable others to support free open source software
Revert "preferences: add donation link"

This reverts commit e5455f579aec48abb30ba68b0248b02d79303126.

The app was removed from the Play Store for violating their payments
policy. Upon filing an appeal, I was told that they do not allow
donations to projects like WireGuard.

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

October has arrived.

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

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

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

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

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

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

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

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

This commit also adds explicit focusability hints for the Switch.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-30 14:28:27 +02:00
Jason A. Donenfeld f35e059194 MonkeyedSnackbar: fix typos
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 19:17:10 +02:00
304 changed files with 19170 additions and 9346 deletions
+2 -2
View File
@@ -6,7 +6,7 @@
/captures/
/local.properties
.DS_Store
.externalNativeBuild/
.cxx/
Thumbs.db
build/
*.apk
@@ -15,4 +15,4 @@ build/
*.iml
*.jks
keystore.properties
package-info.java
gradlew.bat
+3 -9
View File
@@ -1,9 +1,3 @@
[submodule "app/tools/libmnl"]
path = app/tools/libmnl
url = https://git.netfilter.org/libmnl/
[submodule "app/tools/wireguard"]
path = app/tools/wireguard
url = https://git.zx2c4.com/WireGuard
[submodule "app/tools/wireguard-go"]
path = app/tools/wireguard-go
url = https://git.zx2c4.com/wireguard-go
[submodule "tunnel/tools/wireguard-tools"]
path = tunnel/tools/wireguard-tools
url = https://git.zx2c4.com/wireguard-tools
+32 -93
View File
@@ -29,16 +29,10 @@
<emptyLine />
</value>
</option>
<option name="RIGHT_MARGIN" value="100" />
<AndroidXmlCodeStyleSettings>
<option name="USE_CUSTOM_SETTINGS" value="true" />
</AndroidXmlCodeStyleSettings>
<JavaCodeStyleSettings>
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
<value />
</option>
<option name="GENERATE_FINAL_LOCALS" value="true" />
<option name="GENERATE_FINAL_PARAMETERS" value="true" />
<option name="INSERT_INNER_CLASS_IMPORTS" value="true" />
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="android" withSubpackages="true" static="false" />
@@ -62,35 +56,25 @@
</value>
</option>
</JavaCodeStyleSettings>
<Objective-C-extensions>
<file>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
</file>
<class>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
</class>
<extensions>
<pair source="cpp" header="h" fileNamingConvention="NONE" />
<pair source="c" header="h" fileNamingConvention="NONE" />
</extensions>
</Objective-C-extensions>
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="10" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="10" />
</JetCodeStyleSettings>
<XML>
<option name="XML_KEEP_LINE_BREAKS" value="false" />
<option name="XML_ALIGN_ATTRIBUTES" value="false" />
<option name="XML_SPACE_INSIDE_EMPTY_TAG" value="true" />
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
</XML>
<codeStyleSettings language="JAVA">
@@ -382,7 +366,6 @@
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
@@ -393,6 +376,7 @@
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@@ -403,6 +387,7 @@
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@@ -414,6 +399,7 @@
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
@@ -424,6 +410,7 @@
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
@@ -434,6 +421,7 @@
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@@ -444,6 +432,7 @@
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@@ -454,6 +443,7 @@
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@@ -464,64 +454,12 @@
<rule>
<match>
<AND>
<NAME>.*:layout_width</NAME>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_height</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:width</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:height</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
@@ -529,6 +467,7 @@
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
+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: Apache-2.0
<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>
+2 -2
View File
@@ -1,3 +1,3 @@
<component name="CopyrightManager">
<settings default="GPL-2.0-or-later" />
</component>
<settings default="Default" />
</component>
+14 -14
View File
@@ -9,7 +9,6 @@
</option>
<option name="nonThreadSafeTypes" value="" />
</inspection_tool>
<inspection_tool class="AndroidLintGoogleAppIndexingWarning" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="AndroidLintIconExpectedSize" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AndroidLintNegativeMargin" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AndroidLintTypographyQuotes" enabled="true" level="WARNING" enabled_by_default="true" />
@@ -40,7 +39,9 @@
<inspection_tool class="AssignmentToLambdaParameter" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AssignmentToSuperclassField" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AssignmentUsedAsCondition" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true">
<option name="METHOD_MATCHER_CONFIG" value="java.util.Formatter,format,java.io.Writer,append,com.google.common.base.Preconditions,checkNotNull,org.hibernate.Session,close,java.io.PrintWriter,printf,java.util.HashMap,put,java.util.Map,put" />
</inspection_tool>
<inspection_tool class="BadExceptionCaught" enabled="true" level="WARNING" enabled_by_default="true">
<option name="exceptionsString" value="" />
<option name="exceptions">
@@ -59,6 +60,9 @@
<inspection_tool class="CallToStringConcatCanBeReplacedByOperator" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CannotResolve" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="CastConflictsWithInstanceof" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CatchMayIgnoreException" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreCatchBlocksWithComments" value="false" />
</inspection_tool>
<inspection_tool class="ChainedEquality" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ClassInitializer" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ClassNameDiffersFromFileName" enabled="true" level="WARNING" enabled_by_default="true" />
@@ -103,9 +107,6 @@
<inspection_tool class="DoubleLiteralMayBeFloatLiteral" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DuplicateAlternationBranch" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DuplicateBooleanBranch" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DuplicateCondition" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreSideEffectConditions" value="true" />
</inspection_tool>
<inspection_tool class="DuplicateDeclarations" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="DynamicRegexReplaceableByCompiledPattern" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ElementOnlyUsedFromTestCode" enabled="false" level="WARNING" enabled_by_default="false" />
@@ -196,7 +197,6 @@
<inspection_tool class="ImplicitSubclassInspection" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="IncompatibleTypes" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="InitializerIssues" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="InnerClassMayBeStatic" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="InnerClassReferencedViaSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="InnerClassVariableHidesOuterClassVariable" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreInvisibleFields" value="true" />
@@ -215,7 +215,6 @@
<inspection_tool class="InstanceofThis" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="InstantiationOfUtilityClass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="IntLiteralMayBeLongLiteral" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="IntegerDivisionInFloatingPointContext" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="IntegerTypeRequired" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="InterfaceMayBeAnnotatedFunctional" enabled="true" level="INFORMATION" enabled_by_default="true" />
<inspection_tool class="InterfaceNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
@@ -294,6 +293,7 @@
<option name="ignoreForLoopDeclarations" value="true" />
</inspection_tool>
<inspection_tool class="MultipleTopLevelClassesInFile" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="MultipleVariablesInDeclaration" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NamedResource" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="NativeMethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="NegatedConditional" enabled="true" level="WARNING" enabled_by_default="true">
@@ -347,7 +347,6 @@
<inspection_tool class="OverriddenMethodCallDuringObjectConstruction" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PackageInfoWithoutPackage" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PointerTypeRequired" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="PointlessNullCheck" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ProblematicVarargsMethodOverride" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ProtectedField" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ProtectedInnerClass" enabled="true" level="WARNING" enabled_by_default="true">
@@ -385,7 +384,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" />
@@ -395,7 +393,13 @@
<inspection_tool class="SizeReplaceableByIsEmpty" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StaticCallOnSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StaticFieldReferenceOnSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StaticImport" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StaticImport" enabled="true" level="WARNING" enabled_by_default="true">
<option name="allowedClasses">
<set>
<option value="org.junit.Assert" />
</set>
</option>
</inspection_tool>
<inspection_tool class="StaticInheritance" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StaticMethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
<option name="m_regex" value="[a-z][A-Za-z\d]*" />
@@ -433,7 +437,6 @@
<inspection_tool class="TemplateArgumentsIssues" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="TestMethodWithoutAssertion" enabled="false" level="WARNING" enabled_by_default="false">
<option name="assertionMethods" value="org.junit.Assert,assert.*|fail.*,junit.framework.Assert,assert.*|fail.*,org.junit.jupiter.api.Assertions,assert.*|fail.*,org.mockito.Mockito,verify.*,org.mockito.InOrder,verify,org.junit.rules.ExpectedException,expect.*,org.hamcrest.MatcherAssert,assertThat" />
<option name="assertKeywordIsAssertion" value="false" />
</inspection_tool>
<inspection_tool class="TestNGMethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ThisEscapedInConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
@@ -441,7 +444,6 @@
<option name="ignoreRethrownExceptions" value="true" />
</inspection_tool>
<inspection_tool class="ThrowsRuntimeException" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ToArrayCallWithZeroLengthArrayArgument" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="TooBroadScope" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_allowConstructorAsInitializer" value="false" />
<option name="m_onlyLookAtBlocks" value="true" />
@@ -467,10 +469,8 @@
<inspection_tool class="UnnecessaryBlockStatement" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreSwitchBranches" value="false" />
</inspection_tool>
<inspection_tool class="UnnecessaryCallToStringValueOf" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryConstantArrayCreationExpression" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryDefault" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryExplicitNumericCast" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryFullyQualifiedName" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreJavadoc" value="false" />
+28 -3
View File
@@ -7,9 +7,34 @@ This is an Android GUI for [WireGuard](https://www.wireguard.com/). It [opportun
## Building
```
$ git clone https://git.zx2c4.com/wireguard-android
$ git clone --recurse-submodules https://git.zx2c4.com/wireguard-android
$ cd wireguard-android
$ git submodule init
$ git submodule update
$ ./gradlew assembleRelease
```
macOS users may need [flock(1)](https://github.com/discoteq/flock).
## Embedding
The tunnel library is [on JCenter](https://bintray.com/wireguard/wireguard-android/wireguard-android/_latestVersion), alongside [extensive class library documentation](https://javadoc.io/doc/com.wireguard.android/tunnel).
```
implementation 'com.wireguard.android:tunnel:$wireguardTunnelVersion'
```
The library makes use of Java 8 features, so be sure to support those in your gradle configuration with desugaring:
```
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
coreLibraryDesugaringEnabled = true
}
dependencies {
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.0.10"
}
```
## Translating
Please help us translate the app into several languages on [our translation platform](https://crowdin.com/project/WireGuard).
-94
View File
@@ -1,94 +0,0 @@
apply plugin: 'com.android.application'
apply from: 'nonnull.gradle'
// Create a variable called keystorePropertiesFile, and initialize it to your
// keystore.properties file, in the rootProject folder.
final def keystorePropertiesFile = rootProject.file("keystore.properties")
android {
buildToolsVersion '28.0.1'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
compileSdkVersion 27
dataBinding.enabled true
defaultConfig {
applicationId 'com.wireguard.android'
minSdkVersion 21
targetSdkVersion 27
versionCode 438
versionName '0.0.20180729'
buildConfigField 'int', 'MIN_SDK_VERSION', "$minSdkVersion.apiLevel"
}
// If the keystore file exists
if (keystorePropertiesFile.exists()) {
// Initialize a new Properties() object called keystoreProperties.
final def keystoreProperties = new Properties()
// Load your keystore.properties file into the keystoreProperties object.
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}
}
buildTypes {
release {
if (keystorePropertiesFile.exists()) signingConfig signingConfigs.release
externalNativeBuild {
cmake {
arguments "-DANDROID_PACKAGE_NAME=${android.defaultConfig.applicationId}"
}
}
}
debug {
applicationIdSuffix ".debug"
versionNameSuffix "-debug"
externalNativeBuild {
cmake {
arguments "-DANDROID_PACKAGE_NAME=${android.defaultConfig.applicationId}${applicationIdSuffix}"
}
}
}
}
externalNativeBuild {
cmake {
path 'tools/CMakeLists.txt'
}
}
}
ext {
databindingVersion = '3.1.3'
supportLibsVersion = '27.1.1'
streamsupportVersion = '1.6.0'
jsr305Version = '3.0.2'
zxingEmbeddedVersion = '3.6.0'
acraVersion = '5.2.0-rc2'
}
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 "net.sourceforge.streamsupport:android-retrofuture:$streamsupportVersion"
implementation "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion"
implementation "com.google.code.findbugs:jsr305:$jsr305Version"
implementation "com.journeyapps:zxing-android-embedded:$zxingEmbeddedVersion"
implementation "ch.acra:acra-http:$acraVersion"
}
tasks.withType(JavaCompile) {
options.compilerArgs << '-Xlint:unchecked'
options.deprecation = true
}
-87
View File
@@ -1,87 +0,0 @@
/*
* Copyright © 2018 Eric Kuck <eric@bluelinelabs.com>.
* 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"
}
@@ -1,193 +0,0 @@
/*
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatDelegate;
import com.wireguard.android.backend.Backend;
import com.wireguard.android.backend.GoBackend;
import com.wireguard.android.backend.WgQuickBackend;
import com.wireguard.android.configStore.FileConfigStore;
import com.wireguard.android.model.TunnelManager;
import com.wireguard.android.util.AsyncWorker;
import com.wireguard.android.util.RootShell;
import com.wireguard.android.util.ToolsInstaller;
import org.acra.ACRA;
import org.acra.annotation.AcraCore;
import org.acra.annotation.AcraHttpSender;
import org.acra.data.StringFormat;
import org.acra.sender.HttpSender;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.lang.ref.WeakReference;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java9.util.concurrent.CompletableFuture;
@AcraCore(reportFormat = StringFormat.JSON,
buildConfigClass = BuildConfig.class,
logcatArguments = {"-b", "all", "-d", "-v", "threadtime", "*:V"},
excludeMatchingSharedPreferencesKeys = {"last_used_tunnel", "enabled_configs"})
@AcraHttpSender(uri = "https://crashreport.zx2c4.com/android/report",
basicAuthLogin = "6RCovLxEVCTXGiW5",
basicAuthPassword = "O7I3sVa5ULVdiC51",
httpMethod = HttpSender.Method.POST,
compress = true)
public class Application extends android.app.Application {
@SuppressWarnings("NullableProblems") private static WeakReference<Application> weakSelf;
@SuppressWarnings("NullableProblems") private AsyncWorker asyncWorker;
@SuppressWarnings("NullableProblems") private RootShell rootShell;
@SuppressWarnings("NullableProblems") private SharedPreferences sharedPreferences;
@SuppressWarnings("NullableProblems") private ToolsInstaller toolsInstaller;
@SuppressWarnings("NullableProblems") private TunnelManager tunnelManager;
@Nullable private Backend backend;
private final CompletableFuture<Backend> futureBackend = new CompletableFuture<>();
public Application() {
weakSelf = new WeakReference<>(this);
}
/* The ACRA password can be trivially reverse engineered and is open source anyway,
* so there's no point in trying to protect it. However, we do want to at least
* prevent innocent self-builders from uploading stuff to our crash reporter. So, we
* check the DN of the certs that signed the apk, without even bothering to try
* validating that they're authentic. It's a good enough heuristic.
*/
@SuppressWarnings("deprecation")
@Nullable
private static String getInstallSource(final Context context) {
if (BuildConfig.DEBUG)
return null;
try {
final CertificateFactory cf = CertificateFactory.getInstance("X509");
for (final Signature sig : context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES).signatures) {
try {
for (final String category : ((X509Certificate) cf.generateCertificate(new ByteArrayInputStream(sig.toByteArray()))).getSubjectDN().getName().split(", *")) {
final String[] parts = category.split("=", 2);
if (!"O".equals(parts[0]))
continue;
switch (parts[1]) {
case "Google Inc.":
return "Play Store";
case "fdroid.org":
return "F-Droid";
}
}
} catch (final Exception ignored) { }
}
} catch (final Exception ignored) { }
return null;
}
@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);
}
final String installSource = getInstallSource(context);
if (installSource != null) {
ACRA.init(this);
ACRA.getErrorReporter().putCustomData("installSource", installSource);
}
}
public static Application get() {
return weakSelf.get();
}
public static AsyncWorker getAsyncWorker() {
return get().asyncWorker;
}
public static Backend getBackend() {
final Application app = get();
synchronized (app.futureBackend) {
if (app.backend == null) {
Backend backend = null;
if (new File("/sys/module/wireguard").exists()) {
try {
app.rootShell.start();
backend = new WgQuickBackend(app.getApplicationContext());
} catch (final Exception ignored) {
}
}
if (backend == null)
backend = new GoBackend(app.getApplicationContext());
app.backend = backend;
}
return app.backend;
}
}
public static CompletableFuture<Backend> getBackendAsync() {
return get().futureBackend;
}
public static RootShell getRootShell() {
return get().rootShell;
}
public static SharedPreferences getSharedPreferences() {
return get().sharedPreferences;
}
public static ToolsInstaller getToolsInstaller() {
return get().toolsInstaller;
}
public static TunnelManager getTunnelManager() {
return get().tunnelManager;
}
@Override
public void onCreate() {
super.onCreate();
asyncWorker = new AsyncWorker(AsyncTask.SERIAL_EXECUTOR, new Handler(Looper.getMainLooper()));
rootShell = new RootShell(getApplicationContext());
toolsInstaller = new ToolsInstaller(getApplicationContext());
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
AppCompatDelegate.setDefaultNightMode(
sharedPreferences.getBoolean("dark_theme", false) ?
AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);
tunnelManager = new TunnelManager(new FileConfigStore(getApplicationContext()));
tunnelManager.onCreate();
asyncWorker.supplyAsync(Application::getBackend).thenAccept(backend -> {
futureBackend.complete(backend);
if (ACRA.isInitialised()) {
ACRA.getErrorReporter().putCustomData("backend", backend.getClass().getSimpleName());
asyncWorker.supplyAsync(backend::getVersion).thenAccept(version ->
ACRA.getErrorReporter().putCustomData("backendVersion", version));
}
});
}
}
@@ -1,39 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import com.wireguard.android.backend.WgQuickBackend;
import com.wireguard.android.model.TunnelManager;
import com.wireguard.android.util.ExceptionLoggers;
public class BootShutdownReceiver extends BroadcastReceiver {
private static final String TAG = "WireGuard/" + BootShutdownReceiver.class.getSimpleName();
@Override
public void onReceive(final Context context, final Intent intent) {
Application.getBackendAsync().thenAccept(backend -> {
if (!(backend instanceof WgQuickBackend))
return;
final String action = intent.getAction();
if (action == null)
return;
final TunnelManager tunnelManager = Application.getTunnelManager();
if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
Log.i(TAG, "Broadcast receiver restoring state (boot)");
tunnelManager.restoreState(false).whenComplete(ExceptionLoggers.D);
} else if (Intent.ACTION_SHUTDOWN.equals(action)) {
Log.i(TAG, "Broadcast receiver saving state (shutdown)");
tunnelManager.saveState();
}
});
}
}
@@ -1,180 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. 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 android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.IBinder;
import android.service.quicksettings.Tile;
import android.service.quicksettings.TileService;
import android.support.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.widget.SlashDrawable;
import java.util.Objects;
/**
* Service that maintains the application's custom Quick Settings tile. This service is bound by the
* system framework as necessary to update the appearance of the tile in the system UI, and to
* forward click events to the application.
*/
@TargetApi(Build.VERSION_CODES.N)
public class QuickTileService extends TileService {
private static final String TAG = "WireGuard/" + QuickTileService.class.getSimpleName();
private final OnStateChangedCallback onStateChangedCallback = new OnStateChangedCallback();
private final OnTunnelChangedCallback onTunnelChangedCallback = new OnTunnelChangedCallback();
@Nullable private Tunnel tunnel;
@Nullable private Icon iconOn;
@Nullable private Icon iconOff;
/* 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;
}
/* TODO: for Android P: Build.VERSION_CODES.P */
private static final int BUILD_VERSION_CODES_P = 28;
@SuppressWarnings("deprecation")
@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));
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 onClick() {
if (tunnel != null) {
final Tile tile = getQsTile();
if (tile != null) {
tile.setIcon(tile.getIcon() == iconOn ? iconOff : iconOn);
tile.updateTile();
}
tunnel.setState(State.TOGGLE).whenComplete(this::onToggleFinished);
} else {
final Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivityAndCollapse(intent);
}
}
@Override
public void onStartListening() {
Application.getTunnelManager().addOnPropertyChangedCallback(onTunnelChangedCallback);
if (tunnel != null)
tunnel.addOnPropertyChangedCallback(onStateChangedCallback);
updateTile();
}
@Override
public void onStopListening() {
if (tunnel != null)
tunnel.removeOnPropertyChangedCallback(onStateChangedCallback);
Application.getTunnelManager().removeOnPropertyChangedCallback(onTunnelChangedCallback);
}
private void onToggleFinished(@SuppressWarnings("unused") final State state,
@Nullable final Throwable throwable) {
if (throwable == null)
return;
final String error = ExceptionLoggers.unwrapMessage(throwable);
final String message = getString(R.string.toggle_error, error);
Log.e(TAG, message, throwable);
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
private void updateTile() {
// Update the tunnel.
final Tunnel newTunnel = Application.getTunnelManager().getLastUsedTunnel();
if (newTunnel != tunnel) {
if (tunnel != null)
tunnel.removeOnPropertyChangedCallback(onStateChangedCallback);
tunnel = newTunnel;
if (tunnel != null)
tunnel.addOnPropertyChangedCallback(onStateChangedCallback);
}
// Update the tile contents.
final String label;
final int state;
final Tile tile = getQsTile();
if (tunnel != null) {
label = tunnel.getName();
state = tunnel.getState() == Tunnel.State.UP ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
} else {
label = getString(R.string.app_name);
state = Tile.STATE_INACTIVE;
}
if (tile == null)
return;
tile.setLabel(label);
if (tile.getState() != state) {
tile.setIcon(state == Tile.STATE_ACTIVE ? iconOn : iconOff);
tile.setState(state);
}
tile.updateTile();
}
private final class OnStateChangedCallback extends OnPropertyChangedCallback {
@Override
public void onPropertyChanged(final Observable sender, final int propertyId) {
if (!Objects.equals(sender, tunnel)) {
sender.removeOnPropertyChangedCallback(this);
return;
}
if (propertyId != 0 && propertyId != BR.state)
return;
updateTile();
}
}
private final class OnTunnelChangedCallback extends OnPropertyChangedCallback {
@Override
public void onPropertyChanged(final Observable sender, final int propertyId) {
if (propertyId != 0 && propertyId != BR.lastUsedTunnel)
return;
updateTile();
}
}
}
@@ -1,100 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity;
import android.databinding.CallbackRegistry;
import android.databinding.CallbackRegistry.NotifierCallback;
import android.os.Bundle;
import android.support.annotation.Nullable;
import com.wireguard.android.Application;
import com.wireguard.android.model.Tunnel;
import java.util.Objects;
/**
* Base class for activities that need to remember the currently-selected tunnel.
*/
public abstract class BaseActivity extends ThemeChangeAwareActivity {
private static final String KEY_SELECTED_TUNNEL = "selected_tunnel";
private final SelectionChangeRegistry selectionChangeRegistry = new SelectionChangeRegistry();
@Nullable private Tunnel selectedTunnel;
public void addOnSelectedTunnelChangedListener(final OnSelectedTunnelChangedListener listener) {
selectionChangeRegistry.add(listener);
}
@Nullable
public Tunnel getSelectedTunnel() {
return selectedTunnel;
}
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
// Restore the saved tunnel if there is one; otherwise grab it from the arguments.
final String savedTunnelName;
if (savedInstanceState != null)
savedTunnelName = savedInstanceState.getString(KEY_SELECTED_TUNNEL);
else if (getIntent() != null)
savedTunnelName = getIntent().getStringExtra(KEY_SELECTED_TUNNEL);
else
savedTunnelName = null;
if (savedTunnelName != null)
Application.getTunnelManager().getTunnels()
.thenAccept(tunnels -> setSelectedTunnel(tunnels.get(savedTunnelName)));
// The selected tunnel must be set before the superclass method recreates fragments.
super.onCreate(savedInstanceState);
}
@Override
protected void onSaveInstanceState(final Bundle outState) {
if (selectedTunnel != null)
outState.putString(KEY_SELECTED_TUNNEL, selectedTunnel.getName());
super.onSaveInstanceState(outState);
}
protected abstract void onSelectedTunnelChanged(@Nullable Tunnel oldTunnel, @Nullable Tunnel newTunnel);
public void removeOnSelectedTunnelChangedListener(
final OnSelectedTunnelChangedListener listener) {
selectionChangeRegistry.remove(listener);
}
public void setSelectedTunnel(@Nullable final Tunnel tunnel) {
final Tunnel oldTunnel = selectedTunnel;
if (Objects.equals(oldTunnel, tunnel))
return;
selectedTunnel = tunnel;
onSelectedTunnelChanged(oldTunnel, tunnel);
selectionChangeRegistry.notifyCallbacks(oldTunnel, 0, tunnel);
}
public interface OnSelectedTunnelChangedListener {
void onSelectedTunnelChanged(@Nullable Tunnel oldTunnel, @Nullable Tunnel newTunnel);
}
private static final class SelectionChangeNotifier
extends NotifierCallback<OnSelectedTunnelChangedListener, Tunnel, Tunnel> {
@Override
public void onNotifyCallback(final OnSelectedTunnelChangedListener listener,
final Tunnel oldTunnel, final int ignored,
final Tunnel newTunnel) {
listener.onSelectedTunnelChanged(oldTunnel, newTunnel);
}
}
private static final class SelectionChangeRegistry
extends CallbackRegistry<OnSelectedTunnelChangedListener, Tunnel, Tunnel> {
private SelectionChangeRegistry() {
super(new SelectionChangeNotifier());
}
}
}
@@ -1,140 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.ActionBar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.LinearLayout;
import com.wireguard.android.R;
import com.wireguard.android.fragment.TunnelDetailFragment;
import com.wireguard.android.fragment.TunnelEditorFragment;
import com.wireguard.android.fragment.TunnelListFragment;
import com.wireguard.android.model.Tunnel;
/**
* CRUD interface for WireGuard tunnels. This activity serves as the main entry point to the
* WireGuard application, and contains several fragments for listing, viewing details of, and
* editing the configuration and interface state of WireGuard tunnels.
*/
public class MainActivity extends BaseActivity
implements FragmentManager.OnBackStackChangedListener {
@Nullable private ActionBar actionBar;
private boolean isTwoPaneLayout;
@Nullable private TunnelListFragment listFragment;
@Override
public void onBackPressed() {
final int backStackEntries = getSupportFragmentManager().getBackStackEntryCount();
// If the action menu is visible and expanded, collapse it instead of navigating back.
if (isTwoPaneLayout || backStackEntries == 0) {
if (listFragment != null && listFragment.collapseActionMenu())
return;
}
// If the two-pane layout does not have an editor open, going back should exit the app.
if (isTwoPaneLayout && backStackEntries <= 1) {
finish();
return;
}
// Deselect the current tunnel on navigating back from the detail pane to the one-pane list.
if (!isTwoPaneLayout && backStackEntries == 1) {
setSelectedTunnel(null);
return;
}
super.onBackPressed();
}
@Override public void onBackStackChanged() {
if (actionBar == null)
return;
// Do not show the home menu when the two-pane layout is at the detail view (see above).
final int backStackEntries = getSupportFragmentManager().getBackStackEntryCount();
final int minBackStackEntries = isTwoPaneLayout ? 2 : 1;
actionBar.setDisplayHomeAsUpEnabled(backStackEntries >= minBackStackEntries);
}
// We use onTouchListener here to avoid the UI click sound, hence
// calling View#performClick defeats the purpose of it.
@SuppressLint("ClickableViewAccessibility")
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
actionBar = getSupportActionBar();
isTwoPaneLayout = findViewById(R.id.master_detail_wrapper) instanceof LinearLayout;
listFragment = (TunnelListFragment) getSupportFragmentManager().findFragmentByTag("LIST");
getSupportFragmentManager().addOnBackStackChangedListener(this);
onBackStackChanged();
final View actionBarView = findViewById(R.id.action_bar);
if (actionBarView != null)
actionBarView.setOnTouchListener((v, e) -> listFragment != null && listFragment.collapseActionMenu());
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.main_activity, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
// The back arrow in the action bar should act the same as the back button.
onBackPressed();
return true;
case R.id.menu_action_edit:
getSupportFragmentManager().beginTransaction()
.replace(R.id.detail_container, new TunnelEditorFragment())
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.addToBackStack(null)
.commit();
return true;
case R.id.menu_action_save:
// This menu item is handled by the editor fragment.
return false;
case R.id.menu_settings:
startActivity(new Intent(this, SettingsActivity.class));
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
protected void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel,
@Nullable final Tunnel newTunnel) {
final FragmentManager fragmentManager = getSupportFragmentManager();
final int backStackEntries = fragmentManager.getBackStackEntryCount();
if (newTunnel == null) {
// Clear everything off the back stack (all editors and detail fragments).
fragmentManager.popBackStackImmediate(0, FragmentManager.POP_BACK_STACK_INCLUSIVE);
return;
}
if (backStackEntries == 2) {
// Pop the editor off the back stack to reveal the detail fragment. Use the immediate
// method to avoid the editor picking up the new tunnel while it is still visible.
fragmentManager.popBackStackImmediate();
} else if (backStackEntries == 0) {
// Create and show a new detail fragment.
fragmentManager.beginTransaction()
.add(R.id.detail_container, new TunnelDetailFragment())
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.addToBackStack(null)
.commit();
}
}
}
@@ -1,112 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. 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.Nullable;
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 android.util.SparseArray;
import android.view.MenuItem;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.backend.WgQuickBackend;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Interface for changing application-global persistent settings.
*/
public class SettingsActivity extends ThemeChangeAwareActivity {
private final SparseArray<PermissionRequestCallback> permissionRequestCallbacks = new SparseArray<>();
private int permissionRequestCounter;
public void ensurePermissions(final String[] permissions, final PermissionRequestCallback cb) {
final List<String> needPermissions = new ArrayList<>(permissions.length);
for (final String permission : permissions) {
if (ContextCompat.checkSelfPermission(this, permission)
!= PackageManager.PERMISSION_GRANTED)
needPermissions.add(permission);
}
if (needPermissions.isEmpty()) {
final int[] granted = new int[permissions.length];
Arrays.fill(granted, PackageManager.PERMISSION_GRANTED);
cb.done(permissions, granted);
return;
}
final int idx = permissionRequestCounter++;
permissionRequestCallbacks.put(idx, cb);
ActivityCompat.requestPermissions(this,
needPermissions.toArray(new String[needPermissions.size()]), idx);
}
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getSupportFragmentManager().findFragmentById(android.R.id.content) == null) {
getSupportFragmentManager().beginTransaction()
.add(android.R.id.content, new SettingsFragment())
.commit();
}
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onRequestPermissionsResult(final int requestCode,
final String[] permissions,
final int[] grantResults) {
final PermissionRequestCallback f = permissionRequestCallbacks.get(requestCode);
if (f != null) {
permissionRequestCallbacks.remove(requestCode);
f.done(permissions, grantResults);
}
}
public interface PermissionRequestCallback {
void done(String[] permissions, int[] grantResults);
}
public static class SettingsFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String key) {
addPreferencesFromResource(R.xml.preferences);
final Preference wgQuickOnlyPrefs[] = {
getPreferenceManager().findPreference("tools_installer"),
getPreferenceManager().findPreference("restore_on_boot")
};
for (final Preference pref : wgQuickOnlyPrefs)
pref.setVisible(false);
final PreferenceScreen screen = getPreferenceScreen();
Application.getBackendAsync().thenAccept(backend -> {
for (final Preference pref : wgQuickOnlyPrefs) {
if (backend instanceof WgQuickBackend)
pref.setVisible(true);
else
screen.removePreference(pref);
}
});
}
}
}
@@ -1,78 +0,0 @@
/*
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.app.AppCompatDelegate;
import android.util.Log;
import com.wireguard.android.Application;
import java.lang.reflect.Field;
public abstract class ThemeChangeAwareActivity extends AppCompatActivity implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = "WireGuard/" + ThemeChangeAwareActivity.class.getSimpleName();
@Nullable private static Resources lastResources;
private static boolean lastDarkMode;
private static synchronized void invalidateDrawableCache(final Resources resources, final boolean darkMode) {
if (resources == lastResources && darkMode == lastDarkMode)
return;
try {
Field f;
Object o = resources;
try {
f = o.getClass().getDeclaredField("mResourcesImpl");
f.setAccessible(true);
o = f.get(o);
} catch (final Exception ignored) { }
f = o.getClass().getDeclaredField("mDrawableCache");
f.setAccessible(true);
o = f.get(o);
try {
o.getClass().getMethod("onConfigurationChange", int.class).invoke(o, -1);
} catch (final Exception ignored) {
o.getClass().getMethod("clear").invoke(o);
}
} catch (final Exception e) {
Log.e(TAG, "Failed to flush drawable cache", e);
}
lastResources = resources;
lastDarkMode = darkMode;
}
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Application.getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
}
@Override
protected void onDestroy() {
Application.getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
super.onDestroy();
}
@Override
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) {
if ("dark_theme".equals(key)) {
final boolean darkMode = sharedPreferences.getBoolean(key, false);
AppCompatDelegate.setDefaultNightMode(
sharedPreferences.getBoolean(key, false) ?
AppCompatDelegate.MODE_NIGHT_YES :
AppCompatDelegate.MODE_NIGHT_NO);
invalidateDrawableCache(getResources(), darkMode);
recreate();
}
}
}
@@ -1,35 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity;
import android.os.Bundle;
import android.support.annotation.Nullable;
import com.wireguard.android.fragment.TunnelEditorFragment;
import com.wireguard.android.model.Tunnel;
/**
* Standalone activity for creating tunnels.
*/
public class TunnelCreatorActivity extends BaseActivity {
@Override
@SuppressWarnings("UnnecessaryFullyQualifiedName")
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getSupportFragmentManager().findFragmentById(android.R.id.content) == null) {
getSupportFragmentManager().beginTransaction()
.add(android.R.id.content, new TunnelEditorFragment())
.commit();
}
}
@Override
protected void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
finish();
}
}
@@ -1,80 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.backend;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.model.Tunnel.Statistics;
import com.wireguard.config.Config;
import java.util.Set;
/**
* Interface for implementations of the WireGuard secure network tunnel.
*/
public interface Backend {
/**
* Update the volatile configuration of a running tunnel and return the resulting configuration.
* If the tunnel is not up, return the configuration that would result (if known), or else
* simply return the given configuration.
*
* @param tunnel The tunnel to apply the configuration to.
* @param config The new configuration for this tunnel.
* @return The updated configuration of the tunnel.
*/
Config applyConfig(Tunnel tunnel, Config config) throws Exception;
/**
* Enumerate the names of currently-running tunnels.
*
* @return The set of running tunnel names.
*/
Set<String> enumerate();
/**
* Get the actual state of a tunnel.
*
* @param tunnel The tunnel to examine the state of.
* @return The state of the tunnel.
*/
State getState(Tunnel tunnel) throws Exception;
/**
* Get statistics about traffic and errors on this tunnel. If the tunnel is not running, the
* statistics object will be filled with zero values.
*
* @param tunnel The tunnel to retrieve statistics for.
* @return The statistics for the tunnel.
*/
Statistics getStatistics(Tunnel tunnel) throws Exception;
/**
* Set the state of a tunnel.
*
* @param tunnel The tunnel to control the state of.
* @param state The new state for this tunnel. Must be {@code UP}, {@code DOWN}, or
* {@code TOGGLE}.
* @return The updated state of the tunnel.
*/
State setState(Tunnel tunnel, State state) throws Exception;
/**
* Determine version of underlying backend.
*
* @return The version of the backend.
* @throws Exception
*/
String getVersion() throws Exception;
/**
* Determine type name of underlying backend.
*
* @return Type name
*/
String getTypeName();
}
@@ -1,265 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.backend;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.ParcelFileDescriptor;
import android.support.annotation.Nullable;
import android.support.v4.util.ArraySet;
import android.util.Log;
import com.wireguard.android.Application;
import com.wireguard.android.activity.MainActivity;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.model.Tunnel.Statistics;
import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.android.util.SharedLibraryLoader;
import com.wireguard.config.Config;
import com.wireguard.config.InetNetwork;
import com.wireguard.config.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;
import java9.util.concurrent.CompletableFuture;
public final class GoBackend implements Backend {
private static final String TAG = "WireGuard/" + GoBackend.class.getSimpleName();
private static CompletableFuture<VpnService> vpnService = new CompletableFuture<>();
private final Context context;
@Nullable private Tunnel currentTunnel;
private int currentTunnelHandle = -1;
public GoBackend(final Context context) {
SharedLibraryLoader.loadSharedLibrary(context, "wg-go");
this.context = context;
}
private static native int wgGetSocketV4(int handle);
private static native int wgGetSocketV6(int handle);
private static native void wgTurnOff(int handle);
private static native int wgTurnOn(String ifName, int tunFd, String settings);
private static native String wgVersion();
@Override
public 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) {
// Restart the tunnel to apply the new config.
setStateInternal(tunnel, tunnel.getConfig(), State.DOWN);
try {
setStateInternal(tunnel, config, State.UP);
} catch (final Exception e) {
// The new configuration didn't work, so try to go back to the old one.
setStateInternal(tunnel, tunnel.getConfig(), State.UP);
throw e;
}
}
return config;
}
@Override
public Set<String> enumerate() {
if (currentTunnel != null) {
final Set<String> runningTunnels = new ArraySet<>();
runningTunnels.add(currentTunnel.getName());
return runningTunnels;
}
return Collections.emptySet();
}
@Override
public State getState(final Tunnel tunnel) {
return currentTunnel == tunnel ? State.UP : State.DOWN;
}
@Override
public Statistics getStatistics(final Tunnel tunnel) {
return new Statistics();
}
@Override
public State setState(final Tunnel tunnel, State state) throws Exception {
final State originalState = getState(tunnel);
if (state == State.TOGGLE)
state = originalState == State.UP ? State.DOWN : State.UP;
if (state == originalState)
return originalState;
if (state == State.UP && currentTunnel != null)
throw new IllegalStateException("Only one userspace tunnel can run at a time");
Log.d(TAG, "Changing tunnel " + tunnel.getName() + " to state " + state);
setStateInternal(tunnel, tunnel.getConfig(), state);
return getState(tunnel);
}
private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state)
throws Exception {
if (state == State.UP) {
Log.i(TAG, "Bringing tunnel up");
Objects.requireNonNull(config, "Trying to bring up a tunnel with no config");
if (VpnService.prepare(context) != null)
throw new Exception("VPN service not authorized by user");
final VpnService service;
if (!vpnService.isDone())
startVpnService();
try {
service = vpnService.get(2, TimeUnit.SECONDS);
} catch (final TimeoutException e) {
throw new Exception("Unable to start Android VPN service", e);
}
if (currentTunnelHandle != -1) {
Log.w(TAG, "Tunnel already up");
return;
}
// 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();
}
// Create the vpn tunnel with android API
final VpnService.Builder builder = service.getBuilder();
builder.setSession(tunnel.getName());
final Intent configureIntent = new Intent(context, MainActivity.class);
configureIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
builder.setConfigureIntent(PendingIntent.getActivity(context, 0, configureIntent, 0));
for (final String excludedApplication : config.getInterface().getExcludedApplications())
builder.addDisallowedApplication(excludedApplication);
for (final InetNetwork addr : config.getInterface().getAddresses())
builder.addAddress(addr.getAddress(), addr.getMask());
for (final InetAddress addr : config.getInterface().getDnses())
builder.addDnsServer(addr.getHostAddress());
for (final Peer peer : config.getPeers()) {
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.setBlocking(true);
try (final ParcelFileDescriptor tun = builder.establish()) {
if (tun == null)
throw new Exception("Unable to create tun device");
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 + ')');
currentTunnel = tunnel;
service.protect(wgGetSocketV4(currentTunnelHandle));
service.protect(wgGetSocketV6(currentTunnelHandle));
} else {
Log.i(TAG, "Bringing tunnel down");
if (currentTunnelHandle == -1) {
Log.w(TAG, "Tunnel already down");
return;
}
wgTurnOff(currentTunnelHandle);
currentTunnel = null;
currentTunnelHandle = -1;
}
}
private void startVpnService() {
Log.d(TAG, "Requesting to start VpnService");
context.startService(new Intent(context, VpnService.class));
}
public static class VpnService extends android.net.VpnService {
public Builder getBuilder() {
return new Builder();
}
@Override
public void onCreate() {
vpnService.complete(this);
super.onCreate();
}
@Override
public void onDestroy() {
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
for (final Tunnel tunnel : tunnels) {
if (tunnel != null && tunnel.getState() != State.DOWN)
tunnel.setState(State.DOWN);
}
});
vpnService = vpnService.newIncompleteFuture();
super.onDestroy();
}
@Override
public int onStartCommand(@Nullable final Intent intent, final int flags, final int startId) {
vpnService.complete(this);
if (intent == null || intent.getComponent() == null || !intent.getComponent().getPackageName().equals(getPackageName())) {
Log.d(TAG, "Service started by Always-on VPN feature");
Application.getTunnelManager().restoreState(true).whenComplete(ExceptionLoggers.D);
}
return super.onStartCommand(intent, flags, startId);
}
}
}
@@ -1,128 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.backend;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.Log;
import com.wireguard.android.Application;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.model.Tunnel.Statistics;
import com.wireguard.config.Config;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java9.util.stream.Collectors;
import java9.util.stream.Stream;
/**
* WireGuard backend that uses {@code wg-quick} to implement tunnel configuration.
*/
public final class WgQuickBackend implements Backend {
private static final String TAG = "WireGuard/" + WgQuickBackend.class.getSimpleName();
private final File localTemporaryDir;
public WgQuickBackend(final Context context) {
localTemporaryDir = new File(context.getCacheDir(), "tmp");
}
@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) {
// Restart the tunnel to apply the new config.
setStateInternal(tunnel, tunnel.getConfig(), State.DOWN);
try {
setStateInternal(tunnel, config, State.UP);
} catch (final Exception e) {
// The new configuration didn't work, so try to go back to the old one.
setStateInternal(tunnel, tunnel.getConfig(), State.UP);
throw e;
}
}
return config;
}
@Override
public Set<String> enumerate() {
final List<String> output = new ArrayList<>();
// Don't throw an exception here or nothing will show up in the UI.
try {
Application.getToolsInstaller().ensureToolsAvailable();
if (Application.getRootShell().run(output, "wg show interfaces") != 0 || output.isEmpty())
return Collections.emptySet();
} catch (final Exception e) {
Log.w(TAG, "Unable to enumerate running tunnels", e);
return Collections.emptySet();
}
// wg puts all interface names on the same line. Split them into separate elements.
return Stream.of(output.get(0).split(" ")).collect(Collectors.toUnmodifiableSet());
}
@Override
public State getState(final Tunnel tunnel) {
return enumerate().contains(tunnel.getName()) ? State.UP : State.DOWN;
}
@Override
public Statistics getStatistics(final Tunnel tunnel) {
return new Statistics();
}
@Override
public State setState(final Tunnel tunnel, State state) throws Exception {
final State originalState = getState(tunnel);
if (state == State.TOGGLE)
state = originalState == State.UP ? State.DOWN : State.UP;
if (state == originalState)
return originalState;
Log.d(TAG, "Changing tunnel " + tunnel.getName() + " to state " + state);
Application.getToolsInstaller().ensureToolsAvailable();
setStateInternal(tunnel, tunnel.getConfig(), state);
return getState(tunnel);
}
private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state) throws Exception {
Objects.requireNonNull(config, "Trying to set state with a null config");
final File tempFile = new File(localTemporaryDir, tunnel.getName() + ".conf");
try (final FileOutputStream stream = new FileOutputStream(tempFile, false)) {
stream.write(config.toString().getBytes(StandardCharsets.UTF_8));
}
String command = String.format("wg-quick %s '%s'",
state.toString().toLowerCase(), 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 + ')');
}
}
@@ -1,102 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.configStore;
import android.content.Context;
import android.util.Log;
import com.wireguard.config.Config;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java9.util.stream.Collectors;
import java9.util.stream.Stream;
/**
* Configuration store that uses a {@code wg-quick}-style file for each configured tunnel.
*/
public final class FileConfigStore implements ConfigStore {
private static final String TAG = "WireGuard/" + FileConfigStore.class.getSimpleName();
private final Context context;
public FileConfigStore(final Context context) {
this.context = context;
}
@Override
public Config create(final String name, final Config config) throws IOException {
Log.d(TAG, "Creating configuration for tunnel " + name);
final File file = fileFor(name);
if (!file.createNewFile())
throw new IOException("Configuration file " + file.getName() + " already exists");
try (final FileOutputStream stream = new FileOutputStream(file, false)) {
stream.write(config.toString().getBytes(StandardCharsets.UTF_8));
}
return config;
}
@Override
public void delete(final String name) throws IOException {
Log.d(TAG, "Deleting configuration for tunnel " + name);
final File file = fileFor(name);
if (!file.delete())
throw new IOException("Cannot delete configuration file " + file.getName());
}
@Override
public Set<String> enumerate() {
return Stream.of(context.fileList())
.filter(name -> name.endsWith(".conf"))
.map(name -> name.substring(0, name.length() - ".conf".length()))
.collect(Collectors.toUnmodifiableSet());
}
private File fileFor(final String name) {
return new File(context.getFilesDir(), name + ".conf");
}
@Override
public Config load(final String name) throws IOException {
try (final FileInputStream stream = new FileInputStream(fileFor(name))) {
return Config.from(stream);
}
}
@Override
public void rename(final String name, final String replacement) throws IOException {
Log.d(TAG, "Renaming configuration for tunnel " + name + " to " + replacement);
final File file = fileFor(name);
final File replacementFile = fileFor(replacement);
if (!replacementFile.createNewFile())
throw new IOException("Configuration for " + replacement + " already exists");
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());
}
}
@Override
public Config save(final String name, final Config config) throws IOException {
Log.d(TAG, "Saving configuration for tunnel " + name);
final File file = fileFor(name);
if (!file.isFile())
throw new FileNotFoundException("Configuration file " + file.getName() + " not found");
try (final FileOutputStream stream = new FileOutputStream(file, false)) {
stream.write(config.toString().getBytes(StandardCharsets.UTF_8));
}
return config;
}
}
@@ -1,107 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. 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 android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.InputFilter;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.wireguard.android.R;
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler;
import com.wireguard.android.util.ObservableKeyedList;
import com.wireguard.android.widget.ToggleSwitch;
import com.wireguard.android.widget.ToggleSwitch.OnBeforeCheckedChangeListener;
import com.wireguard.util.Keyed;
/**
* Static methods for use by generated code in the Android data binding library.
*/
@SuppressWarnings("unused")
public final class BindingAdapters {
private BindingAdapters() {
// Prevent instantiation.
}
@BindingAdapter("checked")
public static void setChecked(final ToggleSwitch view, final boolean checked) {
view.setCheckedInternal(checked);
}
@BindingAdapter("filter")
public static void setFilter(final TextView view, final InputFilter filter) {
view.setFilters(new InputFilter[]{filter});
}
@BindingAdapter({"items", "layout"})
public static <E> void setItems(final LinearLayout view,
final ObservableList<E> oldList, final int oldLayoutId,
final ObservableList<E> newList, final int newLayoutId) {
if (oldList == newList && oldLayoutId == newLayoutId)
return;
ItemChangeListener<E> listener = ListenerUtil.getListener(view, R.id.item_change_listener);
// If the layout changes, any existing listener must be replaced.
if (listener != null && oldList != null && oldLayoutId != newLayoutId) {
listener.setList(null);
listener = null;
// Stop tracking the old listener.
ListenerUtil.trackListener(view, null, R.id.item_change_listener);
}
// Avoid adding a listener when there is no new list or layout.
if (newList == null || newLayoutId == 0)
return;
if (listener == null) {
listener = new ItemChangeListener<>(view, newLayoutId);
ListenerUtil.trackListener(view, listener, R.id.item_change_listener);
}
// Either the list changed, or this is an entirely new listener because the layout changed.
listener.setList(newList);
}
@BindingAdapter(requireAll = false, value = {"items", "layout", "configurationHandler"})
public static <K, E extends Keyed<? extends K>>
void setItems(final RecyclerView view,
final ObservableKeyedList<K, E> oldList, final int oldLayoutId, final RowConfigurationHandler oldRowConfigurationHandler,
final ObservableKeyedList<K, E> newList, final int newLayoutId, final RowConfigurationHandler newRowConfigurationHandler) {
if (view.getLayoutManager() == null)
view.setLayoutManager(new LinearLayoutManager(view.getContext(), RecyclerView.VERTICAL, false));
if (oldList == newList && oldLayoutId == newLayoutId)
return;
// The ListAdapter interface is not generic, so this cannot be checked.
@SuppressWarnings("unchecked") ObservableKeyedRecyclerViewAdapter<K, E> adapter =
(ObservableKeyedRecyclerViewAdapter<K, E>) view.getAdapter();
// If the layout changes, any existing adapter must be replaced.
if (adapter != null && oldList != null && oldLayoutId != newLayoutId) {
adapter.setList(null);
adapter = null;
}
// Avoid setting an adapter when there is no new list or layout.
if (newList == null || newLayoutId == 0)
return;
if (adapter == null) {
adapter = new ObservableKeyedRecyclerViewAdapter<>(view.getContext(), newLayoutId, newList);
view.setAdapter(adapter);
}
adapter.setRowConfigurationHandler(newRowConfigurationHandler);
// Either the list changed, or this is an entirely new listener because the layout changed.
adapter.setList(newList);
}
@BindingAdapter("onBeforeCheckedChanged")
public static void setOnBeforeCheckedChanged(final ToggleSwitch view,
final OnBeforeCheckedChangeListener listener) {
view.setOnBeforeCheckedChangeListener(listener);
}
}
@@ -1,141 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. 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 android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.wireguard.android.BR;
import java.lang.ref.WeakReference;
import java.util.Objects;
/**
* Helper class for binding an ObservableList to the children of a ViewGroup.
*/
class ItemChangeListener<T> {
private final OnListChangedCallback<T> callback = new OnListChangedCallback<>(this);
private final ViewGroup container;
private final int layoutId;
private final LayoutInflater layoutInflater;
@Nullable private ObservableList<T> list;
ItemChangeListener(final ViewGroup container, final int layoutId) {
this.container = container;
this.layoutId = layoutId;
layoutInflater = LayoutInflater.from(container.getContext());
}
private View getView(final int position, @Nullable final View convertView) {
ViewDataBinding binding = convertView != null ? DataBindingUtil.getBinding(convertView) : null;
if (binding == null) {
binding = DataBindingUtil.inflate(layoutInflater, layoutId, container, false);
}
Objects.requireNonNull(list, "Trying to get a view while list is still null");
binding.setVariable(BR.collection, list);
binding.setVariable(BR.item, list.get(position));
binding.executePendingBindings();
return binding.getRoot();
}
void setList(@Nullable final ObservableList<T> newList) {
if (list != null)
list.removeOnListChangedCallback(callback);
list = newList;
if (list != null) {
list.addOnListChangedCallback(callback);
callback.onChanged(list);
} else {
container.removeAllViews();
}
}
private static final class OnListChangedCallback<T>
extends ObservableList.OnListChangedCallback<ObservableList<T>> {
private final WeakReference<ItemChangeListener<T>> weakListener;
private OnListChangedCallback(final ItemChangeListener<T> listener) {
weakListener = new WeakReference<>(listener);
}
@Override
public void onChanged(final ObservableList<T> sender) {
final ItemChangeListener<T> listener = weakListener.get();
if (listener != null) {
// TODO: recycle views
listener.container.removeAllViews();
for (int i = 0; i < sender.size(); ++i)
listener.container.addView(listener.getView(i, null));
} else {
sender.removeOnListChangedCallback(this);
}
}
@Override
public void onItemRangeChanged(final ObservableList<T> sender, final int positionStart,
final int itemCount) {
final ItemChangeListener<T> listener = weakListener.get();
if (listener != null) {
for (int i = positionStart; i < positionStart + itemCount; ++i) {
final View child = listener.container.getChildAt(i);
listener.container.removeViewAt(i);
listener.container.addView(listener.getView(i, child));
}
} else {
sender.removeOnListChangedCallback(this);
}
}
@Override
public void onItemRangeInserted(final ObservableList<T> sender, final int positionStart,
final int itemCount) {
final ItemChangeListener<T> listener = weakListener.get();
if (listener != null) {
for (int i = positionStart; i < positionStart + itemCount; ++i)
listener.container.addView(listener.getView(i, null));
} else {
sender.removeOnListChangedCallback(this);
}
}
@Override
public void onItemRangeMoved(final ObservableList<T> sender, final int fromPosition,
final int toPosition, final int itemCount) {
final ItemChangeListener<T> listener = weakListener.get();
if (listener != null) {
final View[] views = new View[itemCount];
for (int i = 0; i < itemCount; ++i)
views[i] = listener.container.getChildAt(fromPosition + i);
listener.container.removeViews(fromPosition, itemCount);
for (int i = 0; i < itemCount; ++i)
listener.container.addView(views[i], toPosition + i);
} else {
sender.removeOnListChangedCallback(this);
}
}
@Override
public void onItemRangeRemoved(final ObservableList<T> sender, final int positionStart,
final int itemCount) {
final ItemChangeListener<T> listener = weakListener.get();
if (listener != null) {
listener.container.removeViews(positionStart, itemCount);
} else {
sender.removeOnListChangedCallback(this);
}
}
}
}
@@ -1,160 +0,0 @@
/*
* Copyright © 2018 Eric Kuck <eric@bluelinelabs.com>.
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. 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 android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.Adapter;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import com.wireguard.android.BR;
import com.wireguard.android.util.ObservableKeyedList;
import com.wireguard.util.Keyed;
import java.lang.ref.WeakReference;
/**
* A generic {@code RecyclerView.Adapter} backed by a {@code ObservableKeyedList}.
*/
public class ObservableKeyedRecyclerViewAdapter<K, E extends Keyed<? extends K>> extends Adapter<ObservableKeyedRecyclerViewAdapter.ViewHolder> {
private final OnListChangedCallback<E> callback = new OnListChangedCallback<>(this);
private final int layoutId;
private final LayoutInflater layoutInflater;
@Nullable private ObservableKeyedList<K, E> list;
@Nullable private RowConfigurationHandler rowConfigurationHandler;
ObservableKeyedRecyclerViewAdapter(final Context context, final int layoutId,
final ObservableKeyedList<K, E> list) {
this.layoutId = layoutId;
layoutInflater = LayoutInflater.from(context);
setList(list);
}
@Override
public int getItemCount() {
return list != null ? list.size() : 0;
}
@Nullable
private E getItem(final int position) {
if (list == null || position < 0 || position >= list.size())
return null;
return list.get(position);
}
@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;
}
@Override
public ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
return new ViewHolder(DataBindingUtil.inflate(layoutInflater, layoutId, parent, false));
}
@SuppressWarnings("unchecked")
@Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
holder.binding.setVariable(BR.collection, list);
holder.binding.setVariable(BR.key, getKey(position));
holder.binding.setVariable(BR.item, getItem(position));
holder.binding.executePendingBindings();
if (rowConfigurationHandler != null) {
E item = getItem(position);
if (item != null) {
rowConfigurationHandler.onConfigureRow(holder.binding, item, position);
}
}
}
void setList(@Nullable final ObservableKeyedList<K, E> newList) {
if (list != null)
list.removeOnListChangedCallback(callback);
list = newList;
if (list != null) {
list.addOnListChangedCallback(callback);
}
notifyDataSetChanged();
}
void setRowConfigurationHandler(final RowConfigurationHandler rowConfigurationHandler) {
this.rowConfigurationHandler = rowConfigurationHandler;
}
private static final class OnListChangedCallback<E extends Keyed<?>>
extends ObservableList.OnListChangedCallback<ObservableList<E>> {
private final WeakReference<ObservableKeyedRecyclerViewAdapter<?, E>> weakAdapter;
private OnListChangedCallback(final ObservableKeyedRecyclerViewAdapter<?, E> adapter) {
weakAdapter = new WeakReference<>(adapter);
}
@Override
public void onChanged(final ObservableList<E> sender) {
final ObservableKeyedRecyclerViewAdapter adapter = weakAdapter.get();
if (adapter != null)
adapter.notifyDataSetChanged();
else
sender.removeOnListChangedCallback(this);
}
@Override
public void onItemRangeChanged(final ObservableList<E> sender, final int positionStart,
final int itemCount) {
onChanged(sender);
}
@Override
public void onItemRangeInserted(final ObservableList<E> sender, final int positionStart,
final int itemCount) {
onChanged(sender);
}
@Override
public void onItemRangeMoved(final ObservableList<E> sender, final int fromPosition,
final int toPosition, final int itemCount) {
onChanged(sender);
}
@Override
public void onItemRangeRemoved(final ObservableList<E> sender, final int positionStart,
final int itemCount) {
onChanged(sender);
}
}
public static class ViewHolder extends RecyclerView.ViewHolder {
final ViewDataBinding binding;
public ViewHolder(final ViewDataBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
public interface RowConfigurationHandler<B extends ViewDataBinding, T> {
void onConfigureRow(B binding, T item, int position);
}
}
@@ -1,133 +0,0 @@
/*
* Copyright © 2018 Eric Kuck <eric@bluelinelabs.com>.
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. 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 android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.Fragment;
import android.support.v7.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.ExceptionLoggers;
import com.wireguard.android.util.ObservableKeyedArrayList;
import com.wireguard.android.util.ObservableKeyedList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class AppListDialogFragment extends DialogFragment {
private static final String KEY_EXCLUDED_APPS = "excludedApps";
private List<String> currentlyExcludedApps;
private final ObservableKeyedList<String, ApplicationData> appData = new ObservableKeyedArrayList<>();
public static <T extends Fragment & AppExclusionListener> AppListDialogFragment newInstance(final String[] excludedApps, final T target) {
final Bundle extras = new Bundle();
extras.putStringArray(KEY_EXCLUDED_APPS, excludedApps);
final AppListDialogFragment fragment = new AppListDialogFragment();
fragment.setTargetFragment(target, 0);
fragment.setArguments(extras);
return fragment;
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
currentlyExcludedApps = Arrays.asList(getArguments().getStringArray(KEY_EXCLUDED_APPS));
}
@Override
public Dialog onCreateDialog(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;
}
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, (lhs, rhs) -> lhs.getName().toLowerCase().compareTo(rhs.getName().toLowerCase()));
return appData;
}).whenComplete(((data, throwable) -> {
if (data != null) {
appData.clear();
appData.addAll(data);
} else {
final String error = throwable != null ? ExceptionLoggers.unwrapMessage(throwable) : "Unknown";
final String message = activity.getString(R.string.error_fetching_apps, error);
Toast.makeText(activity, message, Toast.LENGTH_LONG).show();
dismissAllowingStateLoss();
}
}));
}
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,129 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment;
import android.content.Context;
import android.content.Intent;
import android.databinding.DataBindingUtil;
import android.databinding.ViewDataBinding;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v4.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.ExceptionLoggers;
import com.wireguard.android.widget.MonkeyedSnackbar;
/**
* Base class for fragments that need to know the currently-selected tunnel. Only does anything when
* attached to a {@code BaseActivity}.
*/
public abstract class BaseFragment extends Fragment implements OnSelectedTunnelChangedListener {
private static final String TAG = "WireGuard/" + BaseFragment.class.getSimpleName();
private static final int REQUEST_CODE_VPN_PERMISSION = 23491;
@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 onAttach(final Context context) {
super.onAttach(context);
if (context instanceof BaseActivity) {
activity = (BaseActivity) context;
activity.addOnSelectedTunnelChangedListener(this);
} else {
activity = null;
}
}
@Override
public void onDetach() {
if (activity != null)
activity.removeOnSelectedTunnelChangedListener(this);
activity = null;
super.onDetach();
}
@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;
}
}
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 = ExceptionLoggers.unwrapMessage(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)
MonkeyedSnackbar.make(view, message, Snackbar.LENGTH_LONG).show();
else
Toast.makeText(getContext(), message, Toast.LENGTH_LONG).show();
Log.e(TAG, message, throwable);
});
}
}
@@ -1,115 +0,0 @@
/*
* Copyright © 2018 Eric Kuck <eric@bluelinelabs.com>.
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. 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 android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.support.v7.app.AlertDialog;
import android.view.WindowManager;
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.Config;
import java.io.IOException;
import java.util.Objects;
public class ConfigNamingDialogFragment extends DialogFragment {
private static final String KEY_CONFIG_TEXT = "config_text";
@Nullable private Config config;
@Nullable private ConfigNamingDialogFragmentBinding binding;
@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;
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
config = Config.from(getArguments().getString(KEY_CONFIG_TEXT));
} catch (final IOException exception) {
throw new RuntimeException("Invalid config passed to " + getClass().getSimpleName(), exception);
}
}
@Override public void onResume() {
super.onResume();
final AlertDialog dialog = (AlertDialog) getDialog();
if (dialog != null) {
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> createTunnelAndDismiss());
setKeyboardVisible(true);
}
}
@Override
public Dialog onCreateDialog(final Bundle savedInstanceState) {
final Activity activity = getActivity();
imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(activity);
alertDialogBuilder.setTitle(R.string.import_from_qrcode);
binding = ConfigNamingDialogFragmentBinding.inflate(getActivity().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 dismiss() {
setKeyboardVisible(false);
super.dismiss();
}
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());
}
});
}
}
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,83 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import com.wireguard.android.R;
import com.wireguard.android.databinding.TunnelDetailFragmentBinding;
import com.wireguard.android.model.Tunnel;
import com.wireguard.config.Config;
/**
* Fragment that shows details about a specific tunnel.
*/
public class TunnelDetailFragment extends BaseFragment {
@Nullable private TunnelDetailFragmentBinding binding;
private void onConfigLoaded(final String name, final Config config) {
if (binding != null) {
binding.setConfig(new Config.Observable(config, name));
}
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
inflater.inflate(R.menu.tunnel_detail, menu);
}
@Override
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
binding = TunnelDetailFragmentBinding.inflate(inflater, container, false);
binding.executePendingBindings();
return binding.getRoot();
}
@Override
public void onDestroyView() {
binding = null;
super.onDestroyView();
}
@Override
public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
if (binding == null)
return;
binding.setTunnel(newTunnel);
if (newTunnel == null)
binding.setConfig(null);
else
newTunnel.getConfigAsync().thenAccept(a -> onConfigLoaded(newTunnel.getName(), a));
}
@Override
public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
if (binding == null) {
return;
}
binding.setFragment(this);
onSelectedTunnelChanged(null, getSelectedTunnel());
super.onViewStateRestored(savedInstanceState);
}
}
@@ -1,314 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment;
import android.app.Activity;
import android.content.Context;
import android.databinding.Observable;
import android.databinding.ObservableList;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v4.app.FragmentManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;
import com.wireguard.android.Application;
import com.wireguard.android.BR;
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.widget.MonkeyedSnackbar;
import com.wireguard.config.Attribute;
import com.wireguard.config.Config;
import com.wireguard.config.Peer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
/**
* Fragment for editing a WireGuard configuration.
*/
public class TunnelEditorFragment extends BaseFragment implements AppExclusionListener {
private static final String KEY_LOCAL_CONFIG = "local_config";
private static final String KEY_ORIGINAL_NAME = "original_name";
private static final String TAG = "WireGuard/" + TunnelEditorFragment.class.getSimpleName();
@Nullable private TunnelEditorFragmentBinding binding;
@Nullable private Tunnel tunnel;
private void onConfigLoaded(final String name, final Config config) {
if (binding != null) {
binding.setConfig(new Config.Observable(config, name));
}
}
private void onConfigSaved(final Tunnel savedTunnel,
@Nullable final Throwable throwable) {
final String message;
if (throwable == null) {
message = getString(R.string.config_save_success, savedTunnel.getName());
Log.d(TAG, message);
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
onFinished();
} else {
final String error = ExceptionLoggers.unwrapMessage(throwable);
message = getString(R.string.config_save_error, savedTunnel.getName(), error);
Log.e(TAG, message, throwable);
if (binding != null) {
MonkeyedSnackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
}
}
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
inflater.inflate(R.menu.config_editor, menu);
}
private final ObservableList.OnListChangedCallback<? extends ObservableList<Peer.Observable>> breakObjectListOrientedLayeringHandler = new ObservableList.OnListChangedCallback<ObservableList<Peer.Observable>>() {
@Override
public void onChanged(final ObservableList<Peer.Observable> sender) { }
@Override
public void onItemRangeChanged(final ObservableList<Peer.Observable> sender, final int positionStart, final int itemCount) { }
@Override
public void onItemRangeMoved(final ObservableList<Peer.Observable> sender, final int fromPosition, final int toPosition, final int itemCount) { }
@Override
public void onItemRangeInserted(final ObservableList<Peer.Observable> sender, final int positionStart, final int itemCount) {
if (binding != null)
breakObjectOrientedLayeringHandler.onPropertyChanged(binding.getConfig(), BR.peers);
}
@Override
public void onItemRangeRemoved(final ObservableList<Peer.Observable> sender, final int positionStart, final int itemCount) {
if (binding != null)
breakObjectOrientedLayeringHandler.onPropertyChanged(binding.getConfig(), BR.peers);
}
};
private final Collection<Object> breakObjectOrientedLayeringHandlerReceivers = new ArrayList<>();
private final Observable.OnPropertyChangedCallback breakObjectOrientedLayeringHandler = new Observable.OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(final Observable sender, final int propertyId) {
if (binding == null)
return;
final Config.Observable config = binding.getConfig();
if (config == null)
return;
if (propertyId == BR.config) {
config.addOnPropertyChangedCallback(breakObjectOrientedLayeringHandler);
breakObjectOrientedLayeringHandlerReceivers.add(config);
config.getInterfaceSection().addOnPropertyChangedCallback(breakObjectOrientedLayeringHandler);
breakObjectOrientedLayeringHandlerReceivers.add(config.getInterfaceSection());
config.getPeers().addOnListChangedCallback(breakObjectListOrientedLayeringHandler);
breakObjectOrientedLayeringHandlerReceivers.add(config.getPeers());
} else if (propertyId == BR.dnses || propertyId == BR.peers)
;
else
return;
final int numSiblings = config.getPeers().size() - 1;
for (final Peer.Observable peer : config.getPeers()) {
peer.setInterfaceDNSRoutes(config.getInterfaceSection().getDnses());
peer.setNumSiblings(numSiblings);
}
}
};
@Override
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
binding = TunnelEditorFragmentBinding.inflate(inflater, container, false);
binding.addOnPropertyChangedCallback(breakObjectOrientedLayeringHandler);
breakObjectOrientedLayeringHandlerReceivers.add(binding);
binding.executePendingBindings();
return binding.getRoot();
}
@SuppressWarnings("unchecked")
@Override
public void onDestroyView() {
binding = null;
for (final Object o : breakObjectOrientedLayeringHandlerReceivers) {
if (o instanceof Observable)
((Observable)o).removeOnPropertyChangedCallback(breakObjectOrientedLayeringHandler);
else if (o instanceof ObservableList)
((ObservableList)o).removeOnListChangedCallback(breakObjectListOrientedLayeringHandler);
}
super.onDestroyView();
}
private void onFinished() {
// Hide the keyboard; it rarely goes away on its own.
final Activity activity = getActivity();
if (activity == null) return;
final View focusedView = activity.getCurrentFocus();
if (focusedView != null) {
final Object service = activity.getSystemService(Context.INPUT_METHOD_SERVICE);
final InputMethodManager inputManager = (InputMethodManager) service;
if (inputManager != null)
inputManager.hideSoftInputFromWindow(focusedView.getWindowToken(),
InputMethodManager.HIDE_NOT_ALWAYS);
}
// Tell the activity to finish itself or go back to the detail view.
getActivity().runOnUiThread(() -> {
// TODO(smaeul): Remove this hack when fixing the Config ViewModel
// The selected tunnel has to actually change, but we have to remember this one.
final Tunnel savedTunnel = tunnel;
if (savedTunnel == getSelectedTunnel())
setSelectedTunnel(null);
setSelectedTunnel(savedTunnel);
});
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_action_save:
final Config newConfig = new Config();
try {
binding.getConfig().commitData(newConfig);
} catch (final Exception e) {
final String error = ExceptionLoggers.unwrapMessage(e);
final String tunnelName = tunnel == null ? binding.getConfig().getName() : tunnel.getName();
final String message = getString(R.string.config_save_error, tunnelName, error);
Log.e(TAG, message, e);
MonkeyedSnackbar.make(binding.mainContainer, error, Snackbar.LENGTH_LONG).show();
return false;
}
if (tunnel == null) {
Log.d(TAG, "Attempting to create new tunnel " + binding.getConfig().getName());
final TunnelManager manager = Application.getTunnelManager();
manager.create(binding.getConfig().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())
.whenComplete((a, b) -> onTunnelRenamed(tunnel, newConfig, b));
} else {
Log.d(TAG, "Attempting to save config of " + tunnel.getName());
tunnel.setConfig(newConfig)
.whenComplete((a, b) -> onConfigSaved(tunnel, b));
}
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onSaveInstanceState(final Bundle outState) {
outState.putParcelable(KEY_LOCAL_CONFIG, binding.getConfig());
outState.putString(KEY_ORIGINAL_NAME, tunnel == null ? null : tunnel.getName());
super.onSaveInstanceState(outState);
}
@Override
public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
tunnel = newTunnel;
if (binding == null)
return;
binding.setConfig(new Config.Observable(null, null));
if (tunnel != null)
tunnel.getConfigAsync().thenAccept(a -> onConfigLoaded(tunnel.getName(), a));
}
private void onTunnelCreated(final Tunnel newTunnel, @Nullable final Throwable throwable) {
final String message;
if (throwable == null) {
tunnel = newTunnel;
message = getString(R.string.tunnel_create_success, tunnel.getName());
Log.d(TAG, message);
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
onFinished();
} else {
final String error = ExceptionLoggers.unwrapMessage(throwable);
message = getString(R.string.tunnel_create_error, error);
Log.e(TAG, message, throwable);
if (binding != null) {
MonkeyedSnackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
}
}
}
private void onTunnelRenamed(final Tunnel renamedTunnel, final Config newConfig,
@Nullable final Throwable throwable) {
final String message;
if (throwable == null) {
message = getString(R.string.tunnel_rename_success, renamedTunnel.getName());
Log.d(TAG, message);
// Now save the rest of configuration changes.
Log.d(TAG, "Attempting to save config of renamed tunnel " + tunnel.getName());
renamedTunnel.setConfig(newConfig).whenComplete((a, b) -> onConfigSaved(renamedTunnel, b));
} else {
final String error = ExceptionLoggers.unwrapMessage(throwable);
message = getString(R.string.tunnel_rename_error, error);
Log.e(TAG, message, throwable);
if (binding != null) {
MonkeyedSnackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
}
}
}
@Override
public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
if (binding == null) {
return;
}
binding.setFragment(this);
if (savedInstanceState == null) {
onSelectedTunnelChanged(null, getSelectedTunnel());
} else {
tunnel = getSelectedTunnel();
final Config.Observable config = savedInstanceState.getParcelable(KEY_LOCAL_CONFIG);
final String originalName = savedInstanceState.getString(KEY_ORIGINAL_NAME);
if (tunnel != null && !tunnel.getName().equals(originalName))
onSelectedTunnelChanged(null, tunnel);
else
binding.setConfig(config);
}
super.onViewStateRestored(savedInstanceState);
}
public void onRequestSetExcludedApplications(@SuppressWarnings("unused") final View view) {
final FragmentManager fragmentManager = getFragmentManager();
if (fragmentManager != null && binding != null) {
final String[] excludedApps = Attribute.stringToList(binding.getConfig().getInterfaceSection().getExcludedApplications());
final AppListDialogFragment fragment = AppListDialogFragment.newInstance(excludedApps, this);
fragment.show(fragmentManager, null);
}
}
@Override
public void onExcludedAppsSelected(final List<String> excludedApps) {
Objects.requireNonNull(binding, "Tried to set excluded apps while no view was loaded");
binding.getConfig().getInterfaceSection().setExcludedApplications(Attribute.iterableToString(excludedApps));
}
}
@@ -1,490 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.OpenableColumns;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v4.app.FragmentManager;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.view.ActionMode;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.activity.TunnelCreatorActivity;
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter;
import com.wireguard.android.databinding.TunnelListFragmentBinding;
import com.wireguard.android.databinding.TunnelListItemBinding;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.android.util.ObservableSortedKeyedList;
import com.wireguard.android.widget.MonkeyedSnackbar;
import com.wireguard.android.widget.fab.FloatingActionsMenuRecyclerViewScrollListener;
import com.wireguard.config.Config;
import java.io.BufferedReader;
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.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java9.util.concurrent.CompletableFuture;
import java9.util.stream.StreamSupport;
/**
* Fragment containing a list of known WireGuard tunnels. It allows creating and deleting tunnels.
*/
public class TunnelListFragment extends BaseFragment {
private static final int REQUEST_IMPORT = 1;
private static final String TAG = "WireGuard/" + TunnelListFragment.class.getSimpleName();
private final ActionModeListener actionModeListener = new ActionModeListener();
@Nullable private ActionMode actionMode;
@Nullable private TunnelListFragmentBinding binding;
public boolean collapseActionMenu() {
if (binding != null && binding.createMenu.isExpanded()) {
binding.createMenu.collapse();
return true;
}
return false;
}
private void importTunnel(@NonNull final String configText) {
try {
// Ensure the config text is parseable before proceeding…
Config.from(configText);
// Config text is valid, now create the tunnel…
final FragmentManager fragmentManager = getFragmentManager();
if (fragmentManager != null)
ConfigNamingDialogFragment.newInstance(configText).show(fragmentManager, null);
} catch (final Exception exception) {
onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(exception));
}
}
private void importTunnel(@Nullable final Uri uri) {
final Activity activity = getActivity();
if (activity == null || uri == null)
return;
final ContentResolver contentResolver = activity.getContentResolver();
final Collection<CompletableFuture<Tunnel>> futureTunnels = new ArrayList<>();
final List<Throwable> throwables = new ArrayList<>();
Application.getAsyncWorker().supplyAsync(() -> {
final String[] columns = {OpenableColumns.DISPLAY_NAME};
String name = null;
try (Cursor cursor = contentResolver.query(uri, columns,
null, null, null)) {
if (cursor != null && cursor.moveToFirst() && !cursor.isNull(0))
name = cursor.getString(0);
}
if (name == null)
name = Uri.decode(uri.getLastPathSegment());
int idx = name.lastIndexOf('/');
if (idx >= 0) {
if (idx >= name.length() - 1)
throw new IllegalArgumentException("Illegal file name: " + name);
name = name.substring(idx + 1);
}
boolean isZip = name.toLowerCase().endsWith(".zip");
if (name.toLowerCase().endsWith(".conf"))
name = name.substring(0, name.length() - ".conf".length());
else if (!isZip)
throw new IllegalArgumentException("File must be .conf or .zip");
if (isZip) {
try (ZipInputStream zip = new ZipInputStream(contentResolver.openInputStream(uri))) {
BufferedReader reader = new BufferedReader(new InputStreamReader(zip, StandardCharsets.UTF_8));
ZipEntry entry;
while ((entry = zip.getNextEntry()) != null) {
if (entry.isDirectory())
continue;
name = entry.getName();
idx = name.lastIndexOf('/');
if (idx >= 0) {
if (idx >= name.length() - 1)
continue;
name = name.substring(name.lastIndexOf('/') + 1);
}
if (name.toLowerCase().endsWith(".conf"))
name = name.substring(0, name.length() - ".conf".length());
else
continue;
Config config = null;
try {
config = Config.from(reader);
} catch (Exception e) {
throwables.add(e);
}
if (config != null)
futureTunnels.add(Application.getTunnelManager().create(name, config).toCompletableFuture());
}
}
} else {
futureTunnels.add(Application.getTunnelManager().create(name,
Config.from(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");
}
return CompletableFuture.allOf(futureTunnels.toArray(new CompletableFuture[futureTunnels.size()]));
}).whenComplete((future, exception) -> {
if (exception != null) {
onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(exception));
} else {
future.whenComplete((ignored1, ignored2) -> {
final List<Tunnel> tunnels = new ArrayList<>(futureTunnels.size());
for (final CompletableFuture<Tunnel> futureTunnel : futureTunnels) {
Tunnel tunnel = null;
try {
tunnel = futureTunnel.getNow(null);
} catch (final Exception e) {
throwables.add(e);
}
if (tunnel != null)
tunnels.add(tunnel);
}
onTunnelImportFinished(tunnels, throwables);
});
}
});
}
@Override
public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
switch (requestCode) {
case REQUEST_IMPORT:
if (resultCode == Activity.RESULT_OK && data != null)
importTunnel(data.getData());
return;
case IntentIntegrator.REQUEST_CODE:
final IntentResult result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
if (result != null && result.getContents() != null) {
importTunnel(result.getContents());
}
return;
default:
super.onActivityResult(requestCode, resultCode, data);
}
}
@SuppressWarnings("deprecation")
@SuppressLint("ClickableViewAccessibility")
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
binding = TunnelListFragmentBinding.inflate(inflater, container, false);
binding.tunnelList.setOnTouchListener((view, motionEvent) -> {
if (binding != null) {
binding.createMenu.collapse();
}
return false;
});
binding.tunnelList.setOnScrollListener(new FloatingActionsMenuRecyclerViewScrollListener(binding.createMenu));
binding.executePendingBindings();
return binding.getRoot();
}
@Override
public void onDestroyView() {
binding = null;
super.onDestroyView();
}
public void onRequestCreateConfig(@SuppressWarnings("unused") final View view) {
startActivity(new Intent(getActivity(), TunnelCreatorActivity.class));
if (binding != null)
binding.createMenu.collapse();
}
public void onRequestImportConfig(@SuppressWarnings("unused") final View view) {
final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
startActivityForResult(intent, REQUEST_IMPORT);
if (binding != null)
binding.createMenu.collapse();
}
public void onRequestScanQRCode(@SuppressWarnings("unused") final View view) {
final IntentIntegrator intentIntegrator = IntentIntegrator.forSupportFragment(this);
intentIntegrator.setOrientationLocked(false);
intentIntegrator.setBeepEnabled(false);
intentIntegrator.setPrompt(getString(R.string.qrcode_hint));
intentIntegrator.initiateScan(Collections.singletonList(IntentIntegrator.QR_CODE));
if (binding != null)
binding.createMenu.collapse();
}
@Override
public void onPause() {
if (binding != null) {
binding.createMenu.collapse();
}
super.onPause();
}
@Override
public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
if (binding == null)
return;
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
if (newTunnel != null)
binding.tunnelList.findViewHolderForAdapterPosition(tunnels.indexOf(newTunnel)).itemView.setActivated(true);
if (oldTunnel != null)
binding.tunnelList.findViewHolderForAdapterPosition(tunnels.indexOf(oldTunnel)).itemView.setActivated(false);
});
/* Alternative 1: results in sluggish change:
if (binding.tunnelList.getAdapter() == null)
return;
* Alternative 2: results in overly quick change:
binding.tunnelList.getAdapter().notifyDataSetChanged();
* Hence, we go with the above.
*/
}
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);
message = getResources().getQuantityString(R.plurals.delete_error, count, count, error);
Log.e(TAG, message, throwable);
}
if (binding != null) {
MonkeyedSnackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
}
}
private void onTunnelImportFinished(final List<Tunnel> tunnels, final Collection<Throwable> throwables) {
String message = null;
for (final Throwable throwable : throwables) {
final String error = ExceptionLoggers.unwrapMessage(throwable);
message = getString(R.string.import_error, error);
Log.e(TAG, message, throwable);
}
if (tunnels.size() == 1 && throwables.isEmpty())
message = getString(R.string.import_success, tunnels.get(0).getName());
else if (tunnels.isEmpty() && throwables.size() == 1)
/* Use the exception message from above. */ ;
else if (throwables.isEmpty())
message = getResources().getQuantityString(R.plurals.import_total_success,
tunnels.size(), tunnels.size());
else if (!throwables.isEmpty())
message = getResources().getQuantityString(R.plurals.import_partial_success,
tunnels.size() + throwables.size(),
tunnels.size(), tunnels.size() + throwables.size());
if (binding != null)
MonkeyedSnackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
}
@Override
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putIntegerArrayList("CHECKED_ITEMS", actionModeListener.getCheckedItems());
}
@Override
public void onActivityCreated(@Nullable final Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
final Collection<Integer> checkedItems = savedInstanceState.getIntegerArrayList("CHECKED_ITEMS");
if (checkedItems != null) {
for (final Integer i : checkedItems)
actionModeListener.setItemChecked(i, true);
}
}
}
@Override
public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if (binding == null) {
return;
}
binding.setFragment(this);
Application.getTunnelManager().getTunnels().thenAccept(binding::setTunnels);
binding.setRowConfigurationHandler((ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TunnelListItemBinding, Tunnel>) (binding, tunnel, position) -> {
binding.setFragment(this);
binding.getRoot().setOnClickListener(clicked -> {
if (actionMode == null) {
setSelectedTunnel(tunnel);
} else {
actionModeListener.toggleItemChecked(position);
}
});
binding.getRoot().setOnLongClickListener(clicked -> {
actionModeListener.toggleItemChecked(position);
return true;
});
if (actionMode != null)
binding.getRoot().setActivated(actionModeListener.checkedItems.contains(position));
else
binding.getRoot().setActivated(getSelectedTunnel() == tunnel);
});
}
private final class ActionModeListener implements ActionMode.Callback {
private final Collection<Integer> checkedItems = new HashSet<>();
@Nullable private Resources resources;
@Override
public boolean onActionItemClicked(final ActionMode mode, final MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_action_delete:
final Iterable<Integer> copyCheckedItems = new HashSet<>(checkedItems);
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
final Collection<Tunnel> tunnelsToDelete = new ArrayList<>();
for (final Integer position : copyCheckedItems)
tunnelsToDelete.add(tunnels.get(position));
final CompletableFuture[] futures = StreamSupport.stream(tunnelsToDelete)
.map(Tunnel::delete)
.toArray(CompletableFuture[]::new);
CompletableFuture.allOf(futures)
.thenApply(x -> futures.length)
.whenComplete(TunnelListFragment.this::onTunnelDeletionFinished);
});
checkedItems.clear();
mode.finish();
return true;
case R.id.menu_action_select_all:
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
for (int i = 0; i < tunnels.size(); ++i) {
setItemChecked(i, true);
}
});
return true;
default:
return false;
}
}
@Override
public boolean onCreateActionMode(final ActionMode mode, final Menu menu) {
actionMode = mode;
if (getActivity() != null) {
resources = getActivity().getResources();
}
mode.getMenuInflater().inflate(R.menu.tunnel_list_action_mode, menu);
binding.tunnelList.getAdapter().notifyDataSetChanged();
return true;
}
@Override
public void onDestroyActionMode(final ActionMode mode) {
actionMode = null;
resources = null;
checkedItems.clear();
binding.tunnelList.getAdapter().notifyDataSetChanged();
}
void toggleItemChecked(final int position) {
setItemChecked(position, !checkedItems.contains(position));
}
public ArrayList<Integer> getCheckedItems() {
return new ArrayList<>(checkedItems);
}
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);
}
@Override
public boolean onPrepareActionMode(final ActionMode mode, final Menu menu) {
updateTitle(mode);
return false;
}
private void updateTitle(@Nullable final ActionMode mode) {
if (mode == null) {
return;
}
final int count = checkedItems.size();
if (count == 0) {
mode.setTitle("");
} else {
mode.setTitle(resources.getQuantityString(R.plurals.delete_title, count, count));
}
}
}
}
@@ -1,56 +0,0 @@
/*
* Copyright © 2018 Eric Kuck <eric@bluelinelabs.com>.
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.model;
import android.databinding.BaseObservable;
import android.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;
}
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);
}
@Override
public String getKey() {
return name;
}
}
@@ -1,156 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. 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.Nullable;
import com.wireguard.android.BR;
import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.config.Config;
import com.wireguard.util.Keyed;
import java.util.regex.Pattern;
import java9.util.concurrent.CompletableFuture;
import java9.util.concurrent.CompletionStage;
/**
* Encapsulates the volatile and nonvolatile state of a WireGuard tunnel.
*/
public class Tunnel extends BaseObservable implements Keyed<String> {
public static final int NAME_MAX_LENGTH = 15;
private static final Pattern NAME_PATTERN = Pattern.compile("[a-zA-Z0-9_=+.-]{1,15}");
private final TunnelManager manager;
@Nullable private Config config;
private String name;
private State state;
@Nullable private Statistics statistics;
Tunnel(final TunnelManager manager, final String name,
@Nullable final Config config, final State state) {
this.manager = manager;
this.name = name;
this.config = config;
this.state = state;
}
public static boolean isNameInvalid(final CharSequence name) {
return !NAME_PATTERN.matcher(name).matches();
}
public CompletionStage<Void> delete() {
return manager.delete(this);
}
@Bindable @Nullable
public Config getConfig() {
if (config == null)
manager.getTunnelConfig(this).whenComplete(ExceptionLoggers.E);
return config;
}
public CompletionStage<Config> getConfigAsync() {
if (config == null)
return manager.getTunnelConfig(this);
return CompletableFuture.completedFuture(config);
}
@Override
public String getKey() {
return name;
}
@Bindable
public String getName() {
return name;
}
@Bindable
public State getState() {
return state;
}
public CompletionStage<State> getStateAsync() {
return TunnelManager.getTunnelState(this);
}
@Bindable @Nullable
public Statistics getStatistics() {
// FIXME: Check age of statistics.
if (statistics == null)
TunnelManager.getTunnelStatistics(this).whenComplete(ExceptionLoggers.E);
return statistics;
}
public CompletionStage<Statistics> getStatisticsAsync() {
// FIXME: Check age of statistics.
if (statistics == null)
return TunnelManager.getTunnelStatistics(this);
return CompletableFuture.completedFuture(statistics);
}
Config onConfigChanged(final Config config) {
this.config = config;
notifyPropertyChanged(BR.config);
return config;
}
public String onNameChanged(final String name) {
this.name = name;
notifyPropertyChanged(BR.name);
return name;
}
State onStateChanged(final State state) {
if (state != State.UP)
onStatisticsChanged(null);
this.state = state;
notifyPropertyChanged(BR.state);
return state;
}
@Nullable
Statistics onStatisticsChanged(@Nullable final Statistics statistics) {
this.statistics = statistics;
notifyPropertyChanged(BR.statistics);
return statistics;
}
public CompletionStage<Config> setConfig(final Config config) {
if (!config.equals(this.config))
return manager.setTunnelConfig(this, config);
return CompletableFuture.completedFuture(this.config);
}
public CompletionStage<String> setName(final String name) {
if (!name.equals(this.name))
return manager.setTunnelName(this, name);
return CompletableFuture.completedFuture(this.name);
}
public CompletionStage<State> setState(final State state) {
if (state != this.state)
return manager.setTunnelState(this, state);
return CompletableFuture.completedFuture(this.state);
}
public enum State {
DOWN,
TOGGLE,
UP;
public static State of(final boolean running) {
return running ? UP : DOWN;
}
}
public static class Statistics extends BaseObservable { }
}
@@ -1,298 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.model;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.support.annotation.Nullable;
import com.wireguard.android.Application;
import com.wireguard.android.BR;
import com.wireguard.android.configStore.ConfigStore;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.model.Tunnel.Statistics;
import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.android.util.ObservableSortedKeyedArrayList;
import com.wireguard.android.util.ObservableSortedKeyedList;
import com.wireguard.config.Config;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Set;
import java9.util.Comparators;
import java9.util.concurrent.CompletableFuture;
import java9.util.concurrent.CompletionStage;
import java9.util.stream.Collectors;
import java9.util.stream.StreamSupport;
/**
* Maintains and mediates changes to the set of available WireGuard tunnels,
*/
public final class TunnelManager extends BaseObservable {
private static final Comparator<String> COMPARATOR = Comparators.<String>thenComparing(
String.CASE_INSENSITIVE_ORDER, Comparators.naturalOrder());
private static final String KEY_LAST_USED_TUNNEL = "last_used_tunnel";
private static final String KEY_RESTORE_ON_BOOT = "restore_on_boot";
private static final String KEY_RUNNING_TUNNELS = "enabled_configs";
private final ConfigStore configStore;
private final CompletableFuture<ObservableSortedKeyedList<String, Tunnel>> completableTunnels = new CompletableFuture<>();
private final ObservableSortedKeyedList<String, Tunnel> tunnels = new ObservableSortedKeyedArrayList<>(COMPARATOR);
@Nullable private Tunnel lastUsedTunnel;
private boolean haveLoaded;
private final ArrayList<CompletableFuture<Void>> delayedLoadRestoreTunnels = new ArrayList<>();
public TunnelManager(final ConfigStore configStore) {
this.configStore = configStore;
}
private Tunnel addToList(final String name, @Nullable final Config config, final State state) {
final Tunnel tunnel = new Tunnel(this, name, config, state);
tunnels.add(tunnel);
return tunnel;
}
public CompletionStage<Tunnel> create(final String name, @Nullable final Config config) {
if (Tunnel.isNameInvalid(name))
return CompletableFuture.failedFuture(new IllegalArgumentException("Invalid name"));
if (tunnels.containsKey(name)) {
final String message = "Tunnel " + name + " already exists";
return CompletableFuture.failedFuture(new IllegalArgumentException(message));
}
return Application.getAsyncWorker().supplyAsync(() -> configStore.create(name, config))
.thenApply(savedConfig -> addToList(name, savedConfig, State.DOWN));
}
CompletionStage<Void> delete(final Tunnel tunnel) {
final State originalState = tunnel.getState();
final boolean wasLastUsed = tunnel == lastUsedTunnel;
// Make sure nothing touches the tunnel.
if (wasLastUsed)
setLastUsedTunnel(null);
tunnels.remove(tunnel);
return Application.getAsyncWorker().runAsync(() -> {
if (originalState == State.UP)
Application.getBackend().setState(tunnel, State.DOWN);
try {
configStore.delete(tunnel.getName());
} catch (final Exception e) {
if (originalState == State.UP)
Application.getBackend().setState(tunnel, State.UP);
// Re-throw the exception to fail the completion.
throw e;
}
}).whenComplete((x, e) -> {
if (e == null)
return;
// Failure, put the tunnel back.
tunnels.add(tunnel);
if (wasLastUsed)
setLastUsedTunnel(tunnel);
});
}
@Bindable @Nullable
public Tunnel getLastUsedTunnel() {
return lastUsedTunnel;
}
CompletionStage<Config> getTunnelConfig(final Tunnel tunnel) {
return Application.getAsyncWorker().supplyAsync(() -> configStore.load(tunnel.getName()))
.thenApply(tunnel::onConfigChanged);
}
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);
}
public CompletableFuture<ObservableSortedKeyedList<String, Tunnel>> getTunnels() {
return completableTunnels;
}
public void onCreate() {
Application.getAsyncWorker().supplyAsync(configStore::enumerate)
.thenAcceptBoth(Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().enumerate()), this::onTunnelsLoaded)
.whenComplete(ExceptionLoggers.E);
}
@SuppressWarnings("unchecked")
private void onTunnelsLoaded(final Iterable<String> present, final Collection<String> running) {
for (final String name : present)
addToList(name, null, running.contains(name) ? State.UP : State.DOWN);
final String lastUsedName = Application.getSharedPreferences().getString(KEY_LAST_USED_TUNNEL, null);
if (lastUsedName != null)
setLastUsedTunnel(tunnels.get(lastUsedName));
final CompletableFuture<Void>[] toComplete;
synchronized (delayedLoadRestoreTunnels) {
haveLoaded = true;
toComplete = delayedLoadRestoreTunnels.toArray(new CompletableFuture[delayedLoadRestoreTunnels.size()]);
delayedLoadRestoreTunnels.clear();
}
restoreState(true).whenComplete((v, t) -> {
for (final CompletableFuture<Void> f : toComplete) {
if (t == null)
f.complete(v);
else
f.completeExceptionally(t);
}
});
completableTunnels.complete(tunnels);
}
public void refreshTunnelStates() {
Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().enumerate())
.thenAccept(running -> {
for (final Tunnel tunnel : tunnels)
tunnel.onStateChanged(running.contains(tunnel.getName()) ? State.UP : State.DOWN);
})
.whenComplete(ExceptionLoggers.E);
}
public CompletionStage<Void> restoreState(final boolean force) {
if (!force && !Application.getSharedPreferences().getBoolean(KEY_RESTORE_ON_BOOT, false))
return CompletableFuture.completedFuture(null);
synchronized (delayedLoadRestoreTunnels) {
if (!haveLoaded) {
final CompletableFuture<Void> f = new CompletableFuture<>();
delayedLoadRestoreTunnels.add(f);
return f;
}
}
final Set<String> previouslyRunning = Application.getSharedPreferences().getStringSet(KEY_RUNNING_TUNNELS, null);
if (previouslyRunning == null)
return CompletableFuture.completedFuture(null);
return CompletableFuture.allOf(StreamSupport.stream(tunnels)
.filter(tunnel -> previouslyRunning.contains(tunnel.getName()))
.map(tunnel -> setTunnelState(tunnel, State.UP))
.toArray(CompletableFuture[]::new));
}
public void saveState() {
final Set<String> runningTunnels = StreamSupport.stream(tunnels)
.filter(tunnel -> tunnel.getState() == State.UP)
.map(Tunnel::getName)
.collect(Collectors.toUnmodifiableSet());
Application.getSharedPreferences().edit().putStringSet(KEY_RUNNING_TUNNELS, runningTunnels).apply();
}
private void setLastUsedTunnel(@Nullable final Tunnel tunnel) {
if (tunnel == lastUsedTunnel)
return;
lastUsedTunnel = tunnel;
notifyPropertyChanged(BR.lastUsedTunnel);
if (tunnel != null)
Application.getSharedPreferences().edit().putString(KEY_LAST_USED_TUNNEL, tunnel.getName()).apply();
else
Application.getSharedPreferences().edit().remove(KEY_LAST_USED_TUNNEL).apply();
}
CompletionStage<Config> setTunnelConfig(final Tunnel tunnel, final Config config) {
return Application.getAsyncWorker().supplyAsync(() -> {
final Config appliedConfig = Application.getBackend().applyConfig(tunnel, config);
return configStore.save(tunnel.getName(), appliedConfig);
}).thenApply(tunnel::onConfigChanged);
}
CompletionStage<String> setTunnelName(final Tunnel tunnel, final String name) {
if (Tunnel.isNameInvalid(name))
return CompletableFuture.failedFuture(new IllegalArgumentException("Invalid name"));
if (tunnels.containsKey(name)) {
final String message = "Tunnel " + name + " already exists";
return CompletableFuture.failedFuture(new IllegalArgumentException(message));
}
final State originalState = tunnel.getState();
final boolean wasLastUsed = tunnel == lastUsedTunnel;
// Make sure nothing touches the tunnel.
if (wasLastUsed)
setLastUsedTunnel(null);
tunnels.remove(tunnel);
return Application.getAsyncWorker().supplyAsync(() -> {
if (originalState == State.UP)
Application.getBackend().setState(tunnel, State.DOWN);
configStore.rename(tunnel.getName(), name);
final String newName = tunnel.onNameChanged(name);
if (originalState == State.UP)
Application.getBackend().setState(tunnel, State.UP);
return newName;
}).whenComplete((newName, e) -> {
// On failure, we don't know what state the tunnel might be in. Fix that.
if (e != null)
getTunnelState(tunnel);
// Add the tunnel back to the manager, under whatever name it thinks it has.
tunnels.add(tunnel);
if (wasLastUsed)
setLastUsedTunnel(tunnel);
});
}
CompletionStage<State> setTunnelState(final Tunnel tunnel, final State state) {
// Ensure the configuration is loaded before trying to use it.
return tunnel.getConfigAsync().thenCompose(x ->
Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().setState(tunnel, state))
).whenComplete((newState, e) -> {
// Ensure onStateChanged is always called (failure or not), and with the correct state.
tunnel.onStateChanged(e == null ? newState : tunnel.getState());
if (e == null && newState == State.UP)
setLastUsedTunnel(tunnel);
saveState();
});
}
public static final class IntentReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context context, @Nullable final Intent intent) {
final TunnelManager manager = Application.getTunnelManager();
if (intent == null)
return;
final String action = intent.getAction();
if (action == null)
return;
if ("com.wireguard.android.action.REFRESH_TUNNEL_STATES".equals(action)) {
manager.refreshTunnelStates();
return;
}
/* We disable the below, for now, as the security model of allowing this
* might take a bit more consideration.
*/
if (true)
return;
final State state;
if ("com.wireguard.android.action.SET_TUNNEL_UP".equals(action))
state = State.UP;
else if ("com.wireguard.android.action.SET_TUNNEL_DOWN".equals(action))
state = State.DOWN;
else
return;
final String tunnelName = intent.getStringExtra("tunnel");
if (tunnelName == null)
return;
manager.getTunnels().thenAccept(tunnels -> {
final Tunnel tunnel = tunnels.get(tunnelName);
if (tunnel == null)
return;
manager.setTunnelState(tunnel, state);
});
}
}
}
@@ -1,117 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Environment;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v7.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.FragmentUtils;
import com.wireguard.android.widget.MonkeyedSnackbar;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* Preference implementing a button that asynchronously exports logs.
*/
public class LogExporterPreference extends Preference {
private static final String TAG = "WireGuard/" + LogExporterPreference.class.getSimpleName();
@Nullable private String exportedFilePath;
public LogExporterPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
private void exportLog() {
Application.getAsyncWorker().supplyAsync(() -> {
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");
/* We would like to simply run `builder.redirectOutput(file);`, but this is API 26.
* Instead we have to do this dance, since logcat appends.
*/
new FileOutputStream(file).close();
try {
final Process process = Runtime.getRuntime().exec(new String[]{
"logcat", "-b", "all", "-d", "-v", "threadtime", "-f", file.getAbsolutePath(), "*:V"});
if (process.waitFor() != 0) {
try (final BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
final StringBuilder errors = new StringBuilder();
errors.append("Unable to run logcat: ");
String line;
while ((line = reader.readLine()) != null)
errors.append(line);
throw new Exception(errors.toString());
}
}
} catch (final Exception e) {
// noinspection ResultOfMethodCallIgnored
file.delete();
throw e;
}
return file.getAbsolutePath();
}).whenComplete(this::exportLogComplete);
}
private void exportLogComplete(final String filePath, @Nullable final Throwable throwable) {
if (throwable != null) {
final String error = ExceptionLoggers.unwrapMessage(throwable);
final String message = getContext().getString(R.string.log_export_error, error);
Log.e(TAG, message, throwable);
MonkeyedSnackbar.make(
FragmentUtils.getPrefActivity(this).findViewById(android.R.id.content),
message, Snackbar.LENGTH_LONG).show();
setEnabled(true);
} else {
exportedFilePath = filePath;
notifyChanged();
}
}
@Override
public CharSequence getSummary() {
return exportedFilePath == null ?
getContext().getString(R.string.log_export_summary) :
getContext().getString(R.string.log_export_success, exportedFilePath);
}
@Override
public CharSequence getTitle() {
return getContext().getString(R.string.log_exporter_title);
}
@Override
protected void onClick() {
FragmentUtils.getPrefActivity(this).ensurePermissions(
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
(permissions, granted) -> {
if (granted.length > 0 && granted[0] == PackageManager.PERMISSION_GRANTED) {
setEnabled(false);
exportLog();
}
});
}
}
@@ -1,103 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference;
import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v7.preference.Preference;
import android.util.AttributeSet;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.util.ToolsInstaller;
/**
* Preference implementing a button that asynchronously runs {@code ToolsInstaller} and displays the
* result as the preference summary.
*/
public class ToolsInstallerPreference extends Preference {
private State state = State.INITIAL;
public ToolsInstallerPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
@Override
public CharSequence getSummary() {
return getContext().getString(state.messageResourceId);
}
@Override
public CharSequence getTitle() {
return getContext().getString(R.string.tools_installer_title);
}
@Override
public void onAttached() {
super.onAttached();
Application.getAsyncWorker().supplyAsync(Application.getToolsInstaller()::areInstalled).whenComplete(this::onCheckResult);
}
private void onCheckResult(final int state, @Nullable final Throwable throwable) {
if (throwable != null || state == ToolsInstaller.ERROR)
setState(State.INITIAL);
else if ((state & ToolsInstaller.YES) == ToolsInstaller.YES)
setState(State.ALREADY);
else if ((state & (ToolsInstaller.MAGISK | ToolsInstaller.NO)) == (ToolsInstaller.MAGISK | ToolsInstaller.NO))
setState(State.INITIAL_MAGISK);
else if ((state & (ToolsInstaller.SYSTEM | ToolsInstaller.NO)) == (ToolsInstaller.SYSTEM | ToolsInstaller.NO))
setState(State.INITIAL_SYSTEM);
else
setState(State.INITIAL);
}
@Override
protected void onClick() {
setState(State.WORKING);
Application.getAsyncWorker().supplyAsync(Application.getToolsInstaller()::install).whenComplete(this::onInstallResult);
}
private void onInstallResult(final Integer result, @Nullable final Throwable throwable) {
if (throwable != null)
setState(State.FAILURE);
else if ((result & (ToolsInstaller.YES | ToolsInstaller.MAGISK)) == (ToolsInstaller.YES | ToolsInstaller.MAGISK))
setState(State.SUCCESS_MAGISK);
else if ((result & (ToolsInstaller.YES | ToolsInstaller.SYSTEM)) == (ToolsInstaller.YES | ToolsInstaller.SYSTEM))
setState(State.SUCCESS_SYSTEM);
else
setState(State.FAILURE);
}
private void setState(final State state) {
if (this.state == state)
return;
this.state = state;
if (isEnabled() != state.shouldEnableView)
setEnabled(state.shouldEnableView);
notifyChanged();
}
private enum State {
INITIAL(R.string.tools_installer_initial, true),
ALREADY(R.string.tools_installer_already, false),
FAILURE(R.string.tools_installer_failure, true),
WORKING(R.string.tools_installer_working, false),
INITIAL_SYSTEM(R.string.tools_installer_initial_system, true),
SUCCESS_SYSTEM(R.string.tools_installer_success_system, false),
INITIAL_MAGISK(R.string.tools_installer_initial_magisk, true),
SUCCESS_MAGISK(R.string.tools_installer_success_magisk, false);
private final int messageResourceId;
private final boolean shouldEnableView;
State(final int messageResourceId, final boolean shouldEnableView) {
this.messageResourceId = messageResourceId;
this.shouldEnableView = shouldEnableView;
}
}
}
@@ -1,56 +0,0 @@
/*
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. 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.annotation.Nullable;
import android.support.v7.preference.Preference;
import android.util.AttributeSet;
import com.wireguard.android.Application;
import com.wireguard.android.BuildConfig;
import com.wireguard.android.R;
public class VersionPreference extends Preference {
@Nullable private String versionSummary;
public VersionPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
Application.getBackendAsync().thenAccept(backend -> {
versionSummary = getContext().getString(R.string.version_summary_checking, backend.getTypeName().toLowerCase());
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());
notifyChanged();
});
});
}
@Override @Nullable
public CharSequence getSummary() {
return versionSummary;
}
@Override
public CharSequence getTitle() {
return getContext().getString(R.string.version_title, BuildConfig.VERSION_NAME);
}
@Override
protected void onClick() {
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://www.wireguard.com/"));
try {
getContext().startActivity(intent);
} catch (final ActivityNotFoundException ignored) { }
}
}
@@ -1,126 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Environment;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v7.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.FragmentUtils;
import com.wireguard.android.widget.MonkeyedSnackbar;
import com.wireguard.config.Config;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import java9.util.concurrent.CompletableFuture;
/**
* Preference implementing a button that asynchronously exports config zips.
*/
public class ZipExporterPreference extends Preference {
private static final String TAG = "WireGuard/" + ZipExporterPreference.class.getSimpleName();
@Nullable private String exportedFilePath;
public ZipExporterPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
private void exportZip() {
Application.getTunnelManager().getTunnels().thenAccept(this::exportZip);
}
private void exportZip(final List<Tunnel> tunnels) {
final List<CompletableFuture<Config>> futureConfigs = new ArrayList<>(tunnels.size());
for (final Tunnel tunnel : tunnels)
futureConfigs.add(tunnel.getConfigAsync().toCompletableFuture());
if (futureConfigs.isEmpty()) {
exportZipComplete(null, new IllegalArgumentException("No tunnels exist"));
return;
}
CompletableFuture.allOf(futureConfigs.toArray(new CompletableFuture[futureConfigs.size()]))
.whenComplete((ignored1, exception) -> Application.getAsyncWorker().supplyAsync(() -> {
if (exception != null)
throw exception;
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");
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));
}
zip.closeEntry();
} catch (final Exception e) {
// noinspection ResultOfMethodCallIgnored
file.delete();
throw e;
}
return file.getAbsolutePath();
}).whenComplete(this::exportZipComplete));
}
private void exportZipComplete(@Nullable final String filePath, @Nullable final Throwable throwable) {
if (throwable != null) {
final String error = ExceptionLoggers.unwrapMessage(throwable);
final String message = getContext().getString(R.string.zip_export_error, error);
Log.e(TAG, message, throwable);
MonkeyedSnackbar.make(
FragmentUtils.getPrefActivity(this).findViewById(android.R.id.content),
message, Snackbar.LENGTH_LONG).show();
setEnabled(true);
} else {
exportedFilePath = filePath;
notifyChanged();
}
}
@Override
public CharSequence getSummary() {
return exportedFilePath == null ?
getContext().getString(R.string.zip_export_summary) :
getContext().getString(R.string.zip_export_success, exportedFilePath);
}
@Override
public CharSequence getTitle() {
return getContext().getString(R.string.zip_exporter_title);
}
@Override
protected void onClick() {
FragmentUtils.getPrefActivity(this).ensurePermissions(
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
(permissions, granted) -> {
if (granted.length > 0 && granted[0] == PackageManager.PERMISSION_GRANTED) {
setEnabled(false);
exportZip();
}
});
}
}
@@ -1,64 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.os.Handler;
import java.util.concurrent.Executor;
import java9.util.concurrent.CompletableFuture;
import java9.util.concurrent.CompletionStage;
/**
* Helper class for running asynchronous tasks and ensuring they are completed on the main thread.
*/
public class AsyncWorker {
private final Executor executor;
private final Handler handler;
public AsyncWorker(final Executor executor, final Handler handler) {
this.executor = executor;
this.handler = handler;
}
public CompletionStage<Void> runAsync(final AsyncRunnable<?> runnable) {
final CompletableFuture<Void> future = new CompletableFuture<>();
executor.execute(() -> {
try {
runnable.run();
handler.post(() -> future.complete(null));
} catch (final Throwable t) {
handler.post(() -> future.completeExceptionally(t));
}
});
return future;
}
public <T> CompletionStage<T> supplyAsync(final AsyncSupplier<T, ?> supplier) {
final CompletableFuture<T> future = new CompletableFuture<>();
executor.execute(() -> {
try {
final T result = supplier.get();
handler.post(() -> future.complete(result));
} catch (final Throwable t) {
handler.post(() -> future.completeExceptionally(t));
}
});
return future;
}
@FunctionalInterface
public interface AsyncRunnable<E extends Throwable> {
void run() throws E;
}
@FunctionalInterface
public interface AsyncSupplier<T, E extends Throwable> {
T get() throws E;
}
}
@@ -1,40 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.support.design.widget.Snackbar;
import android.view.View;
import android.widget.TextView;
import com.wireguard.android.widget.MonkeyedSnackbar;
/**
* Standalone utilities for interacting with the system clipboard.
*/
public final class ClipboardUtils {
private ClipboardUtils() {
// Prevent instantiation
}
public static void copyTextView(final View view) {
if (!(view instanceof TextView))
return;
final CharSequence text = ((TextView) view).getText();
if (text == null || text.length() == 0)
return;
final Object service = view.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
if (!(service instanceof ClipboardManager))
return;
final CharSequence description = view.getContentDescription();
((ClipboardManager) service).setPrimaryClip(ClipData.newPlainText(description, text));
MonkeyedSnackbar.make(view, description + " copied to clipboard", Snackbar.LENGTH_LONG).show();
}
}
@@ -1,53 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.support.annotation.Nullable;
import android.util.Log;
import java9.util.concurrent.CompletionException;
import java9.util.function.BiConsumer;
/**
* Helpers for logging exceptions from asynchronous tasks. These can be passed to
* {@code CompletionStage.whenComplete()} at the end of an asynchronous future chain.
*/
public enum ExceptionLoggers implements BiConsumer<Object, Throwable> {
D(Log.DEBUG),
E(Log.ERROR);
private static final String TAG = "WireGuard/" + ExceptionLoggers.class.getSimpleName();
private final int priority;
ExceptionLoggers(final int priority) {
this.priority = priority;
}
public static Throwable unwrap(final Throwable throwable) {
if (throwable instanceof CompletionException && throwable.getCause() != null)
return throwable.getCause();
return throwable;
}
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, @Nullable final Throwable throwable) {
if (throwable != null)
Log.println(Log.ERROR, TAG, Log.getStackTraceString(throwable));
else if (priority <= Log.DEBUG)
Log.println(priority, TAG, "Future completed successfully");
}
}
@@ -1,25 +0,0 @@
/*
* Copyright © 2018 Harsh Shandilya <msfjarvis@gmail.com>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. 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 android.view.ContextThemeWrapper;
import com.wireguard.android.activity.SettingsActivity;
public class FragmentUtils {
public static SettingsActivity getPrefActivity(final Preference preference) {
final Context context = preference.getContext();
if (context instanceof ContextThemeWrapper) {
if (((ContextThemeWrapper) context).getBaseContext() instanceof SettingsActivity) {
return ((SettingsActivity) ((ContextThemeWrapper) context).getBaseContext());
}
}
return null;
}
}
@@ -1,108 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.databinding.ObservableArrayList;
import android.support.annotation.Nullable;
import com.wireguard.util.Keyed;
import java.util.Collection;
import java.util.ListIterator;
import java.util.Objects;
/**
* ArrayList that allows looking up elements by some key property. As the key property must always
* be retrievable, this list cannot hold {@code null} elements. Because this class places no
* restrictions on the order or duplication of keys, lookup by key, as well as all list modification
* operations, require O(n) time.
*/
public class ObservableKeyedArrayList<K, E extends Keyed<? extends K>>
extends ObservableArrayList<E> implements ObservableKeyedList<K, E> {
@Override
public boolean add(@Nullable final E e) {
if (e == null)
throw new NullPointerException("Trying to add a null element");
return super.add(e);
}
@Override
public void add(final int index, @Nullable final E e) {
if (e == null)
throw new NullPointerException("Trying to add a null element");
super.add(index, e);
}
@Override
public boolean addAll(final Collection<? extends E> c) {
if (c.contains(null))
throw new NullPointerException("Trying to add a collection with null element(s)");
return super.addAll(c);
}
@Override
public boolean addAll(final int index, final Collection<? extends E> c) {
if (c.contains(null))
throw new NullPointerException("Trying to add a collection with null element(s)");
return super.addAll(index, c);
}
@Override
public boolean containsAllKeys(final Collection<K> keys) {
for (final K key : keys)
if (!containsKey(key))
return false;
return true;
}
@Override
public boolean containsKey(final K key) {
return indexOfKey(key) >= 0;
}
@Override @Nullable
public E get(final K key) {
final int index = indexOfKey(key);
return index >= 0 ? get(index) : null;
}
@Override @Nullable
public E getLast(final K key) {
final int index = lastIndexOfKey(key);
return index >= 0 ? get(index) : null;
}
@Override
public int indexOfKey(final K key) {
final ListIterator<E> iterator = listIterator();
while (iterator.hasNext()) {
final int index = iterator.nextIndex();
if (Objects.equals(iterator.next().getKey(), key))
return index;
}
return -1;
}
@Override
public int lastIndexOfKey(final K key) {
final ListIterator<E> iterator = listIterator(size());
while (iterator.hasPrevious()) {
final int index = iterator.previousIndex();
if (Objects.equals(iterator.previous().getKey(), key))
return index;
}
return -1;
}
@Override
public E set(final int index, @Nullable final E e) {
if (e == null)
throw new NullPointerException("Trying to set a null key");
return super.set(index, e);
}
}
@@ -1,20 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.databinding.ObservableList;
import com.wireguard.util.Keyed;
import com.wireguard.util.KeyedList;
/**
* A list that is both keyed and observable.
*/
public interface ObservableKeyedList<K, E extends Keyed<? extends K>>
extends KeyedList<K, E>, ObservableList<E> {
}
@@ -1,200 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.support.annotation.Nullable;
import com.wireguard.util.Keyed;
import com.wireguard.util.SortedKeyedList;
import java.util.AbstractList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.Spliterator;
/**
* KeyedArrayList that enforces uniqueness and sorted order across the set of keys. This class uses
* binary search to improve lookup and replacement times to O(log(n)). However, due to the
* array-based nature of this class, insertion and removal of elements with anything but the largest
* key still require O(n) time.
*/
public class ObservableSortedKeyedArrayList<K, E extends Keyed<? extends K>>
extends ObservableKeyedArrayList<K, E> implements ObservableSortedKeyedList<K, E> {
@Nullable
private final Comparator<? super K> comparator;
private final transient KeyList<K, E> keyList = new KeyList<>(this);
@SuppressWarnings("WeakerAccess")
public ObservableSortedKeyedArrayList() {
comparator = null;
}
public ObservableSortedKeyedArrayList(final Comparator<? super K> comparator) {
this.comparator = comparator;
}
public ObservableSortedKeyedArrayList(final Collection<? extends E> c) {
this();
addAll(c);
}
public ObservableSortedKeyedArrayList(final SortedKeyedList<K, E> other) {
this(other.comparator());
addAll(other);
}
@Override
public boolean add(final E e) {
final int insertionPoint = getInsertionPoint(e);
if (insertionPoint < 0) {
// Skipping insertion is non-destructive if the new and existing objects are the same.
if (e == get(-insertionPoint - 1))
return false;
throw new IllegalArgumentException("Element with same key already exists in list");
}
super.add(insertionPoint, e);
return true;
}
@Override
public void add(final int index, final E e) {
final int insertionPoint = getInsertionPoint(e);
if (insertionPoint < 0)
throw new IllegalArgumentException("Element with same key already exists in list");
if (insertionPoint != index)
throw new IndexOutOfBoundsException("Wrong index given for element");
super.add(index, e);
}
@Override
public boolean addAll(final Collection<? extends E> c) {
boolean didChange = false;
for (final E e : c)
if (add(e))
didChange = true;
return didChange;
}
@Override
public boolean addAll(int index, final Collection<? extends E> c) {
for (final E e : c)
add(index++, e);
return true;
}
@Nullable
@Override
public Comparator<? super K> comparator() {
return comparator;
}
@Override
public K firstKey() {
if (isEmpty())
// The parameter in the exception is only to shut
// lint up, we never care for the exception message.
throw new NoSuchElementException("Empty set");
return get(0).getKey();
}
private int getInsertionPoint(final E e) {
if (comparator != null) {
return -Collections.binarySearch(keyList, e.getKey(), comparator) - 1;
} else {
@SuppressWarnings("unchecked") final List<Comparable<? super K>> list =
(List<Comparable<? super K>>) keyList;
return -Collections.binarySearch(list, e.getKey()) - 1;
}
}
@Override
public int indexOfKey(final K key) {
final int index;
if (comparator != null) {
index = Collections.binarySearch(keyList, key, comparator);
} else {
@SuppressWarnings("unchecked") final List<Comparable<? super K>> list =
(List<Comparable<? super K>>) keyList;
index = Collections.binarySearch(list, key);
}
return index >= 0 ? index : -1;
}
@Override
public Set<K> keySet() {
return keyList;
}
@Override
public int lastIndexOfKey(final K key) {
// There can never be more than one element with the same key in the list.
return indexOfKey(key);
}
@Override
public K lastKey() {
if (isEmpty())
// The parameter in the exception is only to shut
// lint up, we never care for the exception message.
throw new NoSuchElementException("Empty set");
return get(size() - 1).getKey();
}
@Override
public E set(final int index, final E e) {
final int order;
if (comparator != null) {
order = comparator.compare(e.getKey(), get(index).getKey());
} else {
@SuppressWarnings("unchecked") final Comparable<? super K> key =
(Comparable<? super K>) e.getKey();
order = key.compareTo(get(index).getKey());
}
if (order != 0) {
// Allow replacement if the new key would be inserted adjacent to the replaced element.
final int insertionPoint = getInsertionPoint(e);
if (insertionPoint < index || insertionPoint > index + 1)
throw new IndexOutOfBoundsException("Wrong index given for element");
}
return super.set(index, e);
}
@Override
public Collection<E> values() {
return this;
}
private static final class KeyList<K, E extends Keyed<? extends K>>
extends AbstractList<K> implements Set<K> {
private final ObservableSortedKeyedArrayList<K, E> list;
private KeyList(final ObservableSortedKeyedArrayList<K, E> list) {
this.list = list;
}
@Override
public K get(final int index) {
return list.get(index).getKey();
}
@Override
public int size() {
return list.size();
}
@Override
@SuppressWarnings("EmptyMethod")
public Spliterator<K> spliterator() {
return super.spliterator();
}
}
}
@@ -1,18 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import com.wireguard.util.Keyed;
import com.wireguard.util.SortedKeyedList;
/**
* A list that is both sorted/keyed and observable.
*/
public interface ObservableSortedKeyedList<K, E extends Keyed<? extends K>>
extends ObservableKeyedList<K, E>, SortedKeyedList<K, E> {
}
@@ -1,74 +0,0 @@
/*
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public final class SharedLibraryLoader {
private static final String TAG = "WireGuard/" + SharedLibraryLoader.class.getSimpleName();
private SharedLibraryLoader() { }
public static void loadSharedLibrary(final Context context, final String libName) {
Throwable noAbiException;
try {
System.loadLibrary(libName);
return;
} catch (final UnsatisfiedLinkError e) {
Log.d(TAG, "Failed to load library normally, so attempting to extract from apk", e);
noAbiException = e;
}
final ZipFile zipFile;
try {
zipFile = new ZipFile(new File(context.getApplicationInfo().sourceDir), ZipFile.OPEN_READ);
} catch (final IOException e) {
throw new RuntimeException(e);
}
final String mappedLibName = System.mapLibraryName(libName);
final byte[] buffer = new byte[1024 * 32];
for (final String abi : Build.SUPPORTED_ABIS) {
final String libZipPath = "lib" + File.separatorChar + abi + File.separatorChar + mappedLibName;
final ZipEntry zipEntry = zipFile.getEntry(libZipPath);
if (zipEntry == null)
continue;
File f = null;
try {
f = File.createTempFile("lib", ".so", context.getCacheDir());
Log.d(TAG, "Extracting apk:/" + libZipPath + " to " + f.getAbsolutePath() + " and loading");
try (final FileOutputStream out = new FileOutputStream(f);
final InputStream in = zipFile.getInputStream(zipEntry)) {
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
}
System.load(f.getAbsolutePath());
return;
} catch (final Exception e) {
Log.d(TAG, "Failed to load library apk:/" + libZipPath, e);
noAbiException = e;
} finally {
if (f != null)
// noinspection ResultOfMethodCallIgnored
f.delete();
}
}
if (noAbiException instanceof RuntimeException)
throw (RuntimeException) noAbiException;
throw new RuntimeException(noAbiException);
}
}
@@ -1,188 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.content.Context;
import android.support.annotation.Nullable;
import android.system.OsConstants;
import android.util.Log;
import com.wireguard.android.Application;
import com.wireguard.android.BuildConfig;
import com.wireguard.android.util.RootShell.NoRootException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
/**
* Helper to install WireGuard tools to the system partition.
*/
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 SYSTEM = 0x8;
private static final String[][] EXECUTABLES = {
{"libwg.so", "wg"},
{"libwg-quick.so", "wg-quick"},
};
private static final File[] INSTALL_DIRS = {
new File("/system/xbin"),
new File("/system/bin"),
};
@Nullable private static final File INSTALL_DIR = getInstallDir();
private static final String TAG = "WireGuard/" + ToolsInstaller.class.getSimpleName();
private final File localBinaryDir;
private final Object lock = new Object();
private final File nativeLibraryDir;
@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);
}
@Nullable
private static File getInstallDir() {
final String path = System.getenv("PATH");
if (path == null)
return INSTALL_DIRS[0];
final List<String> paths = Arrays.asList(path.split(":"));
for (final File dir : INSTALL_DIRS) {
if (paths.contains(dir.getPath()) && dir.isDirectory())
return dir;
}
return null;
}
public int areInstalled() throws NoRootException {
if (INSTALL_DIR == null)
return ERROR;
final StringBuilder script = new StringBuilder();
for (final String[] names : EXECUTABLES) {
script.append(String.format("cmp -s '%s' '%s' && ",
new File(nativeLibraryDir, names[0]),
new File(INSTALL_DIR, names[1])));
}
script.append("exit ").append(OsConstants.EALREADY).append(';');
try {
final int ret = Application.getRootShell().run(null, script.toString());
if (ret == OsConstants.EALREADY)
return willInstallAsMagiskModule() ? YES | MAGISK : YES | SYSTEM;
else
return willInstallAsMagiskModule() ? NO | MAGISK : NO | SYSTEM;
} catch (final IOException ignored) {
return ERROR;
}
}
public void ensureToolsAvailable() throws FileNotFoundException, NoRootException {
synchronized (lock) {
if (areToolsAvailable == null) {
final int ret = symlink();
if (ret == OsConstants.EALREADY) {
Log.d(TAG, "Tools were already symlinked into our private binary dir");
areToolsAvailable = true;
} else if (ret == OsConstants.EXIT_SUCCESS) {
Log.d(TAG, "Tools are now symlinked into our private binary dir");
areToolsAvailable = true;
} else {
Log.e(TAG, "For some reason, wg and wg-quick are not available at all");
areToolsAvailable = false;
}
}
if (!areToolsAvailable)
throw new FileNotFoundException("Required tools unavailable");
}
}
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;
}
}
private int installSystem() throws NoRootException {
if (INSTALL_DIR == null)
return OsConstants.ENOENT;
final StringBuilder script = new StringBuilder("set -ex; ");
script.append("trap 'mount -o ro,remount /system' EXIT; mount -o rw,remount /system; ");
for (final String[] names : EXECUTABLES) {
final File destination = new File(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));
}
try {
return Application.getRootShell().run(null, script.toString()) == 0 ? YES | SYSTEM : ERROR;
} catch (final IOException ignored) {
return ERROR;
}
}
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) {
script.append(String.format("test '%s' -ef '%s' && ",
new File(nativeLibraryDir, names[0]),
new File(localBinaryDir, names[1])));
}
script.append("exit ").append(OsConstants.EALREADY).append("; set -e; ");
for (final String[] names : EXECUTABLES) {
script.append(String.format("ln -fns '%s' '%s'; ",
new File(nativeLibraryDir, names[0]),
new File(localBinaryDir, names[1])));
}
script.append("exit ").append(OsConstants.EXIT_SUCCESS).append(';');
try {
return Application.getRootShell().run(null, script.toString());
} catch (final IOException ignored) {
return OsConstants.EXIT_FAILURE;
}
}
}
@@ -1,54 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget;
import android.support.annotation.Nullable;
import android.text.InputFilter;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import com.wireguard.crypto.KeyEncoding;
/**
* InputFilter for entering WireGuard private/public keys encoded with base64.
*/
public class KeyInputFilter implements InputFilter {
private static boolean isAllowed(final char c) {
return Character.isLetterOrDigit(c) || c == '+' || c == '/';
}
public static InputFilter newInstance() {
return new KeyInputFilter();
}
@Override @Nullable
public CharSequence filter(final CharSequence source,
final int sStart, final int sEnd,
final Spanned dest,
final int dStart, final int dEnd) {
SpannableStringBuilder replacement = null;
int rIndex = 0;
final int dLength = dest.length();
for (int sIndex = sStart; sIndex < sEnd; ++sIndex) {
final char c = source.charAt(sIndex);
final int dIndex = dStart + (sIndex - sStart);
// Restrict characters to the base64 character set.
// Ensure adding this character does not push the length over the limit.
if (((dIndex + 1 < KeyEncoding.KEY_LENGTH_BASE64 && isAllowed(c)) ||
(dIndex + 1 == KeyEncoding.KEY_LENGTH_BASE64 && c == '=')) &&
dLength + (sIndex - sStart) < KeyEncoding.KEY_LENGTH_BASE64) {
++rIndex;
} else {
if (replacement == null)
replacement = new SpannableStringBuilder(source, sStart, sEnd);
replacement.delete(rIndex, rIndex + 1);
}
}
return replacement;
}
}
@@ -1,33 +0,0 @@
package com.wireguard.android.widget;
import android.support.annotation.NonNull;
import android.support.design.widget.BaseTransientBottomBar;
import android.support.design.widget.Snackbar;
import android.util.Log;
import android.view.View;
import android.view.accessibility.AccessibilityManager;
import com.wireguard.android.BootShutdownReceiver;
import java.lang.reflect.Field;
public class MonkeyedSnackbar {
private static final String TAG = "WireGuard/" + BootShutdownReceiver.class.getSimpleName();
public static Snackbar make(@NonNull final View view, @NonNull final CharSequence text,
@BaseTransientBottomBar.Duration final int duration) {
final Snackbar snackbar = Snackbar.make(view, text, duration);
try {
final Field accessibilityManager = Snackbar.class.getSuperclass().getDeclaredField("mAccessibilityManager");
accessibilityManager.setAccessible(true);
final Field isEnabled = AccessibilityManager.class.getDeclaredField("mIsEnabled");
isEnabled.setAccessible(true);
isEnabled.setBoolean(accessibilityManager.get(snackbar), false);
} catch (final Exception e) {
Log.e(TAG, "Unable to force-enable snackbar animations", e);
}
return snackbar;
}
}
@@ -1,53 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget;
import android.support.annotation.Nullable;
import android.text.InputFilter;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import com.wireguard.android.model.Tunnel;
/**
* InputFilter for entering WireGuard configuration names (Linux interface names).
*/
public class NameInputFilter implements InputFilter {
private static boolean isAllowed(final char c) {
return Character.isLetterOrDigit(c) || "_=+.-".indexOf(c) >= 0;
}
public static InputFilter newInstance() {
return new NameInputFilter();
}
@Override @Nullable
public CharSequence filter(final CharSequence source,
final int sStart, final int sEnd,
final Spanned dest,
final int dStart, final int dEnd) {
SpannableStringBuilder replacement = null;
int rIndex = 0;
final int dLength = dest.length();
for (int sIndex = sStart; sIndex < sEnd; ++sIndex) {
final char c = source.charAt(sIndex);
final int dIndex = dStart + (sIndex - sStart);
// Restrict characters to those valid in interfaces.
// Ensure adding this character does not push the length over the limit.
if ((dIndex < Tunnel.NAME_MAX_LENGTH && isAllowed(c)) &&
dLength + (sIndex - sStart) < Tunnel.NAME_MAX_LENGTH) {
++rIndex;
} else {
if (replacement == null)
replacement = new SpannableStringBuilder(source, sStart, sEnd);
replacement.delete(rIndex, rIndex + 1);
}
}
return replacement;
}
}
@@ -1,221 +0,0 @@
/*
* Copyright © 2018 The Android Open Source Project
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. 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 android.support.annotation.ColorInt;
import android.support.annotation.IntRange;
import android.support.annotation.Nullable;
import android.util.FloatProperty;
@TargetApi(Build.VERSION_CODES.N)
public class SlashDrawable extends Drawable {
private static final float CORNER_RADIUS = Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? 0f : 1f;
private static final long QS_ANIM_LENGTH = 350;
private final Path mPath = new Path();
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
// These values are derived in un-rotated (vertical) orientation
private static final float SLASH_WIDTH = 1.8384776f;
private static final float SLASH_HEIGHT = 28f;
private static final float CENTER_X = 10.65f;
private static final float CENTER_Y = 11.869239f;
private static final float SCALE = 24f;
// Bottom is derived during animation
private static final float LEFT = (CENTER_X - (SLASH_WIDTH / 2)) / SCALE;
private static final float TOP = (CENTER_Y - (SLASH_HEIGHT / 2)) / SCALE;
private static final float RIGHT = (CENTER_X + (SLASH_WIDTH / 2)) / SCALE;
// Draw the slash washington-monument style; rotate to no-u-turn style
private static final float DEFAULT_ROTATION = -45f;
private final Drawable mDrawable;
private final RectF mSlashRect = new RectF(0, 0, 0, 0);
private float mRotation;
private boolean mSlashed;
private boolean mAnimationEnabled = true;
public SlashDrawable(final Drawable d) {
mDrawable = d;
}
@Override
public int getIntrinsicHeight() {
return mDrawable.getIntrinsicHeight();
}
@Override
public int getIntrinsicWidth() {
return mDrawable.getIntrinsicWidth();
}
@Override
protected void onBoundsChange(final Rect bounds) {
super.onBoundsChange(bounds);
mDrawable.setBounds(bounds);
}
public void setRotation(final float rotation) {
if (mRotation == rotation)
return;
mRotation = rotation;
invalidateSelf();
}
public void setAnimationEnabled(final boolean enabled) {
mAnimationEnabled = enabled;
}
// Animate this value on change
private float mCurrentSlashLength;
private static final FloatProperty mSlashLengthProp = new FloatProperty<SlashDrawable>("slashLength") {
@Override
public void setValue(final SlashDrawable object, final float value) {
object.mCurrentSlashLength = value;
}
@Override
public Float get(final SlashDrawable object) {
return object.mCurrentSlashLength;
}
};
@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();
}
}
@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();
}
private float scale(final float frac, final int width) {
return frac * width;
}
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;
}
@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();
}
private void setDrawableTintList(@Nullable final ColorStateList tint) {
mDrawable.setTintList(tint);
}
@Override
public void setTintMode(final Mode tintMode) {
super.setTintMode(tintMode);
mDrawable.setTintMode(tintMode);
}
@Override
public void setAlpha(@IntRange(from = 0, to = 255) final int alpha) {
mDrawable.setAlpha(alpha);
mPaint.setAlpha(alpha);
}
@Override
public void setColorFilter(@Nullable final ColorFilter colorFilter) {
mDrawable.setColorFilter(colorFilter);
mPaint.setColorFilter(colorFilter);
}
@Override
public int getOpacity() {
return PixelFormat.OPAQUE;
}
}
@@ -1,58 +0,0 @@
/*
* Copyright © 2013 The Android Open Source Project
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget;
import android.content.Context;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.widget.Switch;
public class ToggleSwitch extends Switch {
private boolean isRestoringState;
@Nullable private OnBeforeCheckedChangeListener listener;
public ToggleSwitch(final Context context) {
this(context, null);
}
@SuppressWarnings({"SameParameterValue", "WeakerAccess"})
public ToggleSwitch(final Context context, @Nullable final AttributeSet attrs) {
super(context, attrs);
}
@Override
public void onRestoreInstanceState(final Parcelable state) {
isRestoringState = true;
super.onRestoreInstanceState(state);
isRestoringState = false;
}
@Override
public void setChecked(final boolean checked) {
if (checked == isChecked())
return;
if (isRestoringState || listener == null) {
super.setChecked(checked);
return;
}
setEnabled(false);
listener.onBeforeCheckedChanged(this, checked);
}
public void setCheckedInternal(final boolean checked) {
super.setChecked(checked);
setEnabled(true);
}
public void setOnBeforeCheckedChangeListener(final OnBeforeCheckedChangeListener listener) {
this.listener = listener;
}
public interface OnBeforeCheckedChangeListener {
void onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked);
}
}
@@ -1,67 +0,0 @@
/*
* Copyright © 2018 Harsh Shandilya <msfjarvis@gmail.com>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. 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 android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.Snackbar;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.util.AttributeSet;
import android.view.View;
public class FloatingActionButtonBehavior extends CoordinatorLayout.Behavior<FloatingActionsMenu> {
public FloatingActionButtonBehavior(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean layoutDependsOn(final CoordinatorLayout parent, final FloatingActionsMenu child,
final View dependency) {
return dependency instanceof Snackbar.SnackbarLayout;
}
private static final long ANIMATION_DURATION = 250;
private static final TimeInterpolator FAST_OUT_SLOW_IN_INTERPOLATOR = new FastOutSlowInInterpolator();
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 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,628 +0,0 @@
/*
* Copyright © 2014 Jerzy Chalupski
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget.fab;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Keep;
import android.support.annotation.Nullable;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.content.res.ResourcesCompat;
import android.support.v7.widget.AppCompatTextView;
import android.util.AttributeSet;
import android.view.ContextThemeWrapper;
import android.view.TouchDelegate;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.OvershootInterpolator;
import android.widget.TextView;
import com.wireguard.android.R;
public class FloatingActionsMenu extends ViewGroup {
public static final int EXPAND_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 LABELS_ON_LEFT_SIDE = 0;
public static final int LABELS_ON_RIGHT_SIDE = 1;
private static final int ANIMATION_DURATION = 300;
private static final float COLLAPSED_PLUS_ROTATION = 0f;
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 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);
@Nullable private FloatingActionButton mAddButton;
@Nullable private RotatingDrawable mRotatingDrawable;
private int mMaxButtonWidth;
private int mMaxButtonHeight;
private int mLabelsStyle;
private int mLabelsPosition;
private int mButtonsCount;
@Nullable private TouchDelegateGroup mTouchDelegateGroup;
@Nullable private OnFloatingActionsMenuUpdateListener mListener;
private final Rect touchArea = new Rect(0, 0, 0, 0);
private float scrollYTranslation;
private float behaviorYTranslation;
public FloatingActionsMenu(final Context context) {
this(context, null);
}
public FloatingActionsMenu(final Context context, @Nullable final AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public FloatingActionsMenu(final Context context, @Nullable final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
private 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 is not supported.");
}
createAddButton(context);
}
public float getScrollYTranslation() {
return scrollYTranslation;
}
public void setScrollYTranslation(final float scrollYTranslation) {
this.scrollYTranslation = scrollYTranslation;
setTranslationY(behaviorYTranslation + scrollYTranslation);
}
public float getBehaviorYTranslation() {
return behaviorYTranslation;
}
public void setBehaviorYTranslation(final float behaviorYTranslation) {
this.behaviorYTranslation = behaviorYTranslation;
setTranslationY(behaviorYTranslation + scrollYTranslation);
}
public void setOnFloatingActionsMenuUpdateListener(final OnFloatingActionsMenuUpdateListener listener) {
mListener = listener;
}
private boolean expandsHorizontally() {
return mExpandDirection == EXPAND_LEFT || mExpandDirection == EXPAND_RIGHT;
}
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++;
}
public void addButton(final LabeledFloatingActionButton button) {
addView(button, mButtonsCount - 1);
mButtonsCount++;
if (mLabelsStyle != 0) {
createLabels();
}
}
public void removeButton(final LabeledFloatingActionButton button) {
removeView(button.getLabelView());
removeView(button);
button.setTag(R.id.fab_label, null);
mButtonsCount--;
}
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
int width = 0;
int height = 0;
mMaxButtonWidth = 0;
mMaxButtonHeight = 0;
int maxLabelWidth = 0;
for (int i = 0; i < mButtonsCount; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
switch (mExpandDirection) {
case EXPAND_UP:
case EXPAND_DOWN:
mMaxButtonWidth = Math.max(mMaxButtonWidth, child.getMeasuredWidth());
height += child.getMeasuredHeight();
break;
case EXPAND_LEFT:
case EXPAND_RIGHT:
width += child.getMeasuredWidth();
mMaxButtonHeight = Math.max(mMaxButtonHeight, child.getMeasuredHeight());
break;
}
if (!expandsHorizontally()) {
final TextView label = (TextView) child.getTag(R.id.fab_label);
if (label != null) {
maxLabelWidth = Math.max(maxLabelWidth, label.getMeasuredWidth());
}
}
}
if (expandsHorizontally()) {
height = mMaxButtonHeight;
} else {
width = mMaxButtonWidth + (maxLabelWidth > 0 ? maxLabelWidth + mLabelsMargin : 0);
}
switch (mExpandDirection) {
case EXPAND_UP:
case EXPAND_DOWN:
height += mButtonSpacing * (mButtonsCount - 1);
height = adjustForOvershoot(height);
break;
case EXPAND_LEFT:
case EXPAND_RIGHT:
width += mButtonSpacing * (mButtonsCount - 1);
width = adjustForOvershoot(width);
break;
}
setMeasuredDimension(width, height);
}
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(new Rect(touchArea), child));
label.setTranslationY(mExpanded ? expandedTranslation : collapsedTranslation);
label.setAlpha(mExpanded ? 1f : 0f);
final LayoutParams labelParams = (LayoutParams) label.getLayoutParams();
labelParams.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation);
labelParams.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation);
labelParams.setAnimationsTarget(label);
}
nextY = expandUp ?
childY - mButtonSpacing :
childY + child.getMeasuredHeight() + mButtonSpacing;
}
break;
case EXPAND_LEFT:
case EXPAND_RIGHT:
final boolean expandLeft = mExpandDirection == EXPAND_LEFT;
final int addButtonX = expandLeft ? r - l - mAddButton.getMeasuredWidth() : 0;
// Ensure mAddButton is centered on the line where the buttons should be
final int addButtonTop = b - t - mMaxButtonHeight + (mMaxButtonHeight - mAddButton.getMeasuredHeight()) / 2;
mAddButton.layout(addButtonX, addButtonTop, addButtonX + mAddButton.getMeasuredWidth(), addButtonTop + mAddButton.getMeasuredHeight());
int nextX = expandLeft ?
addButtonX - mButtonSpacing :
addButtonX + mAddButton.getMeasuredWidth() + mButtonSpacing;
for (int i = mButtonsCount - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child == mAddButton || child.getVisibility() == GONE) continue;
final int childX = expandLeft ? nextX - child.getMeasuredWidth() : nextX;
final int childY = addButtonTop + (mAddButton.getMeasuredHeight() - child.getMeasuredHeight()) / 2;
child.layout(childX, childY, childX + child.getMeasuredWidth(), childY + child.getMeasuredHeight());
final float collapsedTranslation = addButtonX - childX;
final float expandedTranslation = 0f;
child.setTranslationX(mExpanded ? expandedTranslation : collapsedTranslation);
child.setAlpha(mExpanded ? 1f : 0f);
final LayoutParams params = (LayoutParams) child.getLayoutParams();
params.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation);
params.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation);
params.setAnimationsTarget(child);
nextX = expandLeft ?
childX - mButtonSpacing :
childX + child.getMeasuredWidth() + mButtonSpacing;
}
break;
}
}
@Override
protected 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);
if (button instanceof LabeledFloatingActionButton) {
final String title = ((LabeledFloatingActionButton) button).getTitle();
final AppCompatTextView label = new AppCompatTextView(context);
label.setTextAppearance(context, mLabelsStyle);
label.setText(title);
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) {
final SavedState savedState = (SavedState) state;
mExpanded = savedState.mExpanded;
mTouchDelegateGroup.setEnabled(mExpanded);
if (mRotatingDrawable != null) {
mRotatingDrawable.setRotation(mExpanded ? EXPANDED_PLUS_ROTATION : COLLAPSED_PLUS_ROTATION);
}
super.onRestoreInstanceState(savedState.getSuperState());
} else {
super.onRestoreInstanceState(state);
}
}
public interface OnFloatingActionsMenuUpdateListener {
void onMenuExpanded();
void onMenuCollapsed();
}
private static class RotatingDrawable extends LayerDrawable {
private float mRotation;
RotatingDrawable(final Drawable drawable) {
super(new Drawable[]{drawable});
}
@SuppressWarnings("UnusedDeclaration")
public float getRotation() {
return mRotation;
}
@Keep
@SuppressWarnings("UnusedDeclaration")
public void setRotation(final float rotation) {
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 {
public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
@Override
public SavedState createFromParcel(final Parcel in) {
return new SavedState(in);
}
@Override
public SavedState[] newArray(final int size) {
return new SavedState[size];
}
};
public boolean mExpanded;
public SavedState(final Parcelable parcel) {
super(parcel);
}
private SavedState(final Parcel in) {
super(in);
mExpanded = in.readInt() == 1;
}
@Override
public void writeToParcel(final Parcel out, final int flags) {
super.writeToParcel(out, flags);
out.writeInt(mExpanded ? 1 : 0);
}
}
private class LayoutParams extends ViewGroup.LayoutParams {
private final ObjectAnimator mExpandDir = new ObjectAnimator();
private final ObjectAnimator mExpandAlpha = new ObjectAnimator();
private final ObjectAnimator mCollapseDir = new ObjectAnimator();
private final ObjectAnimator mCollapseAlpha = new ObjectAnimator();
private boolean animationsSetToPlay;
LayoutParams(final ViewGroup.LayoutParams source) {
super(source);
mExpandDir.setInterpolator(EXPAND_INTERPOLATOR);
mExpandAlpha.setInterpolator(ALPHA_EXPAND_INTERPOLATOR);
mCollapseDir.setInterpolator(COLLAPSE_INTERPOLATOR);
mCollapseAlpha.setInterpolator(COLLAPSE_INTERPOLATOR);
mCollapseAlpha.setProperty(View.ALPHA);
mCollapseAlpha.setFloatValues(1f, 0f);
mExpandAlpha.setProperty(View.ALPHA);
mExpandAlpha.setFloatValues(0f, 1f);
switch (mExpandDirection) {
case EXPAND_UP:
case EXPAND_DOWN:
mCollapseDir.setProperty(View.TRANSLATION_Y);
mExpandDir.setProperty(View.TRANSLATION_Y);
break;
case EXPAND_LEFT:
case EXPAND_RIGHT:
mCollapseDir.setProperty(View.TRANSLATION_X);
mExpandDir.setProperty(View.TRANSLATION_X);
break;
}
}
public void setAnimationsTarget(final View view) {
mCollapseAlpha.setTarget(view);
mCollapseDir.setTarget(view);
mExpandAlpha.setTarget(view);
mExpandDir.setTarget(view);
// Now that the animations have targets, set them to be played
if (!animationsSetToPlay) {
addLayerTypeListener(mExpandDir, view);
addLayerTypeListener(mCollapseDir, view);
mCollapseAnimation.play(mCollapseAlpha);
mCollapseAnimation.play(mCollapseDir);
mExpandAnimation.play(mExpandAlpha);
mExpandAnimation.play(mExpandDir);
animationsSetToPlay = true;
}
}
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);
}
});
}
}
}
@@ -1,27 +0,0 @@
/*
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget.fab;
import android.support.v7.widget.RecyclerView;
public class FloatingActionsMenuRecyclerViewScrollListener extends RecyclerView.OnScrollListener {
private static final float SCALE_FACTOR = 1.5f;
private final FloatingActionsMenu menu;
public FloatingActionsMenuRecyclerViewScrollListener(final FloatingActionsMenu menu) {
this.menu = menu;
}
private static float bound(final float min, final float proposal, final float max) {
return Math.min(max, Math.max(min, proposal));
}
@Override
public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) {
super.onScrolled(recyclerView, dx, dy);
menu.setScrollYTranslation(bound(0, menu.getScrollYTranslation() + dy * SCALE_FACTOR, menu.getMeasuredHeight() - menu.getTranslationY()));
}
}
@@ -1,58 +0,0 @@
/*
* Copyright © 2014 Jerzy Chalupski
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. 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 android.support.annotation.Nullable;
import android.support.design.widget.FloatingActionButton;
import android.util.AttributeSet;
import android.widget.TextView;
import com.wireguard.android.R;
public class LabeledFloatingActionButton extends FloatingActionButton {
@Nullable private final String title;
public LabeledFloatingActionButton(final Context context) {
this(context, null);
}
public LabeledFloatingActionButton(final Context context, @Nullable final AttributeSet attrs) {
this(context, attrs, 0);
}
public LabeledFloatingActionButton(final Context context, @Nullable final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
final TypedArray attr = context.obtainStyledAttributes(attrs, R.styleable.LabeledFloatingActionButton, 0, 0);
title = attr.getString(R.styleable.LabeledFloatingActionButton_fab_title);
attr.recycle();
}
@Nullable
TextView getLabelView() {
return (TextView) getTag(R.id.fab_label);
}
@Nullable
public String getTitle() {
return title;
}
@Override
public void setVisibility(final int visibility) {
final TextView label = getLabelView();
if (label != null) {
label.setVisibility(visibility);
}
super.setVisibility(visibility);
}
}
@@ -1,78 +0,0 @@
/*
* Copyright © 2014 Jerzy Chalupski
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget.fab;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.view.MotionEvent;
import android.view.TouchDelegate;
import android.view.View;
import java.util.ArrayList;
import java.util.Collection;
public class TouchDelegateGroup extends TouchDelegate {
private static final Rect USELESS_HACKY_RECT = new Rect();
private final Collection<TouchDelegate> mTouchDelegates = new ArrayList<>();
@Nullable private TouchDelegate mCurrentTouchDelegate;
private boolean mEnabled;
public TouchDelegateGroup(final View uselessHackyView) {
super(USELESS_HACKY_RECT, uselessHackyView);
}
public void addTouchDelegate(final TouchDelegate touchDelegate) {
mTouchDelegates.add(touchDelegate);
}
public void removeTouchDelegate(final TouchDelegate touchDelegate) {
mTouchDelegates.remove(touchDelegate);
if (mCurrentTouchDelegate == touchDelegate) {
mCurrentTouchDelegate = null;
}
}
public void clearTouchDelegates() {
mTouchDelegates.clear();
mCurrentTouchDelegate = null;
}
@Override
public boolean onTouchEvent(final MotionEvent event) {
if (!mEnabled)
return false;
TouchDelegate delegate = null;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
for (final TouchDelegate touchDelegate : mTouchDelegates) {
if (touchDelegate.onTouchEvent(event)) {
mCurrentTouchDelegate = touchDelegate;
return true;
}
}
break;
case MotionEvent.ACTION_MOVE:
delegate = mCurrentTouchDelegate;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
delegate = mCurrentTouchDelegate;
mCurrentTouchDelegate = null;
break;
}
return delegate != null && delegate.onTouchEvent(event);
}
public void setEnabled(final boolean enabled) {
mEnabled = enabled;
}
}
@@ -1,92 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.config;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import java.util.HashMap;
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.
*/
public enum Attribute {
ADDRESS("Address"),
ALLOWED_IPS("AllowedIPs"),
DNS("DNS"),
EXCLUDED_APPLICATIONS("ExcludedApplications"),
ENDPOINT("Endpoint"),
LISTEN_PORT("ListenPort"),
MTU("MTU"),
PERSISTENT_KEEPALIVE("PersistentKeepalive"),
PRESHARED_KEY("PresharedKey"),
PRIVATE_KEY("PrivateKey"),
PUBLIC_KEY("PublicKey");
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|=");
static {
KEY_MAP = new HashMap<>(Attribute.values().length);
for (final Attribute key : Attribute.values()) {
KEY_MAP.put(key.token.toLowerCase(), key);
}
}
private final Pattern pattern;
private final String token;
Attribute(final String token) {
pattern = Pattern.compile(token + "\\s*=\\s*(\\S.*)");
this.token = token;
}
public static <T> String iterableToString(final Iterable<T> iterable) {
return TextUtils.join(", ", iterable);
}
@Nullable
public static Attribute match(final CharSequence line) {
return KEY_MAP.get(SEPARATOR_PATTERN.split(line)[0].toLowerCase());
}
public static String[] stringToList(@Nullable final String string) {
if (TextUtils.isEmpty(string))
return EMPTY_LIST;
return LIST_SEPARATOR_PATTERN.split(string.trim());
}
public String composeWith(@Nullable final Object value) {
return String.format("%s = %s%n", token, value);
}
public String composeWith(final int value) {
return String.format("%s = %d%n", token, value);
}
public <T> String composeWith(final Iterable<T> value) {
return String.format("%s = %s%n", token, iterableToString(value));
}
@Nullable
public String parse(final CharSequence line) {
final Matcher matcher = pattern.matcher(line);
return matcher.matches() ? matcher.group(1) : null;
}
@Nullable
public String[] parseList(final CharSequence line) {
final Matcher matcher = pattern.matcher(line);
return matcher.matches() ? stringToList(matcher.group(1)) : null;
}
}
@@ -1,171 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. 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 android.support.annotation.Nullable;
import com.android.databinding.library.baseAdapters.BR;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* Represents a wg-quick configuration file, its name, and its connection state.
*/
public class Config {
private final Interface interfaceSection = new Interface();
private List<Peer> peers = new ArrayList<>();
public static Config from(final String string) throws IOException {
return from(new BufferedReader(new StringReader(string)));
}
public static Config from(final InputStream stream) throws IOException {
return from(new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)));
}
public static Config from(final BufferedReader reader) throws IOException {
final Config config = new Config();
Peer currentPeer = null;
String line;
boolean inInterfaceSection = false;
while ((line = reader.readLine()) != null) {
final int commentIndex = line.indexOf('#');
if (commentIndex != -1)
line = line.substring(0, commentIndex);
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;
} else if (inInterfaceSection) {
config.interfaceSection.parse(line);
} else if (currentPeer != null) {
currentPeer.parse(line);
} else {
throw new IllegalArgumentException("Invalid configuration line: " + line);
}
}
if (!inInterfaceSection && currentPeer == null) {
throw new IllegalArgumentException("Could not find any config information");
}
return config;
}
public Interface getInterface() {
return interfaceSection;
}
public List<Peer> getPeers() {
return peers;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder().append(interfaceSection);
for (final Peer peer : peers)
sb.append('\n').append(peer);
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);
}
@Override
public Observable[] newArray(final int size) {
return new Observable[size];
}
};
@Nullable private String name;
private final Interface.Observable observableInterface;
private final ObservableList<Peer.Observable> observablePeers;
public Observable(@Nullable final Config parent, @Nullable final String name) {
this.name = name;
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));
}
}
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 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();
}
@Override
public int describeContents() {
return 0;
}
@Bindable
public Interface.Observable getInterfaceSection() {
return observableInterface;
}
@Bindable
public String getName() {
return name == null ? "" : name;
}
@Bindable
public ObservableList<Peer.Observable> getPeers() {
return observablePeers;
}
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);
}
}
}
@@ -1,40 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.config;
import android.support.annotation.Nullable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetAddress;
public final class InetAddresses {
private static final Method PARSER_METHOD;
static {
try {
// This method is only present on Android.
PARSER_METHOD = InetAddress.class.getMethod("parseNumericAddress", String.class);
} catch (final NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
private InetAddresses() {
// Prevent instantiation.
}
public static InetAddress parse(@Nullable final String address) {
if (address == null || address.isEmpty())
throw new IllegalArgumentException("Empty address");
try {
return (InetAddress) PARSER_METHOD.invoke(null, address);
} catch (final IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e.getCause() == null ? e : e.getCause());
}
}
}
@@ -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: Apache-2.0
*/
package com.wireguard.config;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.util.Objects;
public class InetNetwork {
private final InetAddress address;
private final int mask;
public InetNetwork(final String input) {
final int slash = input.lastIndexOf('/');
final int rawMask;
final String rawAddress;
if (slash >= 0) {
rawMask = Integer.parseInt(input.substring(slash + 1), 10);
rawAddress = input.substring(0, slash);
} else {
rawMask = -1;
rawAddress = input;
}
address = InetAddresses.parse(rawAddress);
final int maxMask = (address instanceof Inet4Address) ? 32 : 128;
mask = rawMask >= 0 && rawMask <= maxMask ? rawMask : maxMask;
}
@Override
public boolean equals(final Object obj) {
if (!(obj instanceof InetNetwork))
return false;
final InetNetwork other = (InetNetwork) obj;
return Objects.equals(address, other.address) && mask == other.mask;
}
public InetAddress getAddress() {
return address;
}
public int getMask() {
return mask;
}
@Override
public int hashCode() {
return address.hashCode() ^ mask;
}
@Override
public String toString() {
return address.getHostAddress() + '/' + mask;
}
}
@@ -1,396 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. 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 android.support.annotation.Nullable;
import com.wireguard.android.BR;
import com.wireguard.crypto.Keypair;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Represents the configuration for a WireGuard interface (an [Interface] block).
*/
public class Interface {
private final List<InetNetwork> addressList;
private final List<InetAddress> dnsList;
private final List<String> excludedApplications;
@Nullable private Keypair keypair;
private int listenPort;
private int mtu;
public Interface() {
addressList = new ArrayList<>();
dnsList = new ArrayList<>();
excludedApplications = new ArrayList<>();
}
private void addAddresses(@Nullable 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));
}
}
}
private void addDnses(@Nullable final String[] dnses) {
if (dnses != null && dnses.length > 0) {
for (final String dns : dnses) {
dnsList.add(InetAddresses.parse(dns));
}
}
}
private void addExcludedApplications(@Nullable final String[] applications) {
if (applications != null && applications.length > 0) {
excludedApplications.addAll(Arrays.asList(applications));
}
}
@Nullable
private String getAddressString() {
if (addressList.isEmpty())
return null;
return Attribute.iterableToString(addressList);
}
public InetNetwork[] getAddresses() {
return addressList.toArray(new InetNetwork[addressList.size()]);
}
@Nullable
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()]);
}
@Nullable
private String getExcludedApplicationsString() {
if (excludedApplications.isEmpty())
return null;
return Attribute.iterableToString(excludedApplications);
}
public String[] getExcludedApplications() {
return excludedApplications.toArray(new String[excludedApplications.size()]);
}
public int getListenPort() {
return listenPort;
}
@Nullable
private String getListenPortString() {
if (listenPort == 0)
return null;
return Integer.valueOf(listenPort).toString();
}
public int getMtu() {
return mtu;
}
@Nullable
private String getMtuString() {
if (mtu == 0)
return null;
return Integer.toString(mtu);
}
@Nullable
public String getPrivateKey() {
if (keypair == null)
return null;
return keypair.getPrivateKey();
}
@Nullable
public String getPublicKey() {
if (keypair == null)
return null;
return keypair.getPublicKey();
}
public void parse(final String line) {
final Attribute key = Attribute.match(line);
if (key == null)
throw new IllegalArgumentException(String.format("Unable to parse line: \"%s\"", line));
switch (key) {
case ADDRESS:
addAddresses(key.parseList(line));
break;
case DNS:
addDnses(key.parseList(line));
break;
case EXCLUDED_APPLICATIONS:
addExcludedApplications(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(@Nullable final String addressString) {
addressList.clear();
addAddresses(Attribute.stringToList(addressString));
}
private void setDnsString(@Nullable final String dnsString) {
dnsList.clear();
addDnses(Attribute.stringToList(dnsString));
}
private void setExcludedApplicationsString(@Nullable final String applicationsString) {
excludedApplications.clear();
addExcludedApplications(Attribute.stringToList(applicationsString));
}
private void setListenPort(final int listenPort) {
this.listenPort = listenPort;
}
private void setListenPortString(@Nullable 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(@Nullable final String mtu) {
if (mtu != null && !mtu.isEmpty())
setMtu(Integer.parseInt(mtu, 10));
else
setMtu(0);
}
private void setPrivateKey(@Nullable String privateKey) {
if (privateKey != null && privateKey.isEmpty())
privateKey = null;
keypair = privateKey == null ? null : new Keypair(privateKey);
}
@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 (!excludedApplications.isEmpty())
sb.append(Attribute.EXCLUDED_APPLICATIONS.composeWith(excludedApplications));
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()));
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);
}
@Override
public Observable[] newArray(final int size) {
return new Observable[size];
}
};
@Nullable private String addresses;
@Nullable private String dnses;
@Nullable private String excludedApplications;
@Nullable private String listenPort;
@Nullable private String mtu;
@Nullable private String privateKey;
@Nullable private String publicKey;
public Observable(@Nullable final Interface parent) {
if (parent != null)
loadData(parent);
}
private Observable(final Parcel in) {
addresses = in.readString();
dnses = in.readString();
publicKey = in.readString();
privateKey = in.readString();
listenPort = in.readString();
mtu = in.readString();
excludedApplications = in.readString();
}
public void commitData(final Interface parent) {
parent.setAddressString(addresses);
parent.setDnsString(dnses);
parent.setExcludedApplicationsString(excludedApplications);
parent.setPrivateKey(privateKey);
parent.setListenPortString(listenPort);
parent.setMtuString(mtu);
loadData(parent);
notifyChange();
}
@Override
public int describeContents() {
return 0;
}
public void generateKeypair() {
final Keypair keypair = new Keypair();
privateKey = keypair.getPrivateKey();
publicKey = keypair.getPublicKey();
notifyPropertyChanged(BR.privateKey);
notifyPropertyChanged(BR.publicKey);
}
@Nullable
@Bindable
public String getAddresses() {
return addresses;
}
@Nullable
@Bindable
public String getDnses() {
return dnses;
}
@Nullable
@Bindable
public String getExcludedApplications() {
return excludedApplications;
}
@Bindable
public int getExcludedApplicationsCount() {
return Attribute.stringToList(excludedApplications).length;
}
@Nullable
@Bindable
public String getListenPort() {
return listenPort;
}
@Nullable
@Bindable
public String getMtu() {
return mtu;
}
@Nullable
@Bindable
public String getPrivateKey() {
return privateKey;
}
@Nullable
@Bindable
public String getPublicKey() {
return publicKey;
}
private void loadData(final Interface parent) {
addresses = parent.getAddressString();
dnses = parent.getDnsString();
excludedApplications = parent.getExcludedApplicationsString();
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 setExcludedApplications(final String excludedApplications) {
this.excludedApplications = excludedApplications;
notifyPropertyChanged(BR.excludedApplications);
notifyPropertyChanged(BR.excludedApplicationsCount);
}
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(privateKey).getPublicKey();
} catch (final IllegalArgumentException ignored) {
publicKey = "";
}
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);
dest.writeString(excludedApplications);
}
}
}
@@ -1,384 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. 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 android.support.annotation.Nullable;
import com.android.databinding.library.baseAdapters.BR;
import com.wireguard.crypto.KeyEncoding;
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.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java9.lang.Iterables;
/**
* Represents the configuration for a WireGuard peer (a [Peer] block).
*/
public class Peer {
private final List<InetNetwork> allowedIPsList;
@Nullable private InetSocketAddress endpoint;
private int persistentKeepalive;
@Nullable private String preSharedKey;
@Nullable private String publicKey;
public Peer() {
allowedIPsList = new ArrayList<>();
}
private void addAllowedIPs(@Nullable final String[] allowedIPs) {
if (allowedIPs != null && allowedIPs.length > 0) {
for (final String allowedIP : allowedIPs) {
allowedIPsList.add(new InetNetwork(allowedIP));
}
}
}
public InetNetwork[] getAllowedIPs() {
return allowedIPsList.toArray(new InetNetwork[allowedIPsList.size()]);
}
@Nullable
private String getAllowedIPsString() {
if (allowedIPsList.isEmpty())
return null;
return Attribute.iterableToString(allowedIPsList);
}
@Nullable
public InetSocketAddress getEndpoint() {
return endpoint;
}
@Nullable
private String getEndpointString() {
if (endpoint == null)
return null;
return String.format("%s:%d", endpoint.getHostString(), endpoint.getPort());
}
public int getPersistentKeepalive() {
return persistentKeepalive;
}
@Nullable
private String getPersistentKeepaliveString() {
if (persistentKeepalive == 0)
return null;
return Integer.valueOf(persistentKeepalive).toString();
}
@Nullable
public String getPreSharedKey() {
return preSharedKey;
}
@Nullable
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("[%s]:%d",
endpoint.getAddress().getHostAddress(),
endpoint.getPort());
return String.format("%s:%d",
endpoint.getAddress().getHostAddress(),
endpoint.getPort());
}
public void parse(final String line) {
final Attribute key = Attribute.match(line);
if (key == null)
throw new IllegalArgumentException(String.format("Unable to parse line: \"%s\"", 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(@Nullable final String allowedIPsString) {
allowedIPsList.clear();
addAllowedIPs(Attribute.stringToList(allowedIPsString));
}
private void setEndpoint(@Nullable final InetSocketAddress endpoint) {
this.endpoint = endpoint;
}
private void setEndpointString(@Nullable 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(@Nullable final String persistentKeepalive) {
if (persistentKeepalive != null && !persistentKeepalive.isEmpty())
setPersistentKeepalive(Integer.parseInt(persistentKeepalive, 10));
else
setPersistentKeepalive(0);
}
private void setPreSharedKey(@Nullable String preSharedKey) {
if (preSharedKey != null && preSharedKey.isEmpty())
preSharedKey = null;
if (preSharedKey != null)
KeyEncoding.keyFromBase64(preSharedKey);
this.preSharedKey = preSharedKey;
}
private void setPublicKey(@Nullable String publicKey) {
if (publicKey != null && publicKey.isEmpty())
publicKey = null;
if (publicKey != null)
KeyEncoding.keyFromBase64(publicKey);
this.publicKey = publicKey;
}
@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));
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);
}
@Override
public Observable[] newArray(final int size) {
return new Observable[size];
}
};
@Nullable private String allowedIPs;
@Nullable private String endpoint;
@Nullable private String persistentKeepalive;
@Nullable private String preSharedKey;
@Nullable private String publicKey;
private final List<String> interfaceDNSRoutes = new ArrayList<>();
private int numSiblings;
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();
numSiblings = in.readInt();
in.readStringList(interfaceDNSRoutes);
}
public static Observable newInstance() {
return new Observable(new Peer());
}
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();
}
@Override
public int describeContents() {
return 0;
}
private static final String DEFAULT_ROUTE_V4 = "0.0.0.0/0";
private static final List<String> DEFAULT_ROUTE_MOD_RFC1918_V4 = Arrays.asList("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");
public void toggleExcludePrivateIPs() {
final Collection<String> ips = new HashSet<>(Arrays.asList(Attribute.stringToList(allowedIPs)));
final boolean hasDefaultRoute = ips.contains(DEFAULT_ROUTE_V4);
final boolean hasDefaultRouteModRFC1918 = ips.containsAll(DEFAULT_ROUTE_MOD_RFC1918_V4);
if ((!hasDefaultRoute && !hasDefaultRouteModRFC1918) || numSiblings > 0)
return;
Iterables.removeIf(ips, ip -> !ip.contains(":"));
if (hasDefaultRoute) {
ips.addAll(DEFAULT_ROUTE_MOD_RFC1918_V4);
ips.addAll(interfaceDNSRoutes);
} else if (hasDefaultRouteModRFC1918)
ips.add(DEFAULT_ROUTE_V4);
setAllowedIPs(Attribute.iterableToString(ips));
}
@Bindable
public boolean getCanToggleExcludePrivateIPs() {
final Collection<String> ips = Arrays.asList(Attribute.stringToList(allowedIPs));
return numSiblings == 0 && (ips.contains(DEFAULT_ROUTE_V4) || ips.containsAll(DEFAULT_ROUTE_MOD_RFC1918_V4));
}
@Bindable
public boolean getIsExcludePrivateIPsOn() {
return numSiblings == 0 && Arrays.asList(Attribute.stringToList(allowedIPs)).containsAll(DEFAULT_ROUTE_MOD_RFC1918_V4);
}
@Bindable @Nullable
public String getAllowedIPs() {
return allowedIPs;
}
@Bindable @Nullable
public String getEndpoint() {
return endpoint;
}
@Bindable @Nullable
public String getPersistentKeepalive() {
return persistentKeepalive;
}
@Bindable @Nullable
public String getPreSharedKey() {
return preSharedKey;
}
@Bindable @Nullable
public String getPublicKey() {
return publicKey;
}
private 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);
notifyPropertyChanged(BR.canToggleExcludePrivateIPs);
notifyPropertyChanged(BR.isExcludePrivateIPsOn);
}
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) {
this.publicKey = publicKey;
notifyPropertyChanged(BR.publicKey);
}
public void setInterfaceDNSRoutes(@Nullable final String dnsServers) {
final Collection<String> ips = new HashSet<>(Arrays.asList(Attribute.stringToList(allowedIPs)));
final boolean modifyAllowedIPs = ips.containsAll(DEFAULT_ROUTE_MOD_RFC1918_V4);
ips.removeAll(interfaceDNSRoutes);
interfaceDNSRoutes.clear();
for (final String dnsServer : Attribute.stringToList(dnsServers)) {
if (!dnsServer.contains(":"))
interfaceDNSRoutes.add(dnsServer + "/32");
}
ips.addAll(interfaceDNSRoutes);
if (modifyAllowedIPs)
setAllowedIPs(Attribute.iterableToString(ips));
}
public void setNumSiblings(final int num) {
numSiblings = num;
notifyPropertyChanged(BR.canToggleExcludePrivateIPs);
notifyPropertyChanged(BR.isExcludePrivateIPsOn);
}
@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);
dest.writeInt(numSiblings);
dest.writeStringList(interfaceDNSRoutes);
}
}
}
@@ -1,158 +0,0 @@
/*
* Copyright © 2015-2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
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);
}
}
@@ -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: Apache-2.0
*/
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,14 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.util;
/**
* Interface for objects that have a identifying key of the given type.
*/
public interface Keyed<K> {
K getKey();
}
@@ -1,32 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.util;
import android.support.annotation.Nullable;
import java.util.Collection;
import java.util.List;
/**
* A list containing elements that can be looked up by key. A {@code KeyedList} cannot contain
* {@code null} elements.
*/
public interface KeyedList<K, E extends Keyed<? extends K>> extends List<E> {
boolean containsAllKeys(Collection<K> keys);
boolean containsKey(K key);
@Nullable
E get(K key);
@Nullable
E getLast(K key);
int indexOfKey(K key);
int lastIndexOfKey(K key);
}
@@ -1,31 +0,0 @@
/*
* Copyright © 2018 Samuel Holland <samuel@sholland.org>
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.util;
import android.support.annotation.Nullable;
import java.util.Collection;
import java.util.Comparator;
import java.util.Set;
/**
* A keyed list where all elements are sorted by the comparator returned by {@code comparator()}
* applied to their keys.
*/
public interface SortedKeyedList<K, E extends Keyed<? extends K>> extends KeyedList<K, E> {
Comparator<? super K> comparator();
@Nullable
K firstKey();
Set<K> keySet();
@Nullable
K lastKey();
Collection<E> values();
}
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<padding
android:bottom="4dp"
android:left="8dp"
android:right="8dp"
android:top="4dp" />
<solid android:color="@color/fab_label_background_color" />
</shape>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="?android:attr/colorForeground"
android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6L6,2zM13,9L13,3.5L18.5,9L13,9z" />
</vector>
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- TODO(msf): somehow make an additional activated color entry here (or elsewhere?) that
is in use only in multiselect mode, and one that's in use only in ordinary
select mode. In otherwords, we don't want to use the same background color
for both multiselect and normal select (on two column machines).
-->
<item android:drawable="@color/list_item_activated" android:state_activated="true" /> <!-- TODO(msf): get color from theme with ?android/attr: -->
<item android:drawable="@android:color/transparent" /> <!-- TODO(msf): get color from theme with ?android/attr: -->
</selector>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/list_item_ripple"> <!-- TODO(msf): get color from theme with ?android/attr: -->
<item android:drawable="@drawable/list_item_background" />
<item android:id="@android:id/mask" android:drawable="@android:color/white" /> <!-- TODO(msf): get color from theme with ?android/attr: -->
</ripple>
@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/master_detail_wrapper"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:divider="?android:attr/dividerHorizontal"
android:orientation="horizontal"
android:showDividers="middle"
tools:context=".activity.MainActivity">
<fragment
android:name="com.wireguard.android.fragment.TunnelListFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
android:tag="LIST" />
<FrameLayout
android:id="@+id/detail_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3" />
</LinearLayout>
@@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<import type="com.wireguard.android.model.ApplicationData" />
<variable
name="fragment"
type="com.wireguard.android.fragment.AppListDialogFragment" />
<variable
name="appData"
type="com.wireguard.android.util.ObservableKeyedList&lt;String, ApplicationData&gt;" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="200dp" >
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="@{appData.isEmpty() ? View.VISIBLE : View.GONE}"/>
<android.support.v7.widget.RecyclerView
android:id="@+id/app_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:items="@{appData}"
app:layout="@{@layout/app_list_item}" />
</FrameLayout>
</layout>
@@ -1,138 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.wireguard.android.model.Tunnel.State" />
<import type="com.wireguard.android.util.ClipboardUtils" />
<variable
name="fragment"
type="com.wireguard.android.fragment.TunnelDetailFragment" />
<variable
name="tunnel"
type="com.wireguard.android.model.Tunnel" />
<variable
name="config"
type="com.wireguard.config.Config.Observable" />
</data>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/colorBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:background="?android:attr/colorBackground"
app:cardCornerRadius="4dp"
app:cardElevation="2dp"
app:contentPadding="8dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/interface_title"
style="?android:attr/textAppearanceMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:text="@string/interface_title" />
<com.wireguard.android.widget.ToggleSwitch
android:id="@+id/tunnel_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/interface_title"
android:layout_alignParentEnd="true"
app:checked="@{tunnel.state == State.UP}"
app:onBeforeCheckedChanged="@{fragment::setTunnelState}" />
<TextView
android:id="@+id/interface_name_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/interface_title"
android:layout_marginTop="8dp"
android:labelFor="@+id/interface_name_text"
android:text="@string/name" />
<TextView
android:id="@+id/interface_name_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/interface_name_label"
android:text="@{tunnel.name}" />
<TextView
android:id="@+id/public_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/interface_name_text"
android:layout_marginTop="8dp"
android:labelFor="@+id/public_key_text"
android:text="@string/public_key" />
<TextView
android:id="@+id/public_key_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/public_key_label"
android:contentDescription="@string/public_key_description"
android:ellipsize="end"
android:maxLines="1"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{config.interfaceSection.publicKey}" />
<TextView
android:id="@+id/addresses_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/public_key_text"
android:layout_marginTop="8dp"
android:labelFor="@+id/addresses_text"
android:text="@string/addresses" />
<TextView
android:id="@+id/addresses_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/addresses_label"
android:contentDescription="@string/addresses"
android:text="@{config.interfaceSection.addresses}" />
</RelativeLayout>
</android.support.v7.widget.CardView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:divider="@null"
android:orientation="vertical"
app:items="@{config.peers}"
app:layout="@{@layout/tunnel_detail_peer}"
tools:ignore="UselessLeaf" />
</LinearLayout>
</ScrollView>
</layout>
@@ -1,94 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="com.wireguard.android.util.ClipboardUtils" />
<variable
name="item"
type="com.wireguard.config.Peer.Observable" />
</data>
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:background="?android:attr/colorBackground"
app:cardCornerRadius="4dp"
app:cardElevation="2dp"
app:contentPadding="8dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/peer_title"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:text="@string/peer" />
<TextView
android:id="@+id/public_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/peer_title"
android:layout_marginTop="8dp"
android:labelFor="@+id/public_key_text"
android:text="@string/public_key" />
<TextView
android:id="@+id/public_key_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/public_key_label"
android:contentDescription="@string/public_key_description"
android:ellipsize="end"
android:maxLines="1"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{item.publicKey}" />
<TextView
android:id="@+id/allowed_ips_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/public_key_text"
android:layout_marginTop="8dp"
android:labelFor="@+id/allowed_ips_text"
android:text="@string/allowed_ips" />
<TextView
android:id="@+id/allowed_ips_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/allowed_ips_label"
android:text="@{item.allowedIPs}" />
<TextView
android:id="@+id/endpoint_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/allowed_ips_text"
android:layout_marginTop="8dp"
android:labelFor="@+id/endpoint_text"
android:text="@string/endpoint" />
<TextView
android:id="@+id/endpoint_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/endpoint_label"
android:text="@{item.endpoint}" />
</RelativeLayout>
</android.support.v7.widget.CardView>
</layout>
@@ -1,252 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.wireguard.android.util.ClipboardUtils" />
<import type="com.wireguard.android.widget.KeyInputFilter" />
<import type="com.wireguard.android.widget.NameInputFilter" />
<import type="com.wireguard.config.Peer" />
<variable
name="fragment"
type="com.wireguard.android.fragment.TunnelEditorFragment" />
<variable
name="config"
type="com.wireguard.config.Config.Observable" />
</data>
<android.support.design.widget.CoordinatorLayout
android:id="@+id/main_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/colorBackground">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:background="?android:attr/colorBackground"
app:cardCornerRadius="4dp"
app:cardElevation="2dp"
app:contentPadding="8dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/interface_title"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:text="@string/interface_title" />
<TextView
android:id="@+id/interface_name_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/interface_title"
android:layout_marginTop="8dp"
android:labelFor="@+id/interface_name_text"
android:text="@string/name" />
<EditText
android:id="@+id/interface_name_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/interface_name_label"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={config.name}"
app:filter="@{NameInputFilter.newInstance()}" />
<TextView
android:id="@+id/private_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/interface_name_text"
android:labelFor="@+id/private_key_text"
android:text="@string/private_key" />
<EditText
android:id="@+id/private_key_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/private_key_label"
android:layout_toStartOf="@+id/generate_private_key_button"
android:contentDescription="@string/public_key_description"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={config.interfaceSection.privateKey}"
app:filter="@{KeyInputFilter.newInstance()}" />
<Button
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:id="@+id/generate_private_key_button"
android:layout_width="96dp"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/private_key_text"
android:layout_alignParentEnd="true"
android:layout_below="@+id/private_key_label"
android:onClick="@{() -> config.interfaceSection.generateKeypair()}"
android:text="@string/generate" />
<TextView
android:id="@+id/public_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/private_key_text"
android:labelFor="@+id/public_key_text"
android:text="@string/public_key" />
<TextView
android:id="@+id/public_key_text"
style="?android:attr/editTextStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/public_key_label"
android:contentDescription="@string/public_key_description"
android:ellipsize="end"
android:focusable="false"
android:hint="@string/hint_generated"
android:maxLines="1"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{config.interfaceSection.publicKey}" />
<TextView
android:id="@+id/addresses_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/public_key_text"
android:layout_toStartOf="@+id/listen_port_label"
android:labelFor="@+id/addresses_text"
android:text="@string/addresses" />
<EditText
android:id="@+id/addresses_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/addresses_label"
android:layout_toStartOf="@+id/listen_port_text"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={config.interfaceSection.addresses}" />
<TextView
android:id="@+id/listen_port_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/addresses_label"
android:layout_alignParentEnd="true"
android:layout_alignStart="@+id/generate_private_key_button"
android:labelFor="@+id/listen_port_text"
android:text="@string/listen_port" />
<EditText
android:id="@+id/listen_port_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/addresses_text"
android:layout_alignParentEnd="true"
android:layout_alignStart="@+id/generate_private_key_button"
android:hint="@string/hint_random"
android:inputType="number"
android:text="@={config.interfaceSection.listenPort}"
android:textAlignment="center" />
<TextView
android:id="@+id/dns_servers_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/addresses_text"
android:layout_toStartOf="@+id/mtu_label"
android:labelFor="@+id/dns_servers_text"
android:text="@string/dns_servers" />
<EditText
android:id="@+id/dns_servers_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/dns_servers_label"
android:layout_toStartOf="@+id/mtu_text"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={config.interfaceSection.dnses}" />
<TextView
android:id="@+id/mtu_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/dns_servers_label"
android:layout_alignParentEnd="true"
android:layout_alignStart="@+id/generate_private_key_button"
android:labelFor="@+id/mtu_text"
android:text="@string/mtu" />
<EditText
android:id="@+id/mtu_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/dns_servers_text"
android:layout_alignParentEnd="true"
android:layout_alignStart="@+id/generate_private_key_button"
android:hint="@string/hint_automatic"
android:inputType="number"
android:text="@={config.interfaceSection.mtu}"
android:textAlignment="center" />
<Button
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:id="@+id/set_excluded_applications"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="-8dp"
android:layout_below="@+id/dns_servers_text"
android:onClick="@{fragment::onRequestSetExcludedApplications}"
android:text="@{@plurals/set_excluded_applications(config.interfaceSection.excludedApplicationsCount, config.interfaceSection.excludedApplicationsCount)}" />
</RelativeLayout>
</android.support.v7.widget.CardView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="@null"
android:orientation="vertical"
app:items="@{config.peers}"
app:layout="@{@layout/tunnel_editor_peer}"
tools:ignore="UselessLeaf" />
<Button
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginEnd="4dp"
android:layout_marginStart="4dp"
android:onClick="@{() -> config.peers.add(Peer.Observable.newInstance())}"
android:text="@string/add_peer" />
</LinearLayout>
</ScrollView>
</android.support.design.widget.CoordinatorLayout>
</layout>
@@ -1,161 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View"/>
<import type="com.wireguard.android.widget.KeyInputFilter" />
<variable
name="collection"
type="android.databinding.ObservableList&lt;com.wireguard.config.Peer.Observable&gt;" />
<variable
name="item"
type="com.wireguard.config.Peer.Observable" />
</data>
<android.support.v7.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:background="?android:attr/colorBackground"
app:cardCornerRadius="4dp"
app:cardElevation="2dp"
app:contentPadding="8dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/peer_title"
style="?android:attr/textAppearanceMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginBottom="8dp"
android:layout_toStartOf="@+id/peer_action_delete"
android:text="@string/peer" />
<ImageButton
android:id="@+id/peer_action_delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:background="@null"
android:contentDescription="@string/delete"
android:onClick="@{() -> collection.remove(item)}"
android:src="@drawable/ic_action_delete" />
<TextView
android:id="@+id/public_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/peer_title"
android:labelFor="@+id/public_key_text"
android:text="@string/public_key" />
<EditText
android:id="@+id/public_key_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/public_key_label"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={item.publicKey}"
app:filter="@{KeyInputFilter.newInstance()}" />
<TextView
android:id="@+id/pre_shared_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/public_key_text"
android:labelFor="@+id/pre_shared_key_text"
android:text="@string/pre_shared_key" />
<EditText
android:id="@+id/pre_shared_key_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/pre_shared_key_label"
android:hint="@string/hint_optional"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={item.preSharedKey}" />
<TextView
android:id="@+id/allowed_ips_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/pre_shared_key_text"
android:layout_toStartOf="@+id/exclude_private_ips"
android:labelFor="@+id/allowed_ips_text"
android:text="@string/allowed_ips" />
<CheckBox
android:id="@+id/exclude_private_ips"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/allowed_ips_label"
android:layout_alignParentEnd="true"
android:checked="@{item.isExcludePrivateIPsOn}"
android:onClick="@{() -> item.toggleExcludePrivateIPs()}"
android:text="@string/exclude_private_ips"
android:visibility="@{item.canToggleExcludePrivateIPs ? View.VISIBLE : View.GONE}" />
<EditText
android:id="@+id/allowed_ips_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/allowed_ips_label"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={item.allowedIPs}" />
<TextView
android:id="@+id/endpoint_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/allowed_ips_text"
android:layout_toStartOf="@+id/persistent_keepalive_label"
android:labelFor="@+id/endpoint_text"
android:text="@string/endpoint" />
<EditText
android:id="@+id/endpoint_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/endpoint_label"
android:layout_toStartOf="@+id/persistent_keepalive_text"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={item.endpoint}" />
<TextView
android:id="@+id/persistent_keepalive_label"
android:layout_width="96dp"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/endpoint_label"
android:layout_alignParentEnd="true"
android:labelFor="@+id/persistent_keepalive_text"
android:text="@string/persistent_keepalive" />
<EditText
android:id="@+id/persistent_keepalive_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/endpoint_text"
android:layout_alignParentEnd="true"
android:layout_alignStart="@+id/persistent_keepalive_label"
android:hint="@string/hint_optional"
android:inputType="number"
android:text="@={item.persistentKeepalive}"
android:textAlignment="center" />
</RelativeLayout>
</android.support.v7.widget.CardView>
</layout>
@@ -1,104 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="com.wireguard.android.model.Tunnel" />
<variable
name="fragment"
type="com.wireguard.android.fragment.TunnelListFragment" />
<variable
name="rowConfigurationHandler"
type="com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler" />
<variable
name="tunnels"
type="com.wireguard.android.util.ObservableKeyedList&lt;String, Tunnel&gt;" />
</data>
<android.support.design.widget.CoordinatorLayout
android:id="@+id/main_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/colorBackground"
android:clipChildren="false">
<android.support.v7.widget.RecyclerView
android:id="@+id/tunnel_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@{@dimen/design_fab_size_normal * 1.1f}"
android:clipToPadding="false"
android:choiceMode="multipleChoiceModal"
android:visibility="@{tunnels.size() > 0 ? android.view.View.VISIBLE : android.view.View.GONE}"
app:items="@{tunnels}"
app:layout="@{@layout/tunnel_list_item}"
app:configurationHandler="@{rowConfigurationHandler}" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="@{tunnels.size() == 0 ? android.view.View.VISIBLE : android.view.View.GONE}"
android:layout_gravity="center">
<android.support.v7.widget.AppCompatImageView
android:id="@+id/logo_placeholder"
android:layout_width="140dp"
android:layout_height="140dp"
android:layout_gravity="center"
android:tint="@color/transparent_background_placeholder"
android:layout_marginTop="-70dp"
android:layout_marginBottom="20dp"
android:src="@mipmap/ic_launcher" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textSize="20sp"
android:text="@string/tunnel_list_placeholder" />
</LinearLayout>
<com.wireguard.android.widget.fab.FloatingActionsMenu
android:id="@+id/create_menu"
android:clipChildren="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
app:fab_labelStyle="@style/fab_label"
app:fab_labelsPosition="@integer/label_position"
app:layout_behavior="com.wireguard.android.widget.fab.FloatingActionButtonBehavior" >
<com.wireguard.android.widget.fab.LabeledFloatingActionButton
android:id="@+id/create_from_file"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{fragment::onRequestImportConfig}"
app:srcCompat="@drawable/ic_action_open_white"
app:fabSize="mini"
app:fab_title="@string/create_from_file" />
<com.wireguard.android.widget.fab.LabeledFloatingActionButton
android:id="@+id/scan_qr_code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{fragment::onRequestScanQRCode}"
app:srcCompat="@drawable/ic_action_scan_qr_code_white"
app:fabSize="mini"
app:fab_title="@string/create_from_qrcode" />
<com.wireguard.android.widget.fab.LabeledFloatingActionButton
android:id="@+id/create_empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{fragment::onRequestCreateConfig}"
app:fabSize="mini"
app:srcCompat="@drawable/ic_action_edit_white"
app:fab_title="@string/create_empty" />
</com.wireguard.android.widget.fab.FloatingActionsMenu>
</android.support.design.widget.CoordinatorLayout>
</layout>
-4
View File
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="label_position">1</integer>
</resources>
-13
View File
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- TODO(msf): remove these 4 hard-coded colors and replace with theme colors -->
<color name="list_item_activated">#64cfd8dc</color>
<color name="list_item_ripple">#a0808e95</color>
<color name="fab_label_text_color">#000000</color>
<color name="fab_label_background_color">#bbbbbb</color>
<color name="accent">#5e97f6</color>
<!-- TODO(msf): replace with ?android:attr/colorBackground with some alpha -->
<color name="transparent_background_placeholder">#aa2e2e2e</color>
</resources>
-13
View File
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- TODO(msf): remove these 4 hard-coded colors and replace with theme colors -->
<color name="list_item_activated">#cfd8dc</color> <!-- Blue Grey 200 -->
<color name="list_item_ripple">#808e95</color> <!-- Blue Grey 200 dark -->
<color name="fab_label_text_color">#ffffff</color>
<color name="fab_label_background_color">#444444</color>
<color name="accent">#2196F3</color>
<!-- TODO(msf): replace with ?android:attr/colorBackground with some alpha -->
<color name="transparent_background_placeholder">#aafafafa</color>
</resources>
-4
View File
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="fab_margin">16dp</dimen>
</resources>
-31
View File
@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="fab_expand_menu_button" type="id"/>
<item name="fab_label" type="id"/>
<dimen name="fab_shadow_offset">3dp</dimen>
<dimen name="fab_shadow_radius">9dp</dimen>
<dimen name="fab_stroke_width">1dp</dimen>
<dimen name="fab_actions_spacing">24dp</dimen>
<dimen name="fab_labels_margin">8dp</dimen>
<declare-styleable name="LabeledFloatingActionButton">
<attr name="fab_title" format="string"/>
</declare-styleable>
<declare-styleable name="FloatingActionsMenu">
<attr name="fab_labelStyle" format="reference"/>
<attr name="fab_labelsPosition" format="enum">
<enum name="left" value="0"/>
<enum name="right" value="1"/>
</attr>
<attr name="fab_expandDirection" format="enum">
<enum name="up" value="0"/>
<enum name="down" value="1"/>
<enum name="left" value="2"/>
<enum name="right" value="3"/>
</attr>
</declare-styleable>
<integer name="label_position">0</integer>
</resources>
-108
View File
@@ -1,108 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="delete_error">
<item quantity="one">Unable to delete %d tunnel: %s</item>
<item quantity="other">Unable to delete %d tunnels: %s</item>
</plurals>
<plurals name="delete_success">
<item quantity="one">Successfully deleted %d tunnel</item>
<item quantity="other">Successfully deleted %d tunnels</item>
</plurals>
<plurals name="delete_title">
<item quantity="one">%d tunnel selected</item>
<item quantity="other">%d tunnels selected</item>
</plurals>
<plurals name="import_partial_success">
<item quantity="one">Imported %d of %d tunnels</item>
<item quantity="other">Imported %d of %d tunnels</item>
</plurals>
<plurals name="import_total_success">
<item quantity="one">Imported %d tunnel</item>
<item quantity="other">Imported %d tunnels</item>
</plurals>
<string name="add_peer">Add peer</string>
<string name="addresses">Addresses</string>
<string name="allowed_ips">Allowed IPs</string>
<string name="app_name">WireGuard</string>
<string name="cancel">Cancel</string>
<string name="config_save_error">Unable to save configuration for “%s”: %s</string>
<string name="config_save_success">Successfully saved configuration for “%s”</string>
<string name="create_activity_title">Create WireGuard Tunnel</string>
<string name="create_empty">Create from scratch</string>
<string name="create_from_file">Create from file or archive</string>
<string name="create_from_qrcode">Create from QR code</string>
<string name="create_tunnel">Create Tunnel</string>
<string name="dark_theme_title">Use dark theme</string>
<string name="dark_theme_summary_on">Currently using dark night theme</string>
<string name="dark_theme_summary_off">Currently using light day theme</string>
<string name="delete">Delete</string>
<string name="deselect_all">Deselect All</string>
<string name="select_all">Select all</string>
<string name="dns_servers">DNS servers</string>
<string name="edit">Edit</string>
<string name="endpoint">Endpoint</string>
<string name="error_fetching_apps">Error fetching apps list: %s</string>
<string name="error_down">Error bringing down tunnel: %s</string>
<string name="error_root">Please obtain root access and try again</string>
<string name="error_up">Error bringing up tunnel: %s</string>
<string name="excluded_applications">Excluded Applications</string>
<string name="exclude_private_ips">Exclude private IPs</string>
<string name="generate">Generate</string>
<string name="hint_automatic">(auto)</string>
<string name="hint_generated">(generated)</string>
<string name="hint_optional">(optional)</string>
<string name="hint_random">(random)</string>
<string name="import_from_qrcode">Import Tunnel from QR Code</string>
<string name="import_error">Unable to import tunnel: %s</string>
<string name="import_success">Imported “%s”</string>
<string name="interface_title">Interface</string>
<string name="listen_port">Listen port</string>
<string name="log_exporter_title">Export log file</string>
<string name="log_export_error">Unable to export log: %s</string>
<string name="log_export_success">Saved to %s</string>
<string name="log_export_summary">Log file will be saved to downloads folder</string>
<string name="mtu">MTU</string>
<string name="name">Name</string>
<string name="peer">Peer</string>
<string name="permission_label">control WireGuard tunnels</string>
<string name="permission_description">Allows an app to control WireGuard tunnels. Apps with this permission may enable and disable WireGuard tunnels at will, potentially misdirecting Internet traffic.</string>
<string name="persistent_keepalive">Persistent keepalive</string>
<string name="pre_shared_key">Pre-shared key</string>
<string name="private_key">Private key</string>
<string name="public_key">Public key</string>
<string name="public_key_description">Public key</string>
<string name="qrcode_hint">Tip: generate with `qrencode -t ansiutf8 &lt; tunnel.conf`.</string>
<string name="restore_on_boot_summary">Bring up previously-enabled tunnels on boot</string>
<string name="restore_on_boot_title">Restore on boot</string>
<string name="save">Save</string>
<plurals name="set_excluded_applications">
<item quantity="one">%d Excluded Application</item>
<item quantity="other">%d Excluded Applications</item>
</plurals>
<string name="set_exclusions">Set Exclusions</string>
<string name="settings">Settings</string>
<string name="toggle_error">Error toggling WireGuard tunnel: %s</string>
<string name="tools_installer_already">wg and wg-quick are already installed</string>
<string name="tools_installer_failure">Unable to install command-line tools (no root?)</string>
<string name="tools_installer_initial">Install optional tools for scripting</string>
<string name="tools_installer_working">Installing wg and wg-quick</string>
<string name="tools_installer_initial_system">Install optional tools for scripting into the system partition</string>
<string name="tools_installer_initial_magisk">Install optional tools for scripting as Magisk module</string>
<string name="tools_installer_success_system">wg and wg-quick installed into the system partition</string>
<string name="tools_installer_success_magisk">wg and wg-quick installed as a Magisk module (reboot required)</string>
<string name="tools_installer_title">Install command line tools</string>
<string name="tunnel_create_error">Unable to create tunnel: %s</string>
<string name="tunnel_create_success">Successfully created tunnel “%s”</string>
<string name="tunnel_list_placeholder">Add a tunnel using the blue button</string>
<string name="tunnel_name">Tunnel Name</string>
<string name="tunnel_rename_error">Unable to rename tunnel: %s</string>
<string name="tunnel_rename_success">Successfully renamed tunnel to “%s”</string>
<string name="version_title">WireGuard for Android v%s"</string>
<string name="version_summary">%s backend v%s</string>
<string name="version_summary_checking">Checking %s backend version</string>
<string name="version_summary_unknown">Unknown %s version</string>
<string name="zip_exporter_title">Export tunnels to zip file</string>
<string name="zip_export_error">Unable to export tunnels: %s</string>
<string name="zip_export_success">Saved to %s</string>
<string name="zip_export_summary">Zip file will be saved to downloads folder</string>
</resources>
-17
View File
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="AppTheme" parent="Theme.AppCompat.DayNight.DarkActionBar">
<item name="colorAccent">@color/accent</item>
</style>
<style name="SettingsTheme" parent="AppTheme">
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
</style>
<style name="fab_label" parent="TextAppearance.AppCompat.Inverse">
<item name="android:background">@drawable/fab_label_background</item>
<item name="android:textColor">@color/fab_label_text_color</item>
</style>
</resources>
-18
View File
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<com.wireguard.android.preference.VersionPreference android:icon="@mipmap/ic_launcher" />
<CheckBoxPreference
android:defaultValue="false"
android:key="restore_on_boot"
android:summary="@string/restore_on_boot_summary"
android:title="@string/restore_on_boot_title" />
<com.wireguard.android.preference.ToolsInstallerPreference android:key="tools_installer" />
<com.wireguard.android.preference.ZipExporterPreference />
<com.wireguard.android.preference.LogExporterPreference />
<CheckBoxPreference
android:defaultValue="false"
android:key="dark_theme"
android:summaryOn="@string/dark_theme_summary_on"
android:summaryOff="@string/dark_theme_summary_off"
android:title="@string/dark_theme_title" />
</PreferenceScreen>
-2
View File
@@ -1,2 +0,0 @@
src/
.gobuildversion
-58
View File
@@ -1,58 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
#
# Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
FILES := $(filter-out %/main.go,$(wildcard ../wireguard-go/*/*.go) $(wildcard ../wireguard-go/*.go))
NDK_GO_ARCH_MAP_x86 := 386
NDK_GO_ARCH_MAP_x86_64 := amd64
NDK_GO_ARCH_MAP_arm := arm
NDK_GO_ARCH_MAP_arm64 := arm64
NDK_GO_ARCH_MAP_mips := mipsx
NDK_GO_ARCH_MAP_mips64 := mips64x
CLANG_FLAGS := --target=$(ANDROID_LLVM_TRIPLE) --gcc-toolchain=$(ANDROID_TOOLCHAIN_ROOT) --sysroot=$(ANDROID_SYSROOT)
export CGO_CFLAGS := $(CLANG_FLAGS) $(CFLAGS)
export CGO_LDFLAGS := $(CLANG_FLAGS) $(LDFLAGS)
export CC := $(ANDROID_C_COMPILER)
export GOARCH := $(NDK_GO_ARCH_MAP_$(ANDROID_ARCH_NAME))
export GOOS := android
export CGO_ENABLED := 1
default: $(DESTDIR)/libwg-go.so
GOBUILDARCH := $(NDK_GO_ARCH_MAP_$(shell uname -m))
GOBUILDOS := $(shell uname -s | tr '[:upper:]' '[:lower:]')
GOBUILDVERSION := 1.10.2
GOBUILDTARBALL := https://dl.google.com/go/go$(GOBUILDVERSION).$(GOBUILDOS)-$(GOBUILDARCH).tar.gz
GOBUILDVERSION_NEEDED := go version go$(GOBUILDVERSION) $(GOBUILDOS)/$(GOBUILDARCH)
GOBUILDVERSION_CURRENT := $(shell go version 2>/dev/null)
ifeq ($(GOBUILDVERSION_NEEDED),$(GOBUILDVERSION_CURRENT))
$(info Using system Go)
else
export GOROOT := $(CURDIR)/goroot
export PATH := $(GOROOT)/bin:$(PATH)
GOBUILDVERSION_CURRENT := $(shell $(GOROOT)/bin/go version 2>/dev/null)
ifneq ($(GOBUILDVERSION_NEEDED),$(GOBUILDVERSION_CURRENT))
$(shell rm -f $(GOROOT)/bin/go)
endif
$(GOROOT)/bin/go:
rm -rf "$(GOROOT)"
mkdir -p "$(GOROOT)"
curl "$(GOBUILDTARBALL)" | tar -C "$(GOROOT)" --strip-components=1 -xzf - || { rm -rf "$(GOROOT)"; exit 1; }
patch -p1 -f -N -r- -d "$(GOROOT)" < goruntime-boottime-over-monotonic.diff || { rm -rf "$(GOROOT)"; exit 1; }
$(DESTDIR)/libwg-go.so: $(GOROOT)/bin/go
endif
$(shell test "$$(cat .gobuildversion 2>/dev/null)" = "$(GOBUILDVERSION_CURRENT)" || rm -f "$(DESTDIR)/libwg-go.so")
$(DESTDIR)/libwg-go.so: $(FILES) src/git.zx2c4.com/wireguard-go/api-android.go src/git.zx2c4.com/wireguard-go/tun/api-android.go src/git.zx2c4.com/wireguard-go/jni.c
@goroot="$$(go env GOROOT)" || exit 1; patch -p1 -f -R -s --dry-run -d "$$goroot" < goruntime-boottime-over-monotonic.diff >/dev/null 2>&1 || { echo "ERROR: Your system's Go is not patched to use the suspend-aware monotonic clock. Please try again after having run: \`patch -p1 -d \"$$goroot\" < \"$(CURDIR)/goruntime-boottime-over-monotonic.diff\"\`."; exit 1; }
find . -name '*.go' -type l -delete
find . -type d -empty -delete
mkdir -p $(subst ../wireguard-go/,./src/git.zx2c4.com/wireguard-go/,$(dir $(FILES)))
$(foreach FILE,$(FILES),ln -sfrt $(subst ../wireguard-go/,./src/git.zx2c4.com/wireguard-go/,$(dir $(FILE))) $(FILE);)
GOPATH=$(PWD) go get -v -d git.zx2c4.com/wireguard-go
GOPATH=$(PWD) go build -ldflags="-X main.socketDirectory=/data/data/$(ANDROID_PACKAGE_NAME)/cache/wireguard" -v -o $(DESTDIR)/libwg-go.so -buildmode c-shared git.zx2c4.com/wireguard-go
go version > .gobuildversion
@@ -1,103 +0,0 @@
diff -ru b/src/runtime/sys_linux_386.s a/src/runtime/sys_linux_386.s
--- b/src/runtime/sys_linux_386.s 2018-04-30 22:26:39.000000000 +0200
+++ a/src/runtime/sys_linux_386.s 2018-05-09 17:09:42.319531743 +0200
@@ -287,13 +287,13 @@
LEAL 8(SP), BX // &ts (struct timespec)
MOVL BX, 4(SP)
- MOVL $1, 0(SP) // CLOCK_MONOTONIC
+ MOVL $7, 0(SP) // CLOCK_BOOTTIME
CALL AX
JMP finish
fallback:
MOVL $SYS_clock_gettime, AX
- MOVL $1, BX // CLOCK_MONOTONIC
+ MOVL $7, BX // CLOCK_BOOTTIME
LEAL 8(SP), CX
INVOKE_SYSCALL
diff -ru b/src/runtime/sys_linux_amd64.s a/src/runtime/sys_linux_amd64.s
--- b/src/runtime/sys_linux_amd64.s 2018-04-30 22:26:39.000000000 +0200
+++ a/src/runtime/sys_linux_amd64.s 2018-05-09 17:09:42.319531743 +0200
@@ -258,7 +258,7 @@
MOVQ runtime·__vdso_clock_gettime_sym(SB), AX
CMPQ AX, $0
JEQ fallback
- MOVL $1, DI // CLOCK_MONOTONIC
+ MOVL $7, DI // CLOCK_BOOTTIME
LEAQ 0(SP), SI
CALL AX
MOVQ 0(SP), AX // sec
diff -ru b/src/runtime/sys_linux_arm64.s a/src/runtime/sys_linux_arm64.s
--- b/src/runtime/sys_linux_arm64.s 2018-04-30 22:26:39.000000000 +0200
+++ a/src/runtime/sys_linux_arm64.s 2018-05-09 17:12:47.844591628 +0200
@@ -201,7 +201,7 @@
RET
TEXT runtime·nanotime(SB),NOSPLIT,$24-8
- MOVW $1, R0 // CLOCK_MONOTONIC
+ MOVW $7, R0 // CLOCK_BOOTTIME
MOVD RSP, R1
MOVD $SYS_clock_gettime, R8
SVC
diff -ru b/src/runtime/sys_linux_arm.s a/src/runtime/sys_linux_arm.s
--- b/src/runtime/sys_linux_arm.s 2018-04-30 22:26:39.000000000 +0200
+++ a/src/runtime/sys_linux_arm.s 2018-05-09 17:13:18.983281049 +0200
@@ -235,7 +235,7 @@
// int64 nanotime(void)
TEXT runtime·nanotime(SB),NOSPLIT,$32
- MOVW $1, R0 // CLOCK_MONOTONIC
+ MOVW $7, R0 // CLOCK_BOOTTIME
MOVW $8(R13), R1 // timespec
MOVW $SYS_clock_gettime, R7
SWI $0
diff -ru b/src/runtime/sys_linux_mips64x.s a/src/runtime/sys_linux_mips64x.s
--- b/src/runtime/sys_linux_mips64x.s 2018-04-30 22:26:39.000000000 +0200
+++ a/src/runtime/sys_linux_mips64x.s 2018-05-09 17:09:42.321531721 +0200
@@ -197,7 +197,7 @@
RET
TEXT runtime·nanotime(SB),NOSPLIT,$16
- MOVW $1, R4 // CLOCK_MONOTONIC
+ MOVW $7, R4 // CLOCK_BOOTTIME
MOVV $0(R29), R5
MOVV $SYS_clock_gettime, R2
SYSCALL
diff -ru b/src/runtime/sys_linux_mipsx.s a/src/runtime/sys_linux_mipsx.s
--- b/src/runtime/sys_linux_mipsx.s 2018-04-30 22:26:39.000000000 +0200
+++ a/src/runtime/sys_linux_mipsx.s 2018-05-09 17:09:42.321531721 +0200
@@ -202,7 +202,7 @@
RET
TEXT runtime·nanotime(SB),NOSPLIT,$8-8
- MOVW $1, R4 // CLOCK_MONOTONIC
+ MOVW $7, R4 // CLOCK_BOOTTIME
MOVW $4(R29), R5
MOVW $SYS_clock_gettime, R2
SYSCALL
diff -ru b/src/runtime/sys_linux_ppc64x.s a/src/runtime/sys_linux_ppc64x.s
--- b/src/runtime/sys_linux_ppc64x.s 2018-04-30 22:26:39.000000000 +0200
+++ a/src/runtime/sys_linux_ppc64x.s 2018-05-09 17:09:42.322531710 +0200
@@ -176,7 +176,7 @@
RET
TEXT runtime·nanotime(SB),NOSPLIT,$16
- MOVW $1, R3 // CLOCK_MONOTONIC
+ MOVW $7, R3 // CLOCK_BOOTTIME
MOVD $0(R1), R4
SYSCALL $SYS_clock_gettime
MOVD 0(R1), R3 // sec
diff -ru b/src/runtime/sys_linux_s390x.s a/src/runtime/sys_linux_s390x.s
--- b/src/runtime/sys_linux_s390x.s 2018-04-30 22:26:39.000000000 +0200
+++ a/src/runtime/sys_linux_s390x.s 2018-05-09 17:09:42.322531710 +0200
@@ -188,7 +188,7 @@
RET
TEXT runtime·nanotime(SB),NOSPLIT,$16
- MOVW $1, R2 // CLOCK_MONOTONIC
+ MOVW $7, R2 // CLOCK_BOOTTIME
MOVD $tp-16(SP), R3
MOVW $SYS_clock_gettime, R1
SYSCALL
@@ -1,31 +0,0 @@
/* SPDX-License-Identifier: Apache-2.0
*
* Copyright (C) 2017-2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
*/
package tun
import (
"git.zx2c4.com/wireguard-go/rwcancel"
"os"
)
func CreateTUNFromFD(tun_fd int) (TUNDevice, string, error) {
tun := &nativeTun{
fd: os.NewFile(uintptr(tun_fd), "/dev/tun"),
events: make(chan TUNEvent, 5),
errors: make(chan error, 5),
nopi: true,
}
var err error
tun.fdCancel, err = rwcancel.NewRWCancel(tun_fd)
if err != nil {
return nil, "", err
}
name, err := tun.Name()
if err != nil {
tun.fdCancel.Cancel()
return nil, "", err
}
return tun, name, nil
}
+72 -4
View File
@@ -1,14 +1,72 @@
allprojects {
buildscript {
ext {
activityVersion = '1.2.0-beta02'
agpVersion = '4.1.1'
annotationsVersion = '1.1.0'
appcompatVersion = '1.2.0'
biometricVersion = '1.1.0-rc01'
collectionVersion = '1.1.0'
constraintLayoutVersion = '2.0.4'
coordinatorLayoutVersion = '1.1.0'
coreKtxVersion = '1.3.2'
coroutinesVersion = '1.4.2'
datastoreVersion = '1.0.0-alpha02'
desugarVersion = '1.0.10'
fragmentVersion = '1.3.0-beta02'
jsr305Version = '3.0.2'
junitVersion = '4.13.1'
kotlinVersion = '1.4.21'
lifecycleRuntimeKtxVersion = '2.3.0-beta01'
materialComponentsVersion = '1.3.0-beta01'
preferenceVersion = '1.1.1'
zxingEmbeddedVersion = '3.6.0'
groupName = 'com.wireguard.android'
}
dependencies {
classpath "com.android.tools.build:gradle:$agpVersion"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
repositories {
google()
jcenter()
}
}
buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:3.1.3'
plugins {
id "de.undercouch.download" version "4.1.1"
}
task downloadCrowdin(type: Download) {
src 'https://crowdin.com/backend/download/project/wireguard.zip'
dest file('build/translations.zip')
overwrite true
}
task cleanCrowdin(type: Delete) {
delete 'ui/src/main/res/values-*/strings.xml'
}
task extractCrowdin(type: Copy, dependsOn: ['downloadCrowdin', 'cleanCrowdin']) {
mustRunAfter 'downloadCrowdin'
from zipTree(file('build/translations.zip'))
into file('build/translations')
doFirst {
delete 'build/translations'
}
}
task crowdin(type: Copy, dependsOn: ['extractCrowdin']) {
mustRunAfter 'extractCrowdin'
from 'build/translations/wireguard-android/ui/src/main/res'
into 'ui/src/main/res/'
doLast {
delete 'build/translations'
delete 'build/translations.zip'
}
}
allprojects {
repositories {
google()
jcenter()
@@ -18,3 +76,13 @@ buildscript {
task clean(type: Delete) {
delete rootProject.buildDir
}
tasks {
wrapper {
gradleVersion = "6.7.1"
distributionType = Wrapper.DistributionType.ALL
distributionSha256Sum = "22449f5231796abd892c98b2a07c9ceebe4688d192cd2d6763f8e3bf8acbedeb"
}
}
apply from: "version.gradle"
+13
View File
@@ -9,9 +9,22 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
android.enableJetifier=true
android.useAndroidX=true
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# https://jakewharton.com/increased-accuracy-of-aapt2-keep-rules/
android.useMinimalKeepRules=true
# Enable rudimentary R class namespacing where each library only contains
# references to the resources it declares instead of declarations plus all
# transitive dependency references.
android.namespacedRClass=true
# Suppress warnings for some features that aren't yet stabilized
android.suppressUnsupportedOptionWarnings=android.enableR8.fullMode,android.useMinimalKeepRules,android.namespacedRClass,android.suppressUnsupportedOptionWarnings
Binary file not shown.
+2 -1
View File
@@ -1,5 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-bin.zip
distributionSha256Sum=22449f5231796abd892c98b2a07c9ceebe4688d192cd2d6763f8e3bf8acbedeb
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+33 -20
View File
@@ -1,5 +1,21 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# 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
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# 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.
#
##############################################################################
##
## Gradle start up script for UN*X
@@ -28,7 +44,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
@@ -66,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -109,10 +126,11 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
@@ -138,19 +156,19 @@ if $cygwin ; then
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
i=`expr $i + 1`
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
@@ -159,14 +177,9 @@ save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
+2 -1
View File
@@ -1 +1,2 @@
include ':app'
include ':tunnel'
include ':ui'

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