Compare commits

...

453 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
296 changed files with 17044 additions and 8475 deletions
+1 -1
View File
@@ -15,4 +15,4 @@ build/
*.iml
*.jks
keystore.properties
package-info.java
gradlew.bat
+3 -6
View File
@@ -1,6 +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 "tunnel/tools/wireguard-tools"]
path = tunnel/tools/wireguard-tools
url = https://git.zx2c4.com/wireguard-tools
+29 -55
View File
@@ -56,6 +56,24 @@
</value>
</option>
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="10" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="10" />
</JetCodeStyleSettings>
<XML>
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
</XML>
@@ -358,6 +376,7 @@
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@@ -368,6 +387,7 @@
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@@ -379,6 +399,7 @@
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
@@ -389,6 +410,7 @@
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
@@ -399,6 +421,7 @@
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@@ -409,6 +432,7 @@
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@@ -419,6 +443,7 @@
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@@ -429,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>
@@ -494,6 +467,7 @@
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
+14 -13
View File
@@ -9,7 +9,6 @@
</option>
<option name="nonThreadSafeTypes" value="" />
</inspection_tool>
<inspection_tool class="AndroidLintGoogleAppIndexingWarning" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="AndroidLintIconExpectedSize" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AndroidLintNegativeMargin" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AndroidLintTypographyQuotes" enabled="true" level="WARNING" enabled_by_default="true" />
@@ -40,7 +39,9 @@
<inspection_tool class="AssignmentToLambdaParameter" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AssignmentToSuperclassField" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AssignmentUsedAsCondition" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true">
<option name="METHOD_MATCHER_CONFIG" value="java.util.Formatter,format,java.io.Writer,append,com.google.common.base.Preconditions,checkNotNull,org.hibernate.Session,close,java.io.PrintWriter,printf,java.util.HashMap,put,java.util.Map,put" />
</inspection_tool>
<inspection_tool class="BadExceptionCaught" enabled="true" level="WARNING" enabled_by_default="true">
<option name="exceptionsString" value="" />
<option name="exceptions">
@@ -59,6 +60,9 @@
<inspection_tool class="CallToStringConcatCanBeReplacedByOperator" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CannotResolve" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="CastConflictsWithInstanceof" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CatchMayIgnoreException" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreCatchBlocksWithComments" value="false" />
</inspection_tool>
<inspection_tool class="ChainedEquality" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ClassInitializer" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ClassNameDiffersFromFileName" enabled="true" level="WARNING" enabled_by_default="true" />
@@ -103,9 +107,6 @@
<inspection_tool class="DoubleLiteralMayBeFloatLiteral" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DuplicateAlternationBranch" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DuplicateBooleanBranch" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DuplicateCondition" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreSideEffectConditions" value="true" />
</inspection_tool>
<inspection_tool class="DuplicateDeclarations" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="DynamicRegexReplaceableByCompiledPattern" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ElementOnlyUsedFromTestCode" enabled="false" level="WARNING" enabled_by_default="false" />
@@ -196,7 +197,6 @@
<inspection_tool class="ImplicitSubclassInspection" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="IncompatibleTypes" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="InitializerIssues" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="InnerClassMayBeStatic" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="InnerClassReferencedViaSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="InnerClassVariableHidesOuterClassVariable" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreInvisibleFields" value="true" />
@@ -215,7 +215,6 @@
<inspection_tool class="InstanceofThis" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="InstantiationOfUtilityClass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="IntLiteralMayBeLongLiteral" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="IntegerDivisionInFloatingPointContext" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="IntegerTypeRequired" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="InterfaceMayBeAnnotatedFunctional" enabled="true" level="INFORMATION" enabled_by_default="true" />
<inspection_tool class="InterfaceNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
@@ -294,6 +293,7 @@
<option name="ignoreForLoopDeclarations" value="true" />
</inspection_tool>
<inspection_tool class="MultipleTopLevelClassesInFile" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="MultipleVariablesInDeclaration" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NamedResource" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="NativeMethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="NegatedConditional" enabled="true" level="WARNING" enabled_by_default="true">
@@ -347,7 +347,6 @@
<inspection_tool class="OverriddenMethodCallDuringObjectConstruction" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PackageInfoWithoutPackage" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PointerTypeRequired" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="PointlessNullCheck" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ProblematicVarargsMethodOverride" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ProtectedField" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ProtectedInnerClass" enabled="true" level="WARNING" enabled_by_default="true">
@@ -394,7 +393,13 @@
<inspection_tool class="SizeReplaceableByIsEmpty" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StaticCallOnSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StaticFieldReferenceOnSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StaticImport" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StaticImport" enabled="true" level="WARNING" enabled_by_default="true">
<option name="allowedClasses">
<set>
<option value="org.junit.Assert" />
</set>
</option>
</inspection_tool>
<inspection_tool class="StaticInheritance" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StaticMethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
<option name="m_regex" value="[a-z][A-Za-z\d]*" />
@@ -432,7 +437,6 @@
<inspection_tool class="TemplateArgumentsIssues" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="TestMethodWithoutAssertion" enabled="false" level="WARNING" enabled_by_default="false">
<option name="assertionMethods" value="org.junit.Assert,assert.*|fail.*,junit.framework.Assert,assert.*|fail.*,org.junit.jupiter.api.Assertions,assert.*|fail.*,org.mockito.Mockito,verify.*,org.mockito.InOrder,verify,org.junit.rules.ExpectedException,expect.*,org.hamcrest.MatcherAssert,assertThat" />
<option name="assertKeywordIsAssertion" value="false" />
</inspection_tool>
<inspection_tool class="TestNGMethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ThisEscapedInConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
@@ -440,7 +444,6 @@
<option name="ignoreRethrownExceptions" value="true" />
</inspection_tool>
<inspection_tool class="ThrowsRuntimeException" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ToArrayCallWithZeroLengthArrayArgument" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="TooBroadScope" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_allowConstructorAsInitializer" value="false" />
<option name="m_onlyLookAtBlocks" value="true" />
@@ -466,10 +469,8 @@
<inspection_tool class="UnnecessaryBlockStatement" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreSwitchBranches" value="false" />
</inspection_tool>
<inspection_tool class="UnnecessaryCallToStringValueOf" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryConstantArrayCreationExpression" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryDefault" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryExplicitNumericCast" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryFullyQualifiedName" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreJavadoc" value="false" />
+27
View File
@@ -11,3 +11,30 @@ $ git clone --recurse-submodules https://git.zx2c4.com/wireguard-android
$ cd wireguard-android
$ ./gradlew assembleRelease
```
macOS users may need [flock(1)](https://github.com/discoteq/flock).
## Embedding
The tunnel library is [on JCenter](https://bintray.com/wireguard/wireguard-android/wireguard-android/_latestVersion), alongside [extensive class library documentation](https://javadoc.io/doc/com.wireguard.android/tunnel).
```
implementation 'com.wireguard.android:tunnel:$wireguardTunnelVersion'
```
The library makes use of Java 8 features, so be sure to support those in your gradle configuration 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).
-104
View File
@@ -1,104 +0,0 @@
apply plugin: 'com.android.application'
apply from: 'nonnull.gradle'
// Create a variable called keystorePropertiesFile, and initialize it to your
// keystore.properties file, in the rootProject folder.
final def keystorePropertiesFile = rootProject.file("keystore.properties")
android {
buildToolsVersion '29.0.2'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
compileSdkVersion 29
dataBinding.enabled true
defaultConfig {
applicationId 'com.wireguard.android'
minSdkVersion 21
targetSdkVersion 29
versionCode 452
versionName '0.0.20191012'
buildConfigField 'int', 'MIN_SDK_VERSION', "$minSdkVersion.apiLevel"
}
// If the keystore file exists
if (keystorePropertiesFile.exists()) {
// Initialize a new Properties() object called keystoreProperties.
final def keystoreProperties = new Properties()
// Load your keystore.properties file into the keystoreProperties object.
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}
}
buildTypes {
release {
if (keystorePropertiesFile.exists()) signingConfig signingConfigs.release
externalNativeBuild {
cmake {
arguments "-DANDROID_PACKAGE_NAME=${android.defaultConfig.applicationId}"
}
}
minifyEnabled true
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
debug {
applicationIdSuffix ".debug"
versionNameSuffix "-debug"
externalNativeBuild {
cmake {
arguments "-DANDROID_PACKAGE_NAME=${android.defaultConfig.applicationId}${applicationIdSuffix}"
}
}
}
}
externalNativeBuild {
cmake {
path 'tools/CMakeLists.txt'
}
}
}
ext {
annotationsVersion = '1.1.0'
appcompatVersion = '1.1.0'
cardviewVersion = '1.0.0'
databindingVersion = '3.5.0'
leakCanaryVersion = "2.0-beta-3"
materialComponentsVersion = '1.0.0'
jsr305Version = '3.0.2'
preferenceVersion = '1.1.0'
streamsupportVersion = '1.7.1'
threetenabpVersion = '1.2.1'
// ZXING switched minSdk to 24 so we cannot upgrade to 4.0.2 without following suit.
// If you choose to upgrade to minSDK 24 then you should also disable Jetifier from
// gradle.properties.
zxingEmbeddedVersion = '3.6.0'
}
dependencies {
implementation "androidx.annotation:annotation:$annotationsVersion"
implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation "androidx.cardview:cardview:$cardviewVersion"
implementation "androidx.databinding:databinding-runtime:$databindingVersion"
implementation "androidx.preference:preference:$preferenceVersion"
implementation "com.google.android.material:material:$materialComponentsVersion"
implementation "com.google.code.findbugs:jsr305:$jsr305Version"
implementation "com.jakewharton.threetenabp:threetenabp:$threetenabpVersion"
implementation "com.journeyapps:zxing-android-embedded:$zxingEmbeddedVersion"
implementation "net.sourceforge.streamsupport:android-retrofuture:$streamsupportVersion"
implementation "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion"
debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryVersion"
}
tasks.withType(JavaCompile) {
options.compilerArgs << '-Xlint:unchecked'
options.deprecation = true
}
-87
View File
@@ -1,87 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
task generateNonNullJavaFiles(dependsOn: "assembleDebug", type: Copy) {
group = "Copying"
description = "Generate package-info.java classes"
def basePackage = "com" + File.separatorChar + "wireguard"
def mainSrcPhrase = "src" + File.separatorChar + "main" + File.separatorChar +
"java" + File.separatorChar
def mainTestSrcPhrase = "src" + File.separatorChar + "test" + File.separatorChar +
"java" + File.separatorChar
def mainAndroidTestSrcPhrase = "src" + File.separatorChar + "androidTest" + File.separatorChar +
"java" + File.separatorChar
def sourceDir = file( "${projectDir}" + File.separatorChar + "src" + File.separatorChar +
"main" + File.separatorChar + "java" + File.separatorChar +
basePackage )
def testSourceDir = file( "${projectDir}" + File.separatorChar + "src" + File.separatorChar +
"test" + File.separatorChar + "java" + File.separatorChar +
basePackage)
def androidTestSourceDir = file( "${projectDir}" + File.separatorChar + "src" + File
.separatorChar +
"androidTest" + File.separatorChar + "java" + File.separatorChar +
basePackage )
generateInfoFiles(sourceDir, mainSrcPhrase);
sourceDir.eachDirRecurse { dir ->
generateInfoFiles(dir, mainSrcPhrase)
}
if (file(testSourceDir).exists()) {
generateInfoFiles(testSourceDir, mainTestSrcPhrase);
testSourceDir.eachDirRecurse { dir ->
generateInfoFiles(dir, mainTestSrcPhrase)
}
}
if (file(androidTestSourceDir).exists()) {
generateInfoFiles(androidTestSourceDir, mainAndroidTestSrcPhrase);
androidTestSourceDir.eachDirRecurse { dir ->
generateInfoFiles(dir, mainAndroidTestSrcPhrase)
}
}
println "[SUCCESS] NonNull generator: package-info.java files checked"
}
private void generateInfoFiles(File dir, String mainSrcPhrase) {
def infoFileContentHeader = getFileContentHeader();
def infoFileContentFooter = getFileContentFooter();
def infoFilePath = dir.getAbsolutePath() + File.separatorChar + "package-info.java"
//file(infoFilePath).delete(); //do not use in production code
if (!file(infoFilePath).exists()) {
def infoFileContentPackage = getFileContentPackage(dir.getAbsolutePath(), mainSrcPhrase);
new File(infoFilePath).write(infoFileContentHeader +
infoFileContentPackage + infoFileContentFooter)
println "[dir] " + infoFilePath + " created";
}
}
def getFileContentPackage(String path, String mainSrcPhrase) {
def mainSrcPhraseIndex = path.indexOf(mainSrcPhrase)
def output = path.substring(mainSrcPhraseIndex)
// Win hotfix
if (System.properties['os.name'].toLowerCase().contains('windows')) {
output = output.replace("\\", "/")
mainSrcPhrase = mainSrcPhrase.replace("\\", "/")
}
return "package " + output.replaceAll(mainSrcPhrase, "").replaceAll(
"/", ".") + ";\n"
}
def getFileContentHeader() {
return "/**\n" +
" * Make all method parameters @NonNull by default.\n" +
" */\n" +
"@NonNullForAll\n"
}
def getFileContentFooter() {
return "\n" +
"import com.wireguard.util.NonNullForAll;\n"
}
-5
View File
@@ -1,5 +0,0 @@
# Squelch all warnings, they're harmless but ProGuard
# escalates them as errors.
-dontwarn sun.misc.Unsafe
# We're OSS anyway and who doesn't love a readable log
-dontobfuscate
@@ -1,131 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import androidx.preference.PreferenceManager;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDelegate;
import com.wireguard.android.backend.Backend;
import com.wireguard.android.backend.GoBackend;
import com.wireguard.android.backend.WgQuickBackend;
import com.wireguard.android.configStore.FileConfigStore;
import com.wireguard.android.model.TunnelManager;
import com.wireguard.android.util.AsyncWorker;
import com.wireguard.android.util.RootShell;
import com.wireguard.android.util.ToolsInstaller;
import java.io.File;
import java.lang.ref.WeakReference;
import java9.util.concurrent.CompletableFuture;
public class Application extends android.app.Application {
@SuppressWarnings("NullableProblems") private static WeakReference<Application> weakSelf;
private final CompletableFuture<Backend> futureBackend = new CompletableFuture<>();
@SuppressWarnings("NullableProblems") private AsyncWorker asyncWorker;
@Nullable private Backend backend;
@SuppressWarnings("NullableProblems") private RootShell rootShell;
@SuppressWarnings("NullableProblems") private SharedPreferences sharedPreferences;
@SuppressWarnings("NullableProblems") private ToolsInstaller toolsInstaller;
@SuppressWarnings("NullableProblems") private TunnelManager tunnelManager;
public Application() {
weakSelf = new WeakReference<>(this);
}
public static Application get() {
return weakSelf.get();
}
public static AsyncWorker getAsyncWorker() {
return get().asyncWorker;
}
public static Backend getBackend() {
final Application app = get();
synchronized (app.futureBackend) {
if (app.backend == null) {
Backend backend = null;
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
protected void attachBaseContext(final Context context) {
super.attachBaseContext(context);
if (BuildConfig.MIN_SDK_VERSION > Build.VERSION.SDK_INT) {
final Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
System.exit(0);
}
}
@Override
public void onCreate() {
super.onCreate();
asyncWorker = new AsyncWorker(AsyncTask.SERIAL_EXECUTOR, new Handler(Looper.getMainLooper()));
rootShell = new RootShell(getApplicationContext());
toolsInstaller = new ToolsInstaller(getApplicationContext());
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
AppCompatDelegate.setDefaultNightMode(
sharedPreferences.getBoolean("dark_theme", false) ?
AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);
} else {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
}
tunnelManager = new TunnelManager(new FileConfigStore(getApplicationContext()));
tunnelManager.onCreate();
asyncWorker.supplyAsync(Application::getBackend).thenAccept(futureBackend::complete);
}
}
@@ -1,38 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import com.wireguard.android.backend.WgQuickBackend;
import com.wireguard.android.model.TunnelManager;
import com.wireguard.android.util.ExceptionLoggers;
public class BootShutdownReceiver extends BroadcastReceiver {
private static final String TAG = "WireGuard/" + BootShutdownReceiver.class.getSimpleName();
@Override
public void onReceive(final Context context, final Intent intent) {
Application.getBackendAsync().thenAccept(backend -> {
if (!(backend instanceof WgQuickBackend))
return;
final String action = intent.getAction();
if (action == null)
return;
final TunnelManager tunnelManager = Application.getTunnelManager();
if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
Log.i(TAG, "Broadcast receiver restoring state (boot)");
tunnelManager.restoreState(false).whenComplete(ExceptionLoggers.D);
} else if (Intent.ACTION_SHUTDOWN.equals(action)) {
Log.i(TAG, "Broadcast receiver saving state (shutdown)");
tunnelManager.saveState();
}
});
}
}
@@ -1,175 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android;
import android.annotation.TargetApi;
import android.content.Intent;
import androidx.databinding.Observable;
import androidx.databinding.Observable.OnPropertyChangedCallback;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.IBinder;
import android.service.quicksettings.Tile;
import android.service.quicksettings.TileService;
import androidx.annotation.Nullable;
import android.util.Log;
import android.widget.Toast;
import com.wireguard.android.activity.MainActivity;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.util.ErrorMessages;
import com.wireguard.android.widget.SlashDrawable;
import java.util.Objects;
/**
* Service that maintains the application's custom Quick Settings tile. This service is bound by the
* system framework as necessary to update the appearance of the tile in the system UI, and to
* forward click events to the application.
*/
@TargetApi(Build.VERSION_CODES.N)
public class QuickTileService extends TileService {
private static final String TAG = "WireGuard/" + QuickTileService.class.getSimpleName();
private final OnStateChangedCallback onStateChangedCallback = new OnStateChangedCallback();
private final OnTunnelChangedCallback onTunnelChangedCallback = new OnTunnelChangedCallback();
@Nullable private Icon iconOff;
@Nullable private Icon iconOn;
@Nullable private Tunnel tunnel;
/* This works around an annoying unsolved frameworks bug some people are hitting. */
@Override
@Nullable
public IBinder onBind(final Intent intent) {
IBinder ret = null;
try {
ret = super.onBind(intent);
} catch (final Exception e) {
Log.d(TAG, "Failed to bind to TileService", e);
}
return ret;
}
@Override
public void onClick() {
if (tunnel != null) {
final Tile tile = getQsTile();
if (tile != null) {
tile.setIcon(tile.getIcon() == iconOn ? iconOff : iconOn);
tile.updateTile();
}
tunnel.setState(State.TOGGLE).whenComplete(this::onToggleFinished);
} else {
final Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivityAndCollapse(intent);
}
}
@Override
public void onCreate() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
iconOff = iconOn = Icon.createWithResource(this, R.drawable.ic_tile);
return;
}
final SlashDrawable icon = new SlashDrawable(getResources().getDrawable(R.drawable.ic_tile, Application.get().getTheme()));
icon.setAnimationEnabled(false); /* Unfortunately we can't have animations, since Icons are marshaled. */
icon.setSlashed(false);
Bitmap b = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);
icon.setBounds(0, 0, c.getWidth(), c.getHeight());
icon.draw(c);
iconOn = Icon.createWithBitmap(b);
icon.setSlashed(true);
b = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
c = new Canvas(b);
icon.setBounds(0, 0, c.getWidth(), c.getHeight());
icon.draw(c);
iconOff = Icon.createWithBitmap(b);
}
@Override
public void onStartListening() {
Application.getTunnelManager().addOnPropertyChangedCallback(onTunnelChangedCallback);
if (tunnel != null)
tunnel.addOnPropertyChangedCallback(onStateChangedCallback);
updateTile();
}
@Override
public void onStopListening() {
if (tunnel != null)
tunnel.removeOnPropertyChangedCallback(onStateChangedCallback);
Application.getTunnelManager().removeOnPropertyChangedCallback(onTunnelChangedCallback);
}
private void onToggleFinished(@SuppressWarnings("unused") final State state,
@Nullable final Throwable throwable) {
if (throwable == null)
return;
final String error = ErrorMessages.get(throwable);
final String message = getString(R.string.toggle_error, error);
Log.e(TAG, message, throwable);
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
private void updateTile() {
// Update the tunnel.
final Tunnel newTunnel = Application.getTunnelManager().getLastUsedTunnel();
if (newTunnel != tunnel) {
if (tunnel != null)
tunnel.removeOnPropertyChangedCallback(onStateChangedCallback);
tunnel = newTunnel;
if (tunnel != null)
tunnel.addOnPropertyChangedCallback(onStateChangedCallback);
}
// Update the tile contents.
final String label;
final int state;
final Tile tile = getQsTile();
if (tunnel != null) {
label = tunnel.getName();
state = tunnel.getState() == Tunnel.State.UP ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
} else {
label = getString(R.string.app_name);
state = Tile.STATE_INACTIVE;
}
if (tile == null)
return;
tile.setLabel(label);
if (tile.getState() != state) {
tile.setIcon(state == Tile.STATE_ACTIVE ? iconOn : iconOff);
tile.setState(state);
}
tile.updateTile();
}
private final class OnStateChangedCallback extends OnPropertyChangedCallback {
@Override
public void onPropertyChanged(final Observable sender, final int propertyId) {
if (!Objects.equals(sender, tunnel)) {
sender.removeOnPropertyChangedCallback(this);
return;
}
if (propertyId != 0 && propertyId != BR.state)
return;
updateTile();
}
}
private final class OnTunnelChangedCallback extends OnPropertyChangedCallback {
@Override
public void onPropertyChanged(final Observable sender, final int propertyId) {
if (propertyId != 0 && propertyId != BR.lastUsedTunnel)
return;
updateTile();
}
}
}
@@ -1,99 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity;
import androidx.databinding.CallbackRegistry;
import androidx.databinding.CallbackRegistry.NotifierCallback;
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.wireguard.android.Application;
import com.wireguard.android.model.Tunnel;
import java.util.Objects;
/**
* Base class for activities that need to remember the currently-selected tunnel.
*/
public abstract class BaseActivity extends ThemeChangeAwareActivity {
private static final String KEY_SELECTED_TUNNEL = "selected_tunnel";
private final SelectionChangeRegistry selectionChangeRegistry = new SelectionChangeRegistry();
@Nullable private Tunnel selectedTunnel;
public void addOnSelectedTunnelChangedListener(final OnSelectedTunnelChangedListener listener) {
selectionChangeRegistry.add(listener);
}
@Nullable
public Tunnel getSelectedTunnel() {
return selectedTunnel;
}
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
// Restore the saved tunnel if there is one; otherwise grab it from the arguments.
final String savedTunnelName;
if (savedInstanceState != null)
savedTunnelName = savedInstanceState.getString(KEY_SELECTED_TUNNEL);
else if (getIntent() != null)
savedTunnelName = getIntent().getStringExtra(KEY_SELECTED_TUNNEL);
else
savedTunnelName = null;
if (savedTunnelName != null)
Application.getTunnelManager().getTunnels()
.thenAccept(tunnels -> setSelectedTunnel(tunnels.get(savedTunnelName)));
// The selected tunnel must be set before the superclass method recreates fragments.
super.onCreate(savedInstanceState);
}
@Override
protected void onSaveInstanceState(final Bundle outState) {
if (selectedTunnel != null)
outState.putString(KEY_SELECTED_TUNNEL, selectedTunnel.getName());
super.onSaveInstanceState(outState);
}
protected abstract void onSelectedTunnelChanged(@Nullable Tunnel oldTunnel, @Nullable Tunnel newTunnel);
public void removeOnSelectedTunnelChangedListener(
final OnSelectedTunnelChangedListener listener) {
selectionChangeRegistry.remove(listener);
}
public void setSelectedTunnel(@Nullable final Tunnel tunnel) {
final Tunnel oldTunnel = selectedTunnel;
if (Objects.equals(oldTunnel, tunnel))
return;
selectedTunnel = tunnel;
onSelectedTunnelChanged(oldTunnel, tunnel);
selectionChangeRegistry.notifyCallbacks(oldTunnel, 0, tunnel);
}
public interface OnSelectedTunnelChangedListener {
void onSelectedTunnelChanged(@Nullable Tunnel oldTunnel, @Nullable Tunnel newTunnel);
}
private static final class SelectionChangeNotifier
extends NotifierCallback<OnSelectedTunnelChangedListener, Tunnel, Tunnel> {
@Override
public void onNotifyCallback(final OnSelectedTunnelChangedListener listener,
final Tunnel oldTunnel, final int ignored,
final Tunnel newTunnel) {
listener.onSelectedTunnelChanged(oldTunnel, newTunnel);
}
}
private static final class SelectionChangeRegistry
extends CallbackRegistry<OnSelectedTunnelChangedListener, Tunnel, Tunnel> {
private SelectionChangeRegistry() {
super(new SelectionChangeNotifier());
}
}
}
@@ -1,148 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.appcompat.app.ActionBar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.LinearLayout;
import com.wireguard.android.R;
import com.wireguard.android.fragment.TunnelDetailFragment;
import com.wireguard.android.fragment.TunnelEditorFragment;
import com.wireguard.android.fragment.TunnelListFragment;
import com.wireguard.android.model.Tunnel;
/**
* CRUD interface for WireGuard tunnels. This activity serves as the main entry point to the
* WireGuard application, and contains several fragments for listing, viewing details of, and
* editing the configuration and interface state of WireGuard tunnels.
*/
public class MainActivity extends BaseActivity
implements FragmentManager.OnBackStackChangedListener {
@Nullable private ActionBar actionBar;
private boolean isTwoPaneLayout;
@Nullable private TunnelListFragment listFragment;
@Override
public void onBackPressed() {
final int backStackEntries = getSupportFragmentManager().getBackStackEntryCount();
// If the action menu is visible and expanded, collapse it instead of navigating back.
if (isTwoPaneLayout || backStackEntries == 0) {
if (listFragment != null && listFragment.collapseActionMenu())
return;
}
// If the two-pane layout does not have an editor open, going back should exit the app.
if (isTwoPaneLayout && backStackEntries <= 1) {
finish();
return;
}
// Deselect the current tunnel on navigating back from the detail pane to the one-pane list.
if (!isTwoPaneLayout && backStackEntries == 1) {
setSelectedTunnel(null);
return;
}
if (isTaskRoot()) {
// @{link TunnelDetailFragment} is in foreground
if (backStackEntries == 2) {
getSupportFragmentManager().popBackStack();
} else if (backStackEntries == 0) {
finishAfterTransition();
}
} else {
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,115 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceScreen;
import android.util.SparseArray;
import android.view.MenuItem;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.backend.WgQuickBackend;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Interface for changing application-global persistent settings.
*/
public class SettingsActivity extends ThemeChangeAwareActivity {
private final SparseArray<PermissionRequestCallback> permissionRequestCallbacks = new SparseArray<>();
private int permissionRequestCounter;
public void ensurePermissions(final String[] permissions, final PermissionRequestCallback cb) {
final List<String> needPermissions = new ArrayList<>(permissions.length);
for (final String permission : permissions) {
if (ContextCompat.checkSelfPermission(this, permission)
!= PackageManager.PERMISSION_GRANTED)
needPermissions.add(permission);
}
if (needPermissions.isEmpty()) {
final int[] granted = new int[permissions.length];
Arrays.fill(granted, PackageManager.PERMISSION_GRANTED);
cb.done(permissions, granted);
return;
}
final int idx = permissionRequestCounter++;
permissionRequestCallbacks.put(idx, cb);
ActivityCompat.requestPermissions(this,
needPermissions.toArray(new String[needPermissions.size()]), idx);
}
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getSupportFragmentManager().findFragmentById(android.R.id.content) == null) {
getSupportFragmentManager().beginTransaction()
.add(android.R.id.content, new SettingsFragment())
.commit();
}
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onRequestPermissionsResult(final int requestCode,
final String[] permissions,
final int[] grantResults) {
final PermissionRequestCallback f = permissionRequestCallbacks.get(requestCode);
if (f != null) {
permissionRequestCallbacks.remove(requestCode);
f.done(permissions, grantResults);
}
}
public interface PermissionRequestCallback {
void done(String[] permissions, int[] grantResults);
}
public static class SettingsFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String key) {
addPreferencesFromResource(R.xml.preferences);
final PreferenceScreen screen = getPreferenceScreen();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
screen.removePreference(getPreferenceManager().findPreference("dark_theme"));
final Preference wgQuickOnlyPrefs[] = {
getPreferenceManager().findPreference("tools_installer"),
getPreferenceManager().findPreference("restore_on_boot")
};
for (final Preference pref : wgQuickOnlyPrefs)
pref.setVisible(false);
Application.getBackendAsync().thenAccept(backend -> {
for (final Preference pref : wgQuickOnlyPrefs) {
if (backend instanceof WgQuickBackend)
pref.setVisible(true);
else
screen.removePreference(pref);
}
});
}
}
}
@@ -1,82 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
import android.util.Log;
import com.wireguard.android.Application;
import java.lang.reflect.Field;
public abstract class ThemeChangeAwareActivity extends AppCompatActivity implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = "WireGuard/" + ThemeChangeAwareActivity.class.getSimpleName();
private static boolean lastDarkMode;
@Nullable private static Resources lastResources;
private static synchronized void invalidateDrawableCache(final Resources resources, final boolean darkMode) {
if (resources == lastResources && darkMode == lastDarkMode)
return;
try {
Field f;
Object o = resources;
try {
f = o.getClass().getDeclaredField("mResourcesImpl");
f.setAccessible(true);
o = f.get(o);
} catch (final Exception ignored) {
}
f = o.getClass().getDeclaredField("mDrawableCache");
f.setAccessible(true);
o = f.get(o);
try {
o.getClass().getMethod("onConfigurationChange", int.class).invoke(o, -1);
} catch (final Exception ignored) {
o.getClass().getMethod("clear").invoke(o);
}
} catch (final Exception e) {
Log.e(TAG, "Failed to flush drawable cache", e);
}
lastResources = resources;
lastDarkMode = darkMode;
}
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
Application.getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
}
@Override
protected void onDestroy() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
Application.getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
super.onDestroy();
}
@Override
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) {
if ("dark_theme".equals(key)) {
final boolean darkMode = sharedPreferences.getBoolean(key, false);
AppCompatDelegate.setDefaultNightMode(
sharedPreferences.getBoolean(key, false) ?
AppCompatDelegate.MODE_NIGHT_YES :
AppCompatDelegate.MODE_NIGHT_NO);
invalidateDrawableCache(getResources(), darkMode);
recreate();
}
}
}
@@ -1,34 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity;
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.wireguard.android.fragment.TunnelEditorFragment;
import com.wireguard.android.model.Tunnel;
/**
* Standalone activity for creating tunnels.
*/
public class TunnelCreatorActivity extends BaseActivity {
@Override
@SuppressWarnings("UnnecessaryFullyQualifiedName")
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getSupportFragmentManager().findFragmentById(android.R.id.content) == null) {
getSupportFragmentManager().beginTransaction()
.add(android.R.id.content, new TunnelEditorFragment())
.commit();
}
}
@Override
protected void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
finish();
}
}
@@ -1,241 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.backend;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.ParcelFileDescriptor;
import androidx.annotation.Nullable;
import androidx.collection.ArraySet;
import android.util.Log;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.activity.MainActivity;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.model.Tunnel.Statistics;
import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.android.util.SharedLibraryLoader;
import com.wireguard.config.Config;
import com.wireguard.config.InetNetwork;
import com.wireguard.config.Peer;
import java.net.InetAddress;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java9.util.concurrent.CompletableFuture;
public final class GoBackend implements Backend {
private static final String TAG = "WireGuard/" + GoBackend.class.getSimpleName();
private static CompletableFuture<VpnService> vpnService = new CompletableFuture<>();
private final Context context;
@Nullable private Tunnel currentTunnel;
private int currentTunnelHandle = -1;
public GoBackend(final Context context) {
SharedLibraryLoader.loadSharedLibrary(context, "wg-go");
this.context = context;
}
private static native int wgGetSocketV4(int handle);
private static native int wgGetSocketV6(int handle);
private static native void wgTurnOff(int handle);
private static native int wgTurnOn(String ifName, int tunFd, String settings);
private static native String wgVersion();
@Override
public Config applyConfig(final Tunnel tunnel, final Config config) throws Exception {
if (tunnel.getState() == State.UP) {
// Restart the tunnel to apply the new config.
setStateInternal(tunnel, tunnel.getConfig(), State.DOWN);
try {
setStateInternal(tunnel, config, State.UP);
} catch (final Exception e) {
// The new configuration didn't work, so try to go back to the old one.
setStateInternal(tunnel, tunnel.getConfig(), State.UP);
throw e;
}
}
return config;
}
@Override
public Set<String> enumerate() {
if (currentTunnel != null) {
final Set<String> runningTunnels = new ArraySet<>();
runningTunnels.add(currentTunnel.getName());
return runningTunnels;
}
return Collections.emptySet();
}
@Override
public State getState(final Tunnel tunnel) {
return currentTunnel == tunnel ? State.UP : State.DOWN;
}
@Override
public Statistics getStatistics(final Tunnel tunnel) {
return new Statistics();
}
@Override
public String getTypePrettyName() {
return context.getString(R.string.type_name_go_userspace);
}
@Override
public String getVersion() {
return wgVersion();
}
@Override
public State setState(final Tunnel tunnel, State state) throws Exception {
final State originalState = getState(tunnel);
if (state == State.TOGGLE)
state = originalState == State.UP ? State.DOWN : State.UP;
if (state == originalState)
return originalState;
if (state == State.UP && currentTunnel != null)
throw new IllegalStateException(context.getString(R.string.multiple_tunnels_error));
Log.d(TAG, "Changing tunnel " + tunnel.getName() + " to state " + state);
setStateInternal(tunnel, tunnel.getConfig(), state);
return getState(tunnel);
}
private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state)
throws Exception {
if (state == State.UP) {
Log.i(TAG, "Bringing tunnel up");
Objects.requireNonNull(config, context.getString(R.string.no_config_error));
if (VpnService.prepare(context) != null)
throw new Exception(context.getString(R.string.vpn_not_authorized_error));
final VpnService service;
if (!vpnService.isDone())
startVpnService();
try {
service = vpnService.get(2, TimeUnit.SECONDS);
} catch (final TimeoutException e) {
throw new Exception(context.getString(R.string.vpn_start_error), e);
}
if (currentTunnelHandle != -1) {
Log.w(TAG, "Tunnel already up");
return;
}
// Build config
final String goConfig = config.toWgUserspaceString();
// Create the vpn tunnel with android API
final VpnService.Builder builder = service.getBuilder();
builder.setSession(tunnel.getName());
final Intent configureIntent = new Intent(context, MainActivity.class);
configureIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
builder.setConfigureIntent(PendingIntent.getActivity(context, 0, configureIntent, 0));
for (final String excludedApplication : config.getInterface().getExcludedApplications())
builder.addDisallowedApplication(excludedApplication);
for (final InetNetwork addr : config.getInterface().getAddresses())
builder.addAddress(addr.getAddress(), addr.getMask());
for (final InetAddress addr : config.getInterface().getDnsServers())
builder.addDnsServer(addr.getHostAddress());
for (final Peer peer : config.getPeers()) {
for (final InetNetwork addr : peer.getAllowedIps())
builder.addRoute(addr.getAddress(), addr.getMask());
}
builder.setMtu(config.getInterface().getMtu().orElse(1280));
builder.setBlocking(true);
try (final ParcelFileDescriptor tun = builder.establish()) {
if (tun == null)
throw new Exception(context.getString(R.string.tun_create_error));
Log.d(TAG, "Go backend v" + wgVersion());
currentTunnelHandle = wgTurnOn(tunnel.getName(), tun.detachFd(), goConfig);
}
if (currentTunnelHandle < 0)
throw new Exception(context.getString(R.string.tunnel_on_error, currentTunnelHandle));
currentTunnel = tunnel;
service.protect(wgGetSocketV4(currentTunnelHandle));
service.protect(wgGetSocketV6(currentTunnelHandle));
} else {
Log.i(TAG, "Bringing tunnel down");
if (currentTunnelHandle == -1) {
Log.w(TAG, "Tunnel already down");
return;
}
wgTurnOff(currentTunnelHandle);
currentTunnel = null;
currentTunnelHandle = -1;
}
}
private void startVpnService() {
Log.d(TAG, "Requesting to start VpnService");
context.startService(new Intent(context, VpnService.class));
}
public static class VpnService extends android.net.VpnService {
public Builder getBuilder() {
return new Builder();
}
@Override
public void onCreate() {
vpnService.complete(this);
super.onCreate();
}
@Override
public void onDestroy() {
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
for (final Tunnel tunnel : tunnels) {
if (tunnel != null && tunnel.getState() != State.DOWN)
tunnel.setState(State.DOWN);
}
});
vpnService = vpnService.newIncompleteFuture();
super.onDestroy();
}
@Override
public int onStartCommand(@Nullable final Intent intent, final int flags, final int startId) {
vpnService.complete(this);
if (intent == null || intent.getComponent() == null || !intent.getComponent().getPackageName().equals(getPackageName())) {
Log.d(TAG, "Service started by Always-on VPN feature");
Application.getTunnelManager().restoreState(true).whenComplete(ExceptionLoggers.D);
}
return super.onStartCommand(intent, flags, startId);
}
}
}
@@ -1,133 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.backend;
import android.content.Context;
import androidx.annotation.Nullable;
import android.util.Log;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.model.Tunnel.Statistics;
import com.wireguard.config.Config;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java9.util.stream.Collectors;
import java9.util.stream.Stream;
/**
* WireGuard backend that uses {@code wg-quick} to implement tunnel configuration.
*/
public final class WgQuickBackend implements Backend {
private static final String TAG = "WireGuard/" + WgQuickBackend.class.getSimpleName();
private final File localTemporaryDir;
private final Context context;
public WgQuickBackend(final Context context) {
localTemporaryDir = new File(context.getCacheDir(), "tmp");
this.context = context;
}
@Override
public Config applyConfig(final Tunnel tunnel, final Config config) throws Exception {
if (tunnel.getState() == State.UP) {
// Restart the tunnel to apply the new config.
setStateInternal(tunnel, tunnel.getConfig(), State.DOWN);
try {
setStateInternal(tunnel, config, State.UP);
} catch (final Exception e) {
// The new configuration didn't work, so try to go back to the old one.
setStateInternal(tunnel, tunnel.getConfig(), State.UP);
throw e;
}
}
return config;
}
@Override
public Set<String> enumerate() {
final List<String> output = new ArrayList<>();
// Don't throw an exception here or nothing will show up in the UI.
try {
Application.getToolsInstaller().ensureToolsAvailable();
if (Application.getRootShell().run(output, "wg show interfaces") != 0 || output.isEmpty())
return Collections.emptySet();
} catch (final Exception e) {
Log.w(TAG, "Unable to enumerate running tunnels", e);
return Collections.emptySet();
}
// wg puts all interface names on the same line. Split them into separate elements.
return Stream.of(output.get(0).split(" ")).collect(Collectors.toUnmodifiableSet());
}
@Override
public State getState(final Tunnel tunnel) {
return enumerate().contains(tunnel.getName()) ? State.UP : State.DOWN;
}
@Override
public Statistics getStatistics(final Tunnel tunnel) {
return new Statistics();
}
@Override
public String getTypePrettyName() {
return context.getString(R.string.type_name_kernel_module);
}
@Override
public String getVersion() throws Exception {
final List<String> output = new ArrayList<>();
if (Application.getRootShell()
.run(output, "cat /sys/module/wireguard/version") != 0 || output.isEmpty())
throw new Exception(context.getString(R.string.module_version_error));
return output.get(0);
}
@Override
public State setState(final Tunnel tunnel, State state) throws Exception {
final State originalState = getState(tunnel);
if (state == State.TOGGLE)
state = originalState == State.UP ? State.DOWN : State.UP;
if (state == originalState)
return originalState;
Log.d(TAG, "Changing tunnel " + tunnel.getName() + " to state " + state);
Application.getToolsInstaller().ensureToolsAvailable();
setStateInternal(tunnel, tunnel.getConfig(), state);
return getState(tunnel);
}
private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state) throws Exception {
Objects.requireNonNull(config, "Trying to set state with a null config");
final File tempFile = new File(localTemporaryDir, tunnel.getName() + ".conf");
try (final FileOutputStream stream = new FileOutputStream(tempFile, false)) {
stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8));
}
String command = String.format("wg-quick %s '%s'",
state.toString().toLowerCase(Locale.ENGLISH), tempFile.getAbsolutePath());
if (state == State.UP)
command = "cat /sys/module/wireguard/version && " + command;
final int result = Application.getRootShell().run(null, command);
// noinspection ResultOfMethodCallIgnored
tempFile.delete();
if (result != 0)
throw new Exception(context.getString(R.string.tunnel_config_error, result));
}
}
@@ -1,103 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.configStore;
import android.content.Context;
import android.util.Log;
import com.wireguard.android.R;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.Config;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java9.util.stream.Collectors;
import java9.util.stream.Stream;
/**
* Configuration store that uses a {@code wg-quick}-style file for each configured tunnel.
*/
public final class FileConfigStore implements ConfigStore {
private static final String TAG = "WireGuard/" + FileConfigStore.class.getSimpleName();
private final Context context;
public FileConfigStore(final Context context) {
this.context = context;
}
@Override
public Config create(final String name, final Config config) throws IOException {
Log.d(TAG, "Creating configuration for tunnel " + name);
final File file = fileFor(name);
if (!file.createNewFile())
throw new IOException(context.getString(R.string.config_file_exists_error, file.getName()));
try (final FileOutputStream stream = new FileOutputStream(file, false)) {
stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8));
}
return config;
}
@Override
public void delete(final String name) throws IOException {
Log.d(TAG, "Deleting configuration for tunnel " + name);
final File file = fileFor(name);
if (!file.delete())
throw new IOException(context.getString(R.string.config_delete_error, file.getName()));
}
@Override
public Set<String> enumerate() {
return Stream.of(context.fileList())
.filter(name -> name.endsWith(".conf"))
.map(name -> name.substring(0, name.length() - ".conf".length()))
.collect(Collectors.toUnmodifiableSet());
}
private File fileFor(final String name) {
return new File(context.getFilesDir(), name + ".conf");
}
@Override
public Config load(final String name) throws BadConfigException, IOException {
try (final FileInputStream stream = new FileInputStream(fileFor(name))) {
return Config.parse(stream);
}
}
@Override
public void rename(final String name, final String replacement) throws IOException {
Log.d(TAG, "Renaming configuration for tunnel " + name + " to " + replacement);
final File file = fileFor(name);
final File replacementFile = fileFor(replacement);
if (!replacementFile.createNewFile())
throw new IOException(context.getString(R.string.config_exists_error, replacement));
if (!file.renameTo(replacementFile)) {
if (!replacementFile.delete())
Log.w(TAG, "Couldn't delete marker file for new name " + replacement);
throw new IOException(context.getString(R.string.config_rename_error, file.getName()));
}
}
@Override
public Config save(final String name, final Config config) throws IOException {
Log.d(TAG, "Saving configuration for tunnel " + name);
final File file = fileFor(name);
if (!file.isFile())
throw new FileNotFoundException(context.getString(R.string.config_not_found_error, file.getName()));
try (final FileOutputStream stream = new FileOutputStream(file, false)) {
stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8));
}
return config;
}
}
@@ -1,148 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.databinding;
import androidx.databinding.BindingAdapter;
import androidx.databinding.DataBindingUtil;
import androidx.databinding.ObservableList;
import androidx.databinding.ViewDataBinding;
import androidx.databinding.adapters.ListenerUtil;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.text.InputFilter;
import android.view.LayoutInflater;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.wireguard.android.BR;
import com.wireguard.android.R;
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler;
import com.wireguard.android.util.ObservableKeyedList;
import com.wireguard.android.widget.ToggleSwitch;
import com.wireguard.android.widget.ToggleSwitch.OnBeforeCheckedChangeListener;
import com.wireguard.config.Attribute;
import com.wireguard.config.InetNetwork;
import com.wireguard.util.Keyed;
import java9.util.Optional;
/**
* Static methods for use by generated code in the Android data binding library.
*/
@SuppressWarnings("unused")
public final class BindingAdapters {
private BindingAdapters() {
// Prevent instantiation.
}
@BindingAdapter("checked")
public static void setChecked(final ToggleSwitch view, final boolean checked) {
view.setCheckedInternal(checked);
}
@BindingAdapter("filter")
public static void setFilter(final TextView view, final InputFilter filter) {
view.setFilters(new InputFilter[]{filter});
}
@BindingAdapter({"items", "layout"})
public static <E>
void setItems(final LinearLayout view,
@Nullable final ObservableList<E> oldList, final int oldLayoutId,
@Nullable final ObservableList<E> newList, final int newLayoutId) {
if (oldList == newList && oldLayoutId == newLayoutId)
return;
ItemChangeListener<E> listener = ListenerUtil.getListener(view, R.id.item_change_listener);
// If the layout changes, any existing listener must be replaced.
if (listener != null && oldList != null && oldLayoutId != newLayoutId) {
listener.setList(null);
listener = null;
// Stop tracking the old listener.
ListenerUtil.trackListener(view, null, R.id.item_change_listener);
}
// Avoid adding a listener when there is no new list or layout.
if (newList == null || newLayoutId == 0)
return;
if (listener == null) {
listener = new ItemChangeListener<>(view, newLayoutId);
ListenerUtil.trackListener(view, listener, R.id.item_change_listener);
}
// Either the list changed, or this is an entirely new listener because the layout changed.
listener.setList(newList);
}
@BindingAdapter({"items", "layout"})
public static <E>
void setItems(final LinearLayout view,
@Nullable final Iterable<E> oldList, final int oldLayoutId,
@Nullable final Iterable<E> newList, final int newLayoutId) {
if (oldList == newList && oldLayoutId == newLayoutId)
return;
view.removeAllViews();
if (newList == null)
return;
final LayoutInflater layoutInflater = LayoutInflater.from(view.getContext());
for (final E item : newList) {
final ViewDataBinding binding =
DataBindingUtil.inflate(layoutInflater, newLayoutId, view, false);
binding.setVariable(BR.collection, newList);
binding.setVariable(BR.item, item);
binding.executePendingBindings();
view.addView(binding.getRoot());
}
}
@BindingAdapter(requireAll = false, value = {"items", "layout", "configurationHandler"})
public static <K, E extends Keyed<? extends K>>
void setItems(final RecyclerView view,
@Nullable final ObservableKeyedList<K, E> oldList, final int oldLayoutId,
final RowConfigurationHandler oldRowConfigurationHandler,
@Nullable final ObservableKeyedList<K, E> newList, final int newLayoutId,
final RowConfigurationHandler newRowConfigurationHandler) {
if (view.getLayoutManager() == null)
view.setLayoutManager(new LinearLayoutManager(view.getContext(), RecyclerView.VERTICAL, false));
if (oldList == newList && oldLayoutId == newLayoutId)
return;
// The ListAdapter interface is not generic, so this cannot be checked.
@SuppressWarnings("unchecked") ObservableKeyedRecyclerViewAdapter<K, E> adapter =
(ObservableKeyedRecyclerViewAdapter<K, E>) view.getAdapter();
// If the layout changes, any existing adapter must be replaced.
if (adapter != null && oldList != null && oldLayoutId != newLayoutId) {
adapter.setList(null);
adapter = null;
}
// Avoid setting an adapter when there is no new list or layout.
if (newList == null || newLayoutId == 0)
return;
if (adapter == null) {
adapter = new ObservableKeyedRecyclerViewAdapter<>(view.getContext(), newLayoutId, newList);
view.setAdapter(adapter);
}
adapter.setRowConfigurationHandler(newRowConfigurationHandler);
// Either the list changed, or this is an entirely new listener because the layout changed.
adapter.setList(newList);
}
@BindingAdapter("onBeforeCheckedChanged")
public static void setOnBeforeCheckedChanged(final ToggleSwitch view,
final OnBeforeCheckedChangeListener listener) {
view.setOnBeforeCheckedChangeListener(listener);
}
@BindingAdapter("android:text")
public static void setText(final TextView view, final Optional<?> text) {
view.setText(text.map(Object::toString).orElse(""));
}
@BindingAdapter("android:text")
public static void setText(final TextView view, @Nullable final Iterable<InetNetwork> networks) {
view.setText(networks != null ? Attribute.join(networks) : "");
}
}
@@ -1,140 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.databinding;
import androidx.databinding.DataBindingUtil;
import androidx.databinding.ObservableList;
import androidx.databinding.ViewDataBinding;
import androidx.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.wireguard.android.BR;
import java.lang.ref.WeakReference;
import java.util.Objects;
/**
* Helper class for binding an ObservableList to the children of a ViewGroup.
*/
class ItemChangeListener<T> {
private final OnListChangedCallback<T> callback = new OnListChangedCallback<>(this);
private final ViewGroup container;
private final int layoutId;
private final LayoutInflater layoutInflater;
@Nullable private ObservableList<T> list;
ItemChangeListener(final ViewGroup container, final int layoutId) {
this.container = container;
this.layoutId = layoutId;
layoutInflater = LayoutInflater.from(container.getContext());
}
private View getView(final int position, @Nullable final View convertView) {
ViewDataBinding binding = convertView != null ? DataBindingUtil.getBinding(convertView) : null;
if (binding == null) {
binding = DataBindingUtil.inflate(layoutInflater, layoutId, container, false);
}
Objects.requireNonNull(list, "Trying to get a view while list is still null");
binding.setVariable(BR.collection, list);
binding.setVariable(BR.item, list.get(position));
binding.executePendingBindings();
return binding.getRoot();
}
void setList(@Nullable final ObservableList<T> newList) {
if (list != null)
list.removeOnListChangedCallback(callback);
list = newList;
if (list != null) {
list.addOnListChangedCallback(callback);
callback.onChanged(list);
} else {
container.removeAllViews();
}
}
private static final class OnListChangedCallback<T>
extends ObservableList.OnListChangedCallback<ObservableList<T>> {
private final WeakReference<ItemChangeListener<T>> weakListener;
private OnListChangedCallback(final ItemChangeListener<T> listener) {
weakListener = new WeakReference<>(listener);
}
@Override
public void onChanged(final ObservableList<T> sender) {
final ItemChangeListener<T> listener = weakListener.get();
if (listener != null) {
// TODO: recycle views
listener.container.removeAllViews();
for (int i = 0; i < sender.size(); ++i)
listener.container.addView(listener.getView(i, null));
} else {
sender.removeOnListChangedCallback(this);
}
}
@Override
public void onItemRangeChanged(final ObservableList<T> sender, final int positionStart,
final int itemCount) {
final ItemChangeListener<T> listener = weakListener.get();
if (listener != null) {
for (int i = positionStart; i < positionStart + itemCount; ++i) {
final View child = listener.container.getChildAt(i);
listener.container.removeViewAt(i);
listener.container.addView(listener.getView(i, child));
}
} else {
sender.removeOnListChangedCallback(this);
}
}
@Override
public void onItemRangeInserted(final ObservableList<T> sender, final int positionStart,
final int itemCount) {
final ItemChangeListener<T> listener = weakListener.get();
if (listener != null) {
for (int i = positionStart; i < positionStart + itemCount; ++i)
listener.container.addView(listener.getView(i, null));
} else {
sender.removeOnListChangedCallback(this);
}
}
@Override
public void onItemRangeMoved(final ObservableList<T> sender, final int fromPosition,
final int toPosition, final int itemCount) {
final ItemChangeListener<T> listener = weakListener.get();
if (listener != null) {
final View[] views = new View[itemCount];
for (int i = 0; i < itemCount; ++i)
views[i] = listener.container.getChildAt(fromPosition + i);
listener.container.removeViews(fromPosition, itemCount);
for (int i = 0; i < itemCount; ++i)
listener.container.addView(views[i], toPosition + i);
} else {
sender.removeOnListChangedCallback(this);
}
}
@Override
public void onItemRangeRemoved(final ObservableList<T> sender, final int positionStart,
final int itemCount) {
final ItemChangeListener<T> listener = weakListener.get();
if (listener != null) {
listener.container.removeViews(positionStart, itemCount);
} else {
sender.removeOnListChangedCallback(this);
}
}
}
}
@@ -1,159 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.databinding;
import android.content.Context;
import androidx.databinding.DataBindingUtil;
import androidx.databinding.ObservableList;
import androidx.databinding.ViewDataBinding;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.Adapter;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import com.wireguard.android.BR;
import com.wireguard.android.util.ObservableKeyedList;
import com.wireguard.util.Keyed;
import java.lang.ref.WeakReference;
/**
* A generic {@code RecyclerView.Adapter} backed by a {@code ObservableKeyedList}.
*/
public class ObservableKeyedRecyclerViewAdapter<K, E extends Keyed<? extends K>> extends Adapter<ObservableKeyedRecyclerViewAdapter.ViewHolder> {
private final OnListChangedCallback<E> callback = new OnListChangedCallback<>(this);
private final int layoutId;
private final LayoutInflater layoutInflater;
@Nullable private ObservableKeyedList<K, E> list;
@Nullable private RowConfigurationHandler rowConfigurationHandler;
ObservableKeyedRecyclerViewAdapter(final Context context, final int layoutId,
final ObservableKeyedList<K, E> list) {
this.layoutId = layoutId;
layoutInflater = LayoutInflater.from(context);
setList(list);
}
@Nullable
private E getItem(final int position) {
if (list == null || position < 0 || position >= list.size())
return null;
return list.get(position);
}
@Override
public int getItemCount() {
return list != null ? list.size() : 0;
}
@Override
public long getItemId(final int position) {
final K key = getKey(position);
return key != null ? key.hashCode() : -1;
}
@Nullable
private K getKey(final int position) {
final E item = getItem(position);
return item != null ? item.getKey() : null;
}
@SuppressWarnings("unchecked")
@Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
holder.binding.setVariable(BR.collection, list);
holder.binding.setVariable(BR.key, getKey(position));
holder.binding.setVariable(BR.item, getItem(position));
holder.binding.executePendingBindings();
if (rowConfigurationHandler != null) {
final E item = getItem(position);
if (item != null) {
rowConfigurationHandler.onConfigureRow(holder.binding, item, position);
}
}
}
@Override
public ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
return new ViewHolder(DataBindingUtil.inflate(layoutInflater, layoutId, parent, false));
}
void setList(@Nullable final ObservableKeyedList<K, E> newList) {
if (list != null)
list.removeOnListChangedCallback(callback);
list = newList;
if (list != null) {
list.addOnListChangedCallback(callback);
}
notifyDataSetChanged();
}
void setRowConfigurationHandler(final RowConfigurationHandler rowConfigurationHandler) {
this.rowConfigurationHandler = rowConfigurationHandler;
}
public interface RowConfigurationHandler<B extends ViewDataBinding, T> {
void onConfigureRow(B binding, T item, int position);
}
private static final class OnListChangedCallback<E extends Keyed<?>>
extends ObservableList.OnListChangedCallback<ObservableList<E>> {
private final WeakReference<ObservableKeyedRecyclerViewAdapter<?, E>> weakAdapter;
private OnListChangedCallback(final ObservableKeyedRecyclerViewAdapter<?, E> adapter) {
weakAdapter = new WeakReference<>(adapter);
}
@Override
public void onChanged(final ObservableList<E> sender) {
final ObservableKeyedRecyclerViewAdapter adapter = weakAdapter.get();
if (adapter != null)
adapter.notifyDataSetChanged();
else
sender.removeOnListChangedCallback(this);
}
@Override
public void onItemRangeChanged(final ObservableList<E> sender, final int positionStart,
final int itemCount) {
onChanged(sender);
}
@Override
public void onItemRangeInserted(final ObservableList<E> sender, final int positionStart,
final int itemCount) {
onChanged(sender);
}
@Override
public void onItemRangeMoved(final ObservableList<E> sender, final int fromPosition,
final int toPosition, final int itemCount) {
onChanged(sender);
}
@Override
public void onItemRangeRemoved(final ObservableList<E> sender, final int positionStart,
final int itemCount) {
onChanged(sender);
}
}
public static class ViewHolder extends RecyclerView.ViewHolder {
final ViewDataBinding binding;
public ViewHolder(final ViewDataBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
}
@@ -1,133 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.appcompat.app.AlertDialog;
import android.widget.Toast;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.databinding.AppListDialogFragmentBinding;
import com.wireguard.android.model.ApplicationData;
import com.wireguard.android.util.ErrorMessages;
import com.wireguard.android.util.ObservableKeyedArrayList;
import com.wireguard.android.util.ObservableKeyedList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java9.util.Comparators;
public class AppListDialogFragment extends DialogFragment {
private static final String KEY_EXCLUDED_APPS = "excludedApps";
private final ObservableKeyedList<String, ApplicationData> appData = new ObservableKeyedArrayList<>();
@Nullable private List<String> currentlyExcludedApps;
public static <T extends Fragment & AppExclusionListener>
AppListDialogFragment newInstance(final ArrayList<String> excludedApps, final T target) {
final Bundle extras = new Bundle();
extras.putStringArrayList(KEY_EXCLUDED_APPS, excludedApps);
final AppListDialogFragment fragment = new AppListDialogFragment();
fragment.setTargetFragment(target, 0);
fragment.setArguments(extras);
return fragment;
}
private void loadData() {
final Activity activity = getActivity();
if (activity == null) {
return;
}
final PackageManager pm = activity.getPackageManager();
Application.getAsyncWorker().supplyAsync(() -> {
final Intent launcherIntent = new Intent(Intent.ACTION_MAIN, null);
launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER);
final List<ResolveInfo> resolveInfos = pm.queryIntentActivities(launcherIntent, 0);
final List<ApplicationData> appData = new ArrayList<>();
for (ResolveInfo resolveInfo : resolveInfos) {
String packageName = resolveInfo.activityInfo.packageName;
appData.add(new ApplicationData(resolveInfo.loadIcon(pm), resolveInfo.loadLabel(pm).toString(), packageName, currentlyExcludedApps.contains(packageName)));
}
Collections.sort(appData, Comparators.comparing(ApplicationData::getName, String.CASE_INSENSITIVE_ORDER));
return appData;
}).whenComplete(((data, throwable) -> {
if (data != null) {
appData.clear();
appData.addAll(data);
} else {
final String error = ErrorMessages.get(throwable);
final String message = activity.getString(R.string.error_fetching_apps, error);
Toast.makeText(activity, message, Toast.LENGTH_LONG).show();
dismissAllowingStateLoss();
}
}));
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
currentlyExcludedApps = getArguments().getStringArrayList(KEY_EXCLUDED_APPS);
}
@Override
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity());
alertDialogBuilder.setTitle(R.string.excluded_applications);
final AppListDialogFragmentBinding binding = AppListDialogFragmentBinding.inflate(getActivity().getLayoutInflater(), null, false);
binding.executePendingBindings();
alertDialogBuilder.setView(binding.getRoot());
alertDialogBuilder.setPositiveButton(R.string.set_exclusions, (dialog, which) -> setExclusionsAndDismiss());
alertDialogBuilder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
alertDialogBuilder.setNeutralButton(R.string.deselect_all, (dialog, which) -> {
});
binding.setFragment(this);
binding.setAppData(appData);
loadData();
final AlertDialog dialog = alertDialogBuilder.create();
dialog.setOnShowListener(d -> dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener(view -> {
for (final ApplicationData app : appData)
app.setExcludedFromTunnel(false);
}));
return dialog;
}
void setExclusionsAndDismiss() {
final List<String> excludedApps = new ArrayList<>();
for (final ApplicationData data : appData) {
if (data.isExcludedFromTunnel()) {
excludedApps.add(data.getPackageName());
}
}
((AppExclusionListener) getTargetFragment()).onExcludedAppsSelected(excludedApps);
dismiss();
}
public interface AppExclusionListener {
void onExcludedAppsSelected(List<String> excludedApps);
}
}
@@ -1,126 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment;
import android.content.Context;
import android.content.Intent;
import androidx.databinding.DataBindingUtil;
import androidx.databinding.ViewDataBinding;
import androidx.annotation.Nullable;
import com.google.android.material.snackbar.Snackbar;
import androidx.fragment.app.Fragment;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.activity.BaseActivity;
import com.wireguard.android.activity.BaseActivity.OnSelectedTunnelChangedListener;
import com.wireguard.android.backend.GoBackend;
import com.wireguard.android.databinding.TunnelDetailFragmentBinding;
import com.wireguard.android.databinding.TunnelListItemBinding;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.util.ErrorMessages;
/**
* Base class for fragments that need to know the currently-selected tunnel. Only does anything when
* attached to a {@code BaseActivity}.
*/
public abstract class BaseFragment extends Fragment implements OnSelectedTunnelChangedListener {
private static final int REQUEST_CODE_VPN_PERMISSION = 23491;
private static final String TAG = "WireGuard/" + BaseFragment.class.getSimpleName();
@Nullable private BaseActivity activity;
@Nullable private Tunnel pendingTunnel;
@Nullable private Boolean pendingTunnelUp;
@Nullable
protected Tunnel getSelectedTunnel() {
return activity != null ? activity.getSelectedTunnel() : null;
}
@Override
public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_VPN_PERMISSION) {
if (pendingTunnel != null && pendingTunnelUp != null)
setTunnelStateWithPermissionsResult(pendingTunnel, pendingTunnelUp);
pendingTunnel = null;
pendingTunnelUp = null;
}
}
@Override
public void onAttach(final Context context) {
super.onAttach(context);
if (context instanceof BaseActivity) {
activity = (BaseActivity) context;
activity.addOnSelectedTunnelChangedListener(this);
} else {
activity = null;
}
}
@Override
public void onDetach() {
if (activity != null)
activity.removeOnSelectedTunnelChangedListener(this);
activity = null;
super.onDetach();
}
protected void setSelectedTunnel(@Nullable final Tunnel tunnel) {
if (activity != null)
activity.setSelectedTunnel(tunnel);
}
public void setTunnelState(final View view, final boolean checked) {
final ViewDataBinding binding = DataBindingUtil.findBinding(view);
final Tunnel tunnel;
if (binding instanceof TunnelDetailFragmentBinding)
tunnel = ((TunnelDetailFragmentBinding) binding).getTunnel();
else if (binding instanceof TunnelListItemBinding)
tunnel = ((TunnelListItemBinding) binding).getItem();
else
return;
if (tunnel == null)
return;
Application.getBackendAsync().thenAccept(backend -> {
if (backend instanceof GoBackend) {
final Intent intent = GoBackend.VpnService.prepare(view.getContext());
if (intent != null) {
pendingTunnel = tunnel;
pendingTunnelUp = checked;
startActivityForResult(intent, REQUEST_CODE_VPN_PERMISSION);
return;
}
}
setTunnelStateWithPermissionsResult(tunnel, checked);
});
}
private void setTunnelStateWithPermissionsResult(final Tunnel tunnel, final boolean checked) {
tunnel.setState(State.of(checked)).whenComplete((state, throwable) -> {
if (throwable == null)
return;
final String error = ErrorMessages.get(throwable);
final int messageResId = checked ? R.string.error_up : R.string.error_down;
final String message = getContext().getString(messageResId, error);
final View view = getView();
if (view != null)
Snackbar.make(view, message, Snackbar.LENGTH_LONG).show();
else
Toast.makeText(getContext(), message, Toast.LENGTH_LONG).show();
Log.e(TAG, message, throwable);
});
}
}
@@ -1,118 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.appcompat.app.AlertDialog;
import android.view.inputmethod.InputMethodManager;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.databinding.ConfigNamingDialogFragmentBinding;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.Config;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
public class ConfigNamingDialogFragment extends DialogFragment {
private static final String KEY_CONFIG_TEXT = "config_text";
@Nullable private ConfigNamingDialogFragmentBinding binding;
@Nullable private Config config;
@Nullable private InputMethodManager imm;
public static ConfigNamingDialogFragment newInstance(final String configText) {
final Bundle extras = new Bundle();
extras.putString(KEY_CONFIG_TEXT, configText);
final ConfigNamingDialogFragment fragment = new ConfigNamingDialogFragment();
fragment.setArguments(extras);
return fragment;
}
private void createTunnelAndDismiss() {
if (binding != null) {
final String name = binding.tunnelNameText.getText().toString();
Application.getTunnelManager().create(name, config).whenComplete((tunnel, throwable) -> {
if (tunnel != null) {
dismiss();
} else {
binding.tunnelNameTextLayout.setError(throwable.getMessage());
}
});
}
}
@Override
public void dismiss() {
setKeyboardVisible(false);
super.dismiss();
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Bundle arguments = getArguments();
final String configText = arguments.getString(KEY_CONFIG_TEXT);
final byte[] configBytes = configText.getBytes(StandardCharsets.UTF_8);
try {
config = Config.parse(new ByteArrayInputStream(configBytes));
} catch (final BadConfigException | IOException e) {
throw new IllegalArgumentException("Invalid config passed to " + getClass().getSimpleName(), e);
}
}
@Override
public Dialog onCreateDialog(final Bundle savedInstanceState) {
final Activity activity = Objects.requireNonNull(getActivity());
imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(activity);
alertDialogBuilder.setTitle(R.string.import_from_qr_code);
binding = ConfigNamingDialogFragmentBinding.inflate(activity.getLayoutInflater(), null, false);
binding.executePendingBindings();
alertDialogBuilder.setView(binding.getRoot());
alertDialogBuilder.setPositiveButton(R.string.create_tunnel, null);
alertDialogBuilder.setNegativeButton(R.string.cancel, (dialog, which) -> dismiss());
return alertDialogBuilder.create();
}
@Override public void onResume() {
super.onResume();
final AlertDialog dialog = (AlertDialog) getDialog();
if (dialog != null) {
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> createTunnelAndDismiss());
setKeyboardVisible(true);
}
}
private void setKeyboardVisible(final boolean visible) {
Objects.requireNonNull(imm);
if (visible) {
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
} else if (binding != null) {
imm.hideSoftInputFromWindow(binding.tunnelNameText.getWindowToken(), 0);
}
}
}
@@ -1,75 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment;
import android.os.Bundle;
import androidx.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import com.wireguard.android.R;
import com.wireguard.android.databinding.TunnelDetailFragmentBinding;
import com.wireguard.android.model.Tunnel;
/**
* Fragment that shows details about a specific tunnel.
*/
public class TunnelDetailFragment extends BaseFragment {
@Nullable private TunnelDetailFragmentBinding binding;
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
inflater.inflate(R.menu.tunnel_detail, menu);
}
@Override
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
binding = TunnelDetailFragmentBinding.inflate(inflater, container, false);
binding.executePendingBindings();
return binding.getRoot();
}
@Override
public void onDestroyView() {
binding = null;
super.onDestroyView();
}
@Override
public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
if (binding == null)
return;
binding.setTunnel(newTunnel);
if (newTunnel == null)
binding.setConfig(null);
else
newTunnel.getConfigAsync().thenAccept(binding::setConfig);
}
@Override
public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
if (binding == null) {
return;
}
binding.setFragment(this);
onSelectedTunnelChanged(null, getSelectedTunnel());
super.onViewStateRestored(savedInstanceState);
}
}
@@ -1,262 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment;
import android.app.Activity;
import android.content.Context;
import androidx.databinding.ObservableList;
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.google.android.material.snackbar.Snackbar;
import androidx.fragment.app.FragmentManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.databinding.TunnelEditorFragmentBinding;
import com.wireguard.android.fragment.AppListDialogFragment.AppExclusionListener;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.TunnelManager;
import com.wireguard.android.util.ErrorMessages;
import com.wireguard.android.viewmodel.ConfigProxy;
import com.wireguard.config.Config;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Fragment for editing a WireGuard configuration.
*/
public class TunnelEditorFragment extends BaseFragment implements AppExclusionListener {
private static final String KEY_LOCAL_CONFIG = "local_config";
private static final String KEY_ORIGINAL_NAME = "original_name";
private static final String TAG = "WireGuard/" + TunnelEditorFragment.class.getSimpleName();
@Nullable private TunnelEditorFragmentBinding binding;
@Nullable private Tunnel tunnel;
private void onConfigLoaded(final Config config) {
if (binding != null) {
binding.setConfig(new ConfigProxy(config));
}
}
private void onConfigSaved(final Tunnel savedTunnel,
@Nullable final Throwable throwable) {
final String message;
if (throwable == null) {
message = getString(R.string.config_save_success, savedTunnel.getName());
Log.d(TAG, message);
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
onFinished();
} else {
final String error = ErrorMessages.get(throwable);
message = getString(R.string.config_save_error, savedTunnel.getName(), error);
Log.e(TAG, message, throwable);
if (binding != null) {
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
}
}
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
inflater.inflate(R.menu.config_editor, menu);
}
@Override
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
binding = TunnelEditorFragmentBinding.inflate(inflater, container, false);
binding.executePendingBindings();
return binding.getRoot();
}
@Override
public void onDestroyView() {
binding = null;
super.onDestroyView();
}
@Override
public void onExcludedAppsSelected(final List<String> excludedApps) {
Objects.requireNonNull(binding, "Tried to set excluded apps while no view was loaded");
final ObservableList<String> excludedApplications =
binding.getConfig().getInterface().getExcludedApplications();
excludedApplications.clear();
excludedApplications.addAll(excludedApps);
}
private void onFinished() {
// Hide the keyboard; it rarely goes away on its own.
final Activity activity = getActivity();
if (activity == null) return;
final View focusedView = activity.getCurrentFocus();
if (focusedView != null) {
final Object service = activity.getSystemService(Context.INPUT_METHOD_SERVICE);
final InputMethodManager inputManager = (InputMethodManager) service;
if (inputManager != null)
inputManager.hideSoftInputFromWindow(focusedView.getWindowToken(),
InputMethodManager.HIDE_NOT_ALWAYS);
}
// Tell the activity to finish itself or go back to the detail view.
getActivity().runOnUiThread(() -> {
// TODO(smaeul): Remove this hack when fixing the Config ViewModel
// The selected tunnel has to actually change, but we have to remember this one.
final Tunnel savedTunnel = tunnel;
if (savedTunnel == getSelectedTunnel())
setSelectedTunnel(null);
setSelectedTunnel(savedTunnel);
});
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_action_save:
if (binding == null)
return false;
final Config newConfig;
try {
newConfig = binding.getConfig().resolve();
} catch (final Exception e) {
final String error = ErrorMessages.get(e);
final String tunnelName = tunnel == null ? binding.getName() : tunnel.getName();
final String message = getString(R.string.config_save_error, tunnelName, error);
Log.e(TAG, message, e);
Snackbar.make(binding.mainContainer, error, Snackbar.LENGTH_LONG).show();
return false;
}
if (tunnel == null) {
Log.d(TAG, "Attempting to create new tunnel " + binding.getName());
final TunnelManager manager = Application.getTunnelManager();
manager.create(binding.getName(), newConfig)
.whenComplete(this::onTunnelCreated);
} else if (!tunnel.getName().equals(binding.getName())) {
Log.d(TAG, "Attempting to rename tunnel to " + binding.getName());
tunnel.setName(binding.getName())
.whenComplete((a, b) -> onTunnelRenamed(tunnel, newConfig, b));
} else {
Log.d(TAG, "Attempting to save config of " + tunnel.getName());
tunnel.setConfig(newConfig)
.whenComplete((a, b) -> onConfigSaved(tunnel, b));
}
return true;
default:
return super.onOptionsItemSelected(item);
}
}
public void onRequestSetExcludedApplications(@SuppressWarnings("unused") final View view) {
final FragmentManager fragmentManager = getFragmentManager();
if (fragmentManager != null && binding != null) {
final ArrayList<String> excludedApps = new ArrayList<>(binding.getConfig().getInterface().getExcludedApplications());
final AppListDialogFragment fragment = AppListDialogFragment.newInstance(excludedApps, this);
fragment.show(fragmentManager, null);
}
}
@Override
public void onSaveInstanceState(final Bundle outState) {
if (binding != null)
outState.putParcelable(KEY_LOCAL_CONFIG, binding.getConfig());
outState.putString(KEY_ORIGINAL_NAME, tunnel == null ? null : tunnel.getName());
super.onSaveInstanceState(outState);
}
@Override
public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel,
@Nullable final Tunnel newTunnel) {
tunnel = newTunnel;
if (binding == null)
return;
binding.setConfig(new ConfigProxy());
if (tunnel != null) {
binding.setName(tunnel.getName());
tunnel.getConfigAsync().thenAccept(this::onConfigLoaded);
} else {
binding.setName("");
}
}
private void onTunnelCreated(final Tunnel newTunnel, @Nullable final Throwable throwable) {
final String message;
if (throwable == null) {
tunnel = newTunnel;
message = getString(R.string.tunnel_create_success, tunnel.getName());
Log.d(TAG, message);
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
onFinished();
} else {
final String error = ErrorMessages.get(throwable);
message = getString(R.string.tunnel_create_error, error);
Log.e(TAG, message, throwable);
if (binding != null) {
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
}
}
}
private void onTunnelRenamed(final Tunnel renamedTunnel, final Config newConfig,
@Nullable final Throwable throwable) {
final String message;
if (throwable == null) {
message = getString(R.string.tunnel_rename_success, renamedTunnel.getName());
Log.d(TAG, message);
// Now save the rest of configuration changes.
Log.d(TAG, "Attempting to save config of renamed tunnel " + tunnel.getName());
renamedTunnel.setConfig(newConfig).whenComplete((a, b) -> onConfigSaved(renamedTunnel, b));
} else {
final String error = ErrorMessages.get(throwable);
message = getString(R.string.tunnel_rename_error, error);
Log.e(TAG, message, throwable);
if (binding != null) {
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
}
}
}
@Override
public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
if (binding == null) {
return;
}
binding.setFragment(this);
if (savedInstanceState == null) {
onSelectedTunnelChanged(null, getSelectedTunnel());
} else {
tunnel = getSelectedTunnel();
final ConfigProxy config = savedInstanceState.getParcelable(KEY_LOCAL_CONFIG);
final String originalName = savedInstanceState.getString(KEY_ORIGINAL_NAME);
if (tunnel != null && !tunnel.getName().equals(originalName))
onSelectedTunnelChanged(null, tunnel);
else
binding.setConfig(config);
}
super.onViewStateRestored(savedInstanceState);
}
}
@@ -1,479 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.OpenableColumns;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.snackbar.Snackbar;
import androidx.fragment.app.FragmentManager;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.recyclerview.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.activity.TunnelCreatorActivity;
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter;
import com.wireguard.android.databinding.TunnelListFragmentBinding;
import com.wireguard.android.databinding.TunnelListItemBinding;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.util.ErrorMessages;
import com.wireguard.android.widget.MultiselectableRelativeLayout;
import com.wireguard.android.widget.fab.FloatingActionsMenuRecyclerViewScrollListener;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.Config;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java9.util.concurrent.CompletableFuture;
import java9.util.stream.StreamSupport;
/**
* Fragment containing a list of known WireGuard tunnels. It allows creating and deleting tunnels.
*/
public class TunnelListFragment extends BaseFragment {
private static final int REQUEST_IMPORT = 1;
private static final String TAG = "WireGuard/" + TunnelListFragment.class.getSimpleName();
private final ActionModeListener actionModeListener = new ActionModeListener();
@Nullable private ActionMode actionMode;
@Nullable private TunnelListFragmentBinding binding;
public boolean collapseActionMenu() {
if (binding != null && binding.createMenu.isExpanded()) {
binding.createMenu.collapse();
return true;
}
return false;
}
private void importTunnel(@NonNull final String configText) {
try {
// Ensure the config text is parseable before proceeding…
Config.parse(new ByteArrayInputStream(configText.getBytes(StandardCharsets.UTF_8)));
// Config text is valid, now create the tunnel…
final FragmentManager fragmentManager = getFragmentManager();
if (fragmentManager != null)
ConfigNamingDialogFragment.newInstance(configText).show(fragmentManager, null);
} catch (final BadConfigException | IOException e) {
onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(e));
}
}
private void importTunnel(@Nullable final Uri uri) {
final Activity activity = getActivity();
if (activity == null || uri == null)
return;
final ContentResolver contentResolver = activity.getContentResolver();
final Collection<CompletableFuture<Tunnel>> futureTunnels = new ArrayList<>();
final List<Throwable> throwables = new ArrayList<>();
Application.getAsyncWorker().supplyAsync(() -> {
final String[] columns = {OpenableColumns.DISPLAY_NAME};
String name = null;
try (Cursor cursor = contentResolver.query(uri, columns,
null, null, null)) {
if (cursor != null && cursor.moveToFirst() && !cursor.isNull(0))
name = cursor.getString(0);
}
if (name == null)
name = Uri.decode(uri.getLastPathSegment());
int idx = name.lastIndexOf('/');
if (idx >= 0) {
if (idx >= name.length() - 1)
throw new IllegalArgumentException(getResources().getString(R.string.illegal_filename_error, name));
name = name.substring(idx + 1);
}
boolean isZip = name.toLowerCase(Locale.ENGLISH).endsWith(".zip");
if (name.toLowerCase(Locale.ENGLISH).endsWith(".conf"))
name = name.substring(0, name.length() - ".conf".length());
else if (!isZip)
throw new IllegalArgumentException(getResources().getString(R.string.bad_extension_error));
if (isZip) {
try (ZipInputStream zip = new ZipInputStream(contentResolver.openInputStream(uri));
BufferedReader reader = new BufferedReader(new InputStreamReader(zip))) {
ZipEntry entry;
while ((entry = zip.getNextEntry()) != null) {
if (entry.isDirectory())
continue;
name = entry.getName();
idx = name.lastIndexOf('/');
if (idx >= 0) {
if (idx >= name.length() - 1)
continue;
name = name.substring(name.lastIndexOf('/') + 1);
}
if (name.toLowerCase(Locale.ENGLISH).endsWith(".conf"))
name = name.substring(0, name.length() - ".conf".length());
else
continue;
Config config = null;
try {
config = Config.parse(reader);
} catch (Exception e) {
throwables.add(e);
}
if (config != null)
futureTunnels.add(Application.getTunnelManager().create(name, config).toCompletableFuture());
}
}
} else {
futureTunnels.add(Application.getTunnelManager().create(name,
Config.parse(contentResolver.openInputStream(uri))).toCompletableFuture());
}
if (futureTunnels.isEmpty()) {
if (throwables.size() == 1)
throw throwables.get(0);
else if (throwables.isEmpty())
throw new IllegalArgumentException(getResources().getString(R.string.no_configs_error));
}
return CompletableFuture.allOf(futureTunnels.toArray(new CompletableFuture[futureTunnels.size()]));
}).whenComplete((future, exception) -> {
if (exception != null) {
onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(exception));
} else {
future.whenComplete((ignored1, ignored2) -> {
final List<Tunnel> tunnels = new ArrayList<>(futureTunnels.size());
for (final CompletableFuture<Tunnel> futureTunnel : futureTunnels) {
Tunnel tunnel = null;
try {
tunnel = futureTunnel.getNow(null);
} catch (final Exception e) {
throwables.add(e);
}
if (tunnel != null)
tunnels.add(tunnel);
}
onTunnelImportFinished(tunnels, throwables);
});
}
});
}
@Override
public void onActivityCreated(@Nullable final Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
final Collection<Integer> checkedItems = savedInstanceState.getIntegerArrayList("CHECKED_ITEMS");
if (checkedItems != null) {
for (final Integer i : checkedItems)
actionModeListener.setItemChecked(i, true);
}
}
}
@Override
public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
switch (requestCode) {
case REQUEST_IMPORT:
if (resultCode == Activity.RESULT_OK && data != null)
importTunnel(data.getData());
return;
case IntentIntegrator.REQUEST_CODE:
final IntentResult result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
if (result != null && result.getContents() != null) {
importTunnel(result.getContents());
}
return;
default:
super.onActivityResult(requestCode, resultCode, data);
}
}
@SuppressWarnings("deprecation")
@SuppressLint("ClickableViewAccessibility")
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
binding = TunnelListFragmentBinding.inflate(inflater, container, false);
binding.tunnelList.setOnTouchListener((view, motionEvent) -> {
if (binding != null) {
binding.createMenu.collapse();
}
return false;
});
binding.tunnelList.setOnScrollListener(new FloatingActionsMenuRecyclerViewScrollListener(binding.createMenu));
binding.executePendingBindings();
return binding.getRoot();
}
@Override
public void onDestroyView() {
binding = null;
super.onDestroyView();
}
@Override
public void onPause() {
if (binding != null) {
binding.createMenu.collapse();
}
super.onPause();
}
public void onRequestCreateConfig(@SuppressWarnings("unused") final View view) {
startActivity(new Intent(getActivity(), TunnelCreatorActivity.class));
if (binding != null)
binding.createMenu.collapse();
}
public void onRequestImportConfig(@SuppressWarnings("unused") final View view) {
final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
startActivityForResult(intent, REQUEST_IMPORT);
if (binding != null)
binding.createMenu.collapse();
}
public void onRequestScanQRCode(@SuppressWarnings("unused") final View view) {
final IntentIntegrator intentIntegrator = IntentIntegrator.forSupportFragment(this);
intentIntegrator.setOrientationLocked(false);
intentIntegrator.setBeepEnabled(false);
intentIntegrator.setPrompt(getString(R.string.qr_code_hint));
intentIntegrator.initiateScan(Collections.singletonList(IntentIntegrator.QR_CODE));
if (binding != null)
binding.createMenu.collapse();
}
@Override
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putIntegerArrayList("CHECKED_ITEMS", actionModeListener.getCheckedItems());
}
@Override
public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
if (binding == null)
return;
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
if (newTunnel != null)
viewForTunnel(newTunnel, tunnels).setSingleSelected(true);
if (oldTunnel != null)
viewForTunnel(oldTunnel, tunnels).setSingleSelected(false);
});
}
private void onTunnelDeletionFinished(final Integer count, @Nullable final Throwable throwable) {
final String message;
if (throwable == null) {
message = getResources().getQuantityString(R.plurals.delete_success, count, count);
} else {
final String error = ErrorMessages.get(throwable);
message = getResources().getQuantityString(R.plurals.delete_error, count, count, error);
Log.e(TAG, message, throwable);
}
if (binding != null) {
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
}
}
private void onTunnelImportFinished(final List<Tunnel> tunnels, final Collection<Throwable> throwables) {
String message = null;
for (final Throwable throwable : throwables) {
final String error = ErrorMessages.get(throwable);
message = getString(R.string.import_error, error);
Log.e(TAG, message, throwable);
}
if (tunnels.size() == 1 && throwables.isEmpty())
message = getString(R.string.import_success, tunnels.get(0).getName());
else if (tunnels.isEmpty() && throwables.size() == 1)
/* Use the exception message from above. */ ;
else if (throwables.isEmpty())
message = getResources().getQuantityString(R.plurals.import_total_success,
tunnels.size(), tunnels.size());
else if (!throwables.isEmpty())
message = getResources().getQuantityString(R.plurals.import_partial_success,
tunnels.size() + throwables.size(),
tunnels.size(), tunnels.size() + throwables.size());
if (binding != null)
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
}
@Override
public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if (binding == null) {
return;
}
binding.setFragment(this);
Application.getTunnelManager().getTunnels().thenAccept(binding::setTunnels);
binding.setRowConfigurationHandler((ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TunnelListItemBinding, Tunnel>) (binding, tunnel, position) -> {
binding.setFragment(this);
binding.getRoot().setOnClickListener(clicked -> {
if (actionMode == null) {
setSelectedTunnel(tunnel);
} else {
actionModeListener.toggleItemChecked(position);
}
});
binding.getRoot().setOnLongClickListener(clicked -> {
actionModeListener.toggleItemChecked(position);
return true;
});
if (actionMode != null)
((MultiselectableRelativeLayout) binding.getRoot()).setMultiSelected(actionModeListener.checkedItems.contains(position));
else
((MultiselectableRelativeLayout) binding.getRoot()).setSingleSelected(getSelectedTunnel() == tunnel);
});
}
private MultiselectableRelativeLayout viewForTunnel(final Tunnel tunnel, final List tunnels) {
return (MultiselectableRelativeLayout) binding.tunnelList.findViewHolderForAdapterPosition(tunnels.indexOf(tunnel)).itemView;
}
private final class ActionModeListener implements ActionMode.Callback {
private final Collection<Integer> checkedItems = new HashSet<>();
@Nullable private Resources resources;
public ArrayList<Integer> getCheckedItems() {
return new ArrayList<>(checkedItems);
}
@Override
public boolean onActionItemClicked(final ActionMode mode, final MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_action_delete:
final Iterable<Integer> copyCheckedItems = new HashSet<>(checkedItems);
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
final Collection<Tunnel> tunnelsToDelete = new ArrayList<>();
for (final Integer position : copyCheckedItems)
tunnelsToDelete.add(tunnels.get(position));
final CompletableFuture[] futures = StreamSupport.stream(tunnelsToDelete)
.map(Tunnel::delete)
.toArray(CompletableFuture[]::new);
CompletableFuture.allOf(futures)
.thenApply(x -> futures.length)
.whenComplete(TunnelListFragment.this::onTunnelDeletionFinished);
});
checkedItems.clear();
mode.finish();
return true;
case R.id.menu_action_select_all:
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
for (int i = 0; i < tunnels.size(); ++i) {
setItemChecked(i, true);
}
});
return true;
default:
return false;
}
}
@Override
public boolean onCreateActionMode(final ActionMode mode, final Menu menu) {
actionMode = mode;
if (getActivity() != null) {
resources = getActivity().getResources();
}
mode.getMenuInflater().inflate(R.menu.tunnel_list_action_mode, menu);
binding.tunnelList.getAdapter().notifyDataSetChanged();
return true;
}
@Override
public void onDestroyActionMode(final ActionMode mode) {
actionMode = null;
resources = null;
checkedItems.clear();
binding.tunnelList.getAdapter().notifyDataSetChanged();
}
@Override
public boolean onPrepareActionMode(final ActionMode mode, final Menu menu) {
updateTitle(mode);
return false;
}
void setItemChecked(final int position, final boolean checked) {
if (checked) {
checkedItems.add(position);
} else {
checkedItems.remove(position);
}
final RecyclerView.Adapter adapter = binding == null ? null : binding.tunnelList.getAdapter();
if (actionMode == null && !checkedItems.isEmpty() && getActivity() != null) {
((AppCompatActivity) getActivity()).startSupportActionMode(this);
} else if (actionMode != null && checkedItems.isEmpty()) {
actionMode.finish();
}
if (adapter != null)
adapter.notifyItemChanged(position);
updateTitle(actionMode);
}
void toggleItemChecked(final int position) {
setItemChecked(position, !checkedItems.contains(position));
}
private void updateTitle(@Nullable final ActionMode mode) {
if (mode == null) {
return;
}
final int count = checkedItems.size();
if (count == 0) {
mode.setTitle("");
} else {
mode.setTitle(resources.getQuantityString(R.plurals.delete_title, count, count));
}
}
}
}
@@ -1,54 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.model;
import androidx.databinding.BaseObservable;
import androidx.databinding.Bindable;
import android.graphics.drawable.Drawable;
import com.wireguard.android.BR;
import com.wireguard.util.Keyed;
public class ApplicationData extends BaseObservable implements Keyed<String> {
private final Drawable icon;
private final String name;
private final String packageName;
private boolean excludedFromTunnel;
public ApplicationData(final Drawable icon, final String name, final String packageName, final boolean excludedFromTunnel) {
this.icon = icon;
this.name = name;
this.packageName = packageName;
this.excludedFromTunnel = excludedFromTunnel;
}
public Drawable getIcon() {
return icon;
}
@Override
public String getKey() {
return name;
}
public String getName() {
return name;
}
public String getPackageName() {
return packageName;
}
@Bindable
public boolean isExcludedFromTunnel() {
return excludedFromTunnel;
}
public void setExcludedFromTunnel(final boolean excludedFromTunnel) {
this.excludedFromTunnel = excludedFromTunnel;
notifyPropertyChanged(BR.excludedFromTunnel);
}
}
@@ -1,158 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.model;
import androidx.databinding.BaseObservable;
import androidx.databinding.Bindable;
import androidx.annotation.Nullable;
import com.wireguard.android.BR;
import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.config.Config;
import com.wireguard.util.Keyed;
import java.util.regex.Pattern;
import java9.util.concurrent.CompletableFuture;
import java9.util.concurrent.CompletionStage;
/**
* Encapsulates the volatile and nonvolatile state of a WireGuard tunnel.
*/
public class Tunnel extends BaseObservable implements Keyed<String> {
public static final int NAME_MAX_LENGTH = 15;
private static final Pattern NAME_PATTERN = Pattern.compile("[a-zA-Z0-9_=+.-]{1,15}");
private final TunnelManager manager;
@Nullable private Config config;
private String name;
private State state;
@Nullable private Statistics statistics;
Tunnel(final TunnelManager manager, final String name,
@Nullable final Config config, final State state) {
this.manager = manager;
this.name = name;
this.config = config;
this.state = state;
}
public static boolean isNameInvalid(final CharSequence name) {
return !NAME_PATTERN.matcher(name).matches();
}
public CompletionStage<Void> delete() {
return manager.delete(this);
}
@Bindable
@Nullable
public Config getConfig() {
if (config == null)
manager.getTunnelConfig(this).whenComplete(ExceptionLoggers.E);
return config;
}
public CompletionStage<Config> getConfigAsync() {
if (config == null)
return manager.getTunnelConfig(this);
return CompletableFuture.completedFuture(config);
}
@Override
public String getKey() {
return name;
}
@Bindable
public String getName() {
return name;
}
@Bindable
public State getState() {
return state;
}
public CompletionStage<State> getStateAsync() {
return TunnelManager.getTunnelState(this);
}
@Bindable
@Nullable
public Statistics getStatistics() {
// FIXME: Check age of statistics.
if (statistics == null)
TunnelManager.getTunnelStatistics(this).whenComplete(ExceptionLoggers.E);
return statistics;
}
public CompletionStage<Statistics> getStatisticsAsync() {
// FIXME: Check age of statistics.
if (statistics == null)
return TunnelManager.getTunnelStatistics(this);
return CompletableFuture.completedFuture(statistics);
}
Config onConfigChanged(final Config config) {
this.config = config;
notifyPropertyChanged(BR.config);
return config;
}
public String onNameChanged(final String name) {
this.name = name;
notifyPropertyChanged(BR.name);
return name;
}
State onStateChanged(final State state) {
if (state != State.UP)
onStatisticsChanged(null);
this.state = state;
notifyPropertyChanged(BR.state);
return state;
}
@Nullable
Statistics onStatisticsChanged(@Nullable final Statistics statistics) {
this.statistics = statistics;
notifyPropertyChanged(BR.statistics);
return statistics;
}
public CompletionStage<Config> setConfig(final Config config) {
if (!config.equals(this.config))
return manager.setTunnelConfig(this, config);
return CompletableFuture.completedFuture(this.config);
}
public CompletionStage<String> setName(final String name) {
if (!name.equals(this.name))
return manager.setTunnelName(this, name);
return CompletableFuture.completedFuture(this.name);
}
public CompletionStage<State> setState(final State state) {
if (state != this.state)
return manager.setTunnelState(this, state);
return CompletableFuture.completedFuture(this.state);
}
public enum State {
DOWN,
TOGGLE,
UP;
public static State of(final boolean running) {
return running ? UP : DOWN;
}
}
public static class Statistics extends BaseObservable {
}
}
@@ -1,300 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.model;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import androidx.databinding.BaseObservable;
import androidx.databinding.Bindable;
import androidx.annotation.Nullable;
import com.wireguard.android.Application;
import com.wireguard.android.BR;
import com.wireguard.android.R;
import com.wireguard.android.configStore.ConfigStore;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.model.Tunnel.Statistics;
import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.android.util.ObservableSortedKeyedArrayList;
import com.wireguard.android.util.ObservableSortedKeyedList;
import com.wireguard.config.Config;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Set;
import java9.util.Comparators;
import java9.util.concurrent.CompletableFuture;
import java9.util.concurrent.CompletionStage;
import java9.util.stream.Collectors;
import java9.util.stream.StreamSupport;
/**
* Maintains and mediates changes to the set of available WireGuard tunnels,
*/
public final class TunnelManager extends BaseObservable {
private static final Comparator<String> COMPARATOR = Comparators.<String>thenComparing(
String.CASE_INSENSITIVE_ORDER, Comparators.naturalOrder());
private static final String KEY_LAST_USED_TUNNEL = "last_used_tunnel";
private static final String KEY_RESTORE_ON_BOOT = "restore_on_boot";
private static final String KEY_RUNNING_TUNNELS = "enabled_configs";
private final CompletableFuture<ObservableSortedKeyedList<String, Tunnel>> completableTunnels = new CompletableFuture<>();
private final ConfigStore configStore;
private final Context context = Application.get();
private final ArrayList<CompletableFuture<Void>> delayedLoadRestoreTunnels = new ArrayList<>();
private final ObservableSortedKeyedList<String, Tunnel> tunnels = new ObservableSortedKeyedArrayList<>(COMPARATOR);
private boolean haveLoaded;
@Nullable private Tunnel lastUsedTunnel;
public TunnelManager(final ConfigStore configStore) {
this.configStore = configStore;
}
static CompletionStage<State> getTunnelState(final Tunnel tunnel) {
return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getState(tunnel))
.thenApply(tunnel::onStateChanged);
}
static CompletionStage<Statistics> getTunnelStatistics(final Tunnel tunnel) {
return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getStatistics(tunnel))
.thenApply(tunnel::onStatisticsChanged);
}
private Tunnel addToList(final String name, @Nullable final Config config, final State state) {
final Tunnel tunnel = new Tunnel(this, name, config, state);
tunnels.add(tunnel);
return tunnel;
}
public CompletionStage<Tunnel> create(final String name, @Nullable final Config config) {
if (Tunnel.isNameInvalid(name))
return CompletableFuture.failedFuture(new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)));
if (tunnels.containsKey(name)) {
final String message = context.getString(R.string.tunnel_error_already_exists, name);
return CompletableFuture.failedFuture(new IllegalArgumentException(message));
}
return Application.getAsyncWorker().supplyAsync(() -> configStore.create(name, config))
.thenApply(savedConfig -> addToList(name, savedConfig, State.DOWN));
}
CompletionStage<Void> delete(final Tunnel tunnel) {
final State originalState = tunnel.getState();
final boolean wasLastUsed = tunnel == lastUsedTunnel;
// Make sure nothing touches the tunnel.
if (wasLastUsed)
setLastUsedTunnel(null);
tunnels.remove(tunnel);
return Application.getAsyncWorker().runAsync(() -> {
if (originalState == State.UP)
Application.getBackend().setState(tunnel, State.DOWN);
try {
configStore.delete(tunnel.getName());
} catch (final Exception e) {
if (originalState == State.UP)
Application.getBackend().setState(tunnel, State.UP);
// Re-throw the exception to fail the completion.
throw e;
}
}).whenComplete((x, e) -> {
if (e == null)
return;
// Failure, put the tunnel back.
tunnels.add(tunnel);
if (wasLastUsed)
setLastUsedTunnel(tunnel);
});
}
@Bindable
@Nullable
public Tunnel getLastUsedTunnel() {
return lastUsedTunnel;
}
CompletionStage<Config> getTunnelConfig(final Tunnel tunnel) {
return Application.getAsyncWorker().supplyAsync(() -> configStore.load(tunnel.getName()))
.thenApply(tunnel::onConfigChanged);
}
public CompletableFuture<ObservableSortedKeyedList<String, Tunnel>> getTunnels() {
return completableTunnels;
}
public void onCreate() {
Application.getAsyncWorker().supplyAsync(configStore::enumerate)
.thenAcceptBoth(Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().enumerate()), this::onTunnelsLoaded)
.whenComplete(ExceptionLoggers.E);
}
@SuppressWarnings("unchecked")
private void onTunnelsLoaded(final Iterable<String> present, final Collection<String> running) {
for (final String name : present)
addToList(name, null, running.contains(name) ? State.UP : State.DOWN);
final String lastUsedName = Application.getSharedPreferences().getString(KEY_LAST_USED_TUNNEL, null);
if (lastUsedName != null)
setLastUsedTunnel(tunnels.get(lastUsedName));
final CompletableFuture<Void>[] toComplete;
synchronized (delayedLoadRestoreTunnels) {
haveLoaded = true;
toComplete = delayedLoadRestoreTunnels.toArray(new CompletableFuture[delayedLoadRestoreTunnels.size()]);
delayedLoadRestoreTunnels.clear();
}
restoreState(true).whenComplete((v, t) -> {
for (final CompletableFuture<Void> f : toComplete) {
if (t == null)
f.complete(v);
else
f.completeExceptionally(t);
}
});
completableTunnels.complete(tunnels);
}
public void refreshTunnelStates() {
Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().enumerate())
.thenAccept(running -> {
for (final Tunnel tunnel : tunnels)
tunnel.onStateChanged(running.contains(tunnel.getName()) ? State.UP : State.DOWN);
})
.whenComplete(ExceptionLoggers.E);
}
public CompletionStage<Void> restoreState(final boolean force) {
if (!force && !Application.getSharedPreferences().getBoolean(KEY_RESTORE_ON_BOOT, false))
return CompletableFuture.completedFuture(null);
synchronized (delayedLoadRestoreTunnels) {
if (!haveLoaded) {
final CompletableFuture<Void> f = new CompletableFuture<>();
delayedLoadRestoreTunnels.add(f);
return f;
}
}
final Set<String> previouslyRunning = Application.getSharedPreferences().getStringSet(KEY_RUNNING_TUNNELS, null);
if (previouslyRunning == null)
return CompletableFuture.completedFuture(null);
return CompletableFuture.allOf(StreamSupport.stream(tunnels)
.filter(tunnel -> previouslyRunning.contains(tunnel.getName()))
.map(tunnel -> setTunnelState(tunnel, State.UP))
.toArray(CompletableFuture[]::new));
}
public void saveState() {
final Set<String> runningTunnels = StreamSupport.stream(tunnels)
.filter(tunnel -> tunnel.getState() == State.UP)
.map(Tunnel::getName)
.collect(Collectors.toUnmodifiableSet());
Application.getSharedPreferences().edit().putStringSet(KEY_RUNNING_TUNNELS, runningTunnels).apply();
}
private void setLastUsedTunnel(@Nullable final Tunnel tunnel) {
if (tunnel == lastUsedTunnel)
return;
lastUsedTunnel = tunnel;
notifyPropertyChanged(BR.lastUsedTunnel);
if (tunnel != null)
Application.getSharedPreferences().edit().putString(KEY_LAST_USED_TUNNEL, tunnel.getName()).apply();
else
Application.getSharedPreferences().edit().remove(KEY_LAST_USED_TUNNEL).apply();
}
CompletionStage<Config> setTunnelConfig(final Tunnel tunnel, final Config config) {
return Application.getAsyncWorker().supplyAsync(() -> {
final Config appliedConfig = Application.getBackend().applyConfig(tunnel, config);
return configStore.save(tunnel.getName(), appliedConfig);
}).thenApply(tunnel::onConfigChanged);
}
CompletionStage<String> setTunnelName(final Tunnel tunnel, final String name) {
if (Tunnel.isNameInvalid(name))
return CompletableFuture.failedFuture(new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)));
if (tunnels.containsKey(name)) {
final String message = context.getString(R.string.tunnel_error_already_exists, name);
return CompletableFuture.failedFuture(new IllegalArgumentException(message));
}
final State originalState = tunnel.getState();
final boolean wasLastUsed = tunnel == lastUsedTunnel;
// Make sure nothing touches the tunnel.
if (wasLastUsed)
setLastUsedTunnel(null);
tunnels.remove(tunnel);
return Application.getAsyncWorker().supplyAsync(() -> {
if (originalState == State.UP)
Application.getBackend().setState(tunnel, State.DOWN);
configStore.rename(tunnel.getName(), name);
final String newName = tunnel.onNameChanged(name);
if (originalState == State.UP)
Application.getBackend().setState(tunnel, State.UP);
return newName;
}).whenComplete((newName, e) -> {
// On failure, we don't know what state the tunnel might be in. Fix that.
if (e != null)
getTunnelState(tunnel);
// Add the tunnel back to the manager, under whatever name it thinks it has.
tunnels.add(tunnel);
if (wasLastUsed)
setLastUsedTunnel(tunnel);
});
}
CompletionStage<State> setTunnelState(final Tunnel tunnel, final State state) {
// Ensure the configuration is loaded before trying to use it.
return tunnel.getConfigAsync().thenCompose(x ->
Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().setState(tunnel, state))
).whenComplete((newState, e) -> {
// Ensure onStateChanged is always called (failure or not), and with the correct state.
tunnel.onStateChanged(e == null ? newState : tunnel.getState());
if (e == null && newState == State.UP)
setLastUsedTunnel(tunnel);
saveState();
});
}
public static final class IntentReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context context, @Nullable final Intent intent) {
final TunnelManager manager = Application.getTunnelManager();
if (intent == null)
return;
final String action = intent.getAction();
if (action == null)
return;
if ("com.wireguard.android.action.REFRESH_TUNNEL_STATES".equals(action)) {
manager.refreshTunnelStates();
return;
}
/* We disable the below, for now, as the security model of allowing this
* might take a bit more consideration.
*/
if (true)
return;
final State state;
if ("com.wireguard.android.action.SET_TUNNEL_UP".equals(action))
state = State.UP;
else if ("com.wireguard.android.action.SET_TUNNEL_DOWN".equals(action))
state = State.DOWN;
else
return;
final String tunnelName = intent.getStringExtra("tunnel");
if (tunnelName == null)
return;
manager.getTunnels().thenAccept(tunnels -> {
final Tunnel tunnel = tunnels.get(tunnelName);
if (tunnel == null)
return;
manager.setTunnelState(tunnel, state);
});
}
}
}
@@ -1,44 +0,0 @@
/*
* Copyright © 2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.AttributeSet;
import com.wireguard.android.Application;
import com.wireguard.android.BuildConfig;
import com.wireguard.android.R;
import java.util.Locale;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
public class DonatePreference extends Preference {
public DonatePreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
@Override
public CharSequence getSummary() { return getContext().getString(R.string.donate_summary); }
@Override
public CharSequence getTitle() { return getContext().getString(R.string.donate_title); }
@Override
protected void onClick() {
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://www.wireguard.com/donations/"));
try {
getContext().startActivity(intent);
} catch (final ActivityNotFoundException ignored) {
}
}
}
@@ -1,112 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import androidx.annotation.Nullable;
import com.google.android.material.snackbar.Snackbar;
import androidx.preference.Preference;
import android.util.AttributeSet;
import android.util.Log;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.util.DownloadsFileSaver;
import com.wireguard.android.util.DownloadsFileSaver.DownloadsFile;
import com.wireguard.android.util.ErrorMessages;
import com.wireguard.android.util.FragmentUtils;
import java.io.BufferedReader;
import java.io.InputStreamReader;
/**
* Preference implementing a button that asynchronously exports logs.
*/
public class LogExporterPreference extends Preference {
private static final String TAG = "WireGuard/" + LogExporterPreference.class.getSimpleName();
@Nullable private String exportedFilePath;
public LogExporterPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
private void exportLog() {
Application.getAsyncWorker().supplyAsync(() -> {
DownloadsFile outputFile = DownloadsFileSaver.save(getContext(), "wireguard-log.txt", "text/plain", true);
try {
final Process process = Runtime.getRuntime().exec(new String[]{
"logcat", "-b", "all", "-d", "-v", "threadtime", "*:V"});
try (final BufferedReader stdout = new BufferedReader(new InputStreamReader(process.getInputStream()));
final BufferedReader stderr = new BufferedReader(new InputStreamReader(process.getErrorStream())))
{
String line;
while ((line = stdout.readLine()) != null) {
outputFile.getOutputStream().write(line.getBytes());
outputFile.getOutputStream().write('\n');
}
outputFile.getOutputStream().close();
stdout.close();
if (process.waitFor() != 0) {
final StringBuilder errors = new StringBuilder();
errors.append(R.string.logcat_error);
while ((line = stderr.readLine()) != null)
errors.append(line);
throw new Exception(errors.toString());
}
}
} catch (final Exception e) {
outputFile.delete();
throw e;
}
return outputFile.getFileName();
}).whenComplete(this::exportLogComplete);
}
private void exportLogComplete(final String filePath, @Nullable final Throwable throwable) {
if (throwable != null) {
final String error = ErrorMessages.get(throwable);
final String message = getContext().getString(R.string.log_export_error, error);
Log.e(TAG, message, throwable);
Snackbar.make(
FragmentUtils.getPrefActivity(this).findViewById(android.R.id.content),
message, Snackbar.LENGTH_LONG).show();
setEnabled(true);
} else {
exportedFilePath = filePath;
notifyChanged();
}
}
@Override
public CharSequence getSummary() {
return exportedFilePath == null ?
getContext().getString(R.string.log_export_summary) :
getContext().getString(R.string.log_export_success, exportedFilePath);
}
@Override
public CharSequence getTitle() {
return getContext().getString(R.string.log_export_title);
}
@Override
protected void onClick() {
FragmentUtils.getPrefActivity(this).ensurePermissions(
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
(permissions, granted) -> {
if (granted.length > 0 && granted[0] == PackageManager.PERMISSION_GRANTED) {
setEnabled(false);
exportLog();
}
});
}
}
@@ -1,102 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference;
import android.content.Context;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import android.util.AttributeSet;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.util.ToolsInstaller;
/**
* Preference implementing a button that asynchronously runs {@code ToolsInstaller} and displays the
* result as the preference summary.
*/
public class ToolsInstallerPreference extends Preference {
private State state = State.INITIAL;
public ToolsInstallerPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
@Override
public CharSequence getSummary() {
return getContext().getString(state.messageResourceId);
}
@Override
public CharSequence getTitle() {
return getContext().getString(R.string.tools_installer_title);
}
@Override
public void onAttached() {
super.onAttached();
Application.getAsyncWorker().supplyAsync(Application.getToolsInstaller()::areInstalled).whenComplete(this::onCheckResult);
}
private void onCheckResult(final int state, @Nullable final Throwable throwable) {
if (throwable != null || state == ToolsInstaller.ERROR)
setState(State.INITIAL);
else if ((state & ToolsInstaller.YES) == ToolsInstaller.YES)
setState(State.ALREADY);
else if ((state & (ToolsInstaller.MAGISK | ToolsInstaller.NO)) == (ToolsInstaller.MAGISK | ToolsInstaller.NO))
setState(State.INITIAL_MAGISK);
else if ((state & (ToolsInstaller.SYSTEM | ToolsInstaller.NO)) == (ToolsInstaller.SYSTEM | ToolsInstaller.NO))
setState(State.INITIAL_SYSTEM);
else
setState(State.INITIAL);
}
@Override
protected void onClick() {
setState(State.WORKING);
Application.getAsyncWorker().supplyAsync(Application.getToolsInstaller()::install).whenComplete(this::onInstallResult);
}
private void onInstallResult(final Integer result, @Nullable final Throwable throwable) {
if (throwable != null)
setState(State.FAILURE);
else if ((result & (ToolsInstaller.YES | ToolsInstaller.MAGISK)) == (ToolsInstaller.YES | ToolsInstaller.MAGISK))
setState(State.SUCCESS_MAGISK);
else if ((result & (ToolsInstaller.YES | ToolsInstaller.SYSTEM)) == (ToolsInstaller.YES | ToolsInstaller.SYSTEM))
setState(State.SUCCESS_SYSTEM);
else
setState(State.FAILURE);
}
private void setState(final State state) {
if (this.state == state)
return;
this.state = state;
if (isEnabled() != state.shouldEnableView)
setEnabled(state.shouldEnableView);
notifyChanged();
}
private enum State {
INITIAL(R.string.tools_installer_initial, true),
ALREADY(R.string.tools_installer_already, false),
FAILURE(R.string.tools_installer_failure, true),
WORKING(R.string.tools_installer_working, false),
INITIAL_SYSTEM(R.string.tools_installer_initial_system, true),
SUCCESS_SYSTEM(R.string.tools_installer_success_system, false),
INITIAL_MAGISK(R.string.tools_installer_initial_magisk, true),
SUCCESS_MAGISK(R.string.tools_installer_success_magisk, false);
private final int messageResourceId;
private final boolean shouldEnableView;
State(final int messageResourceId, final boolean shouldEnableView) {
this.messageResourceId = messageResourceId;
this.shouldEnableView = shouldEnableView;
}
}
}
@@ -1,60 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import android.util.AttributeSet;
import com.wireguard.android.Application;
import com.wireguard.android.BuildConfig;
import com.wireguard.android.R;
import java.util.Locale;
public class VersionPreference extends Preference {
@Nullable private String versionSummary;
public VersionPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
Application.getBackendAsync().thenAccept(backend -> {
versionSummary = getContext().getString(R.string.version_summary_checking, backend.getTypePrettyName().toLowerCase(Locale.ENGLISH));
Application.getAsyncWorker().supplyAsync(backend::getVersion).whenComplete((version, exception) -> {
versionSummary = exception == null
? getContext().getString(R.string.version_summary, backend.getTypePrettyName(), version)
: getContext().getString(R.string.version_summary_unknown, backend.getTypePrettyName().toLowerCase(Locale.ENGLISH));
notifyChanged();
});
});
}
@Nullable
@Override
public CharSequence getSummary() {
return versionSummary;
}
@Override
public CharSequence getTitle() {
return getContext().getString(R.string.version_title, BuildConfig.VERSION_NAME);
}
@Override
protected void onClick() {
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://www.wireguard.com/"));
try {
getContext().startActivity(intent);
} catch (final ActivityNotFoundException ignored) {
}
}
}
@@ -1,119 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import androidx.annotation.Nullable;
import com.google.android.material.snackbar.Snackbar;
import androidx.preference.Preference;
import android.util.AttributeSet;
import android.util.Log;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.util.DownloadsFileSaver;
import com.wireguard.android.util.DownloadsFileSaver.DownloadsFile;
import com.wireguard.android.util.ErrorMessages;
import com.wireguard.android.util.FragmentUtils;
import com.wireguard.config.Config;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import java9.util.concurrent.CompletableFuture;
/**
* Preference implementing a button that asynchronously exports config zips.
*/
public class ZipExporterPreference extends Preference {
private static final String TAG = "WireGuard/" + ZipExporterPreference.class.getSimpleName();
@Nullable private String exportedFilePath;
public ZipExporterPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
private void exportZip() {
Application.getTunnelManager().getTunnels().thenAccept(this::exportZip);
}
private void exportZip(final List<Tunnel> tunnels) {
final List<CompletableFuture<Config>> futureConfigs = new ArrayList<>(tunnels.size());
for (final Tunnel tunnel : tunnels)
futureConfigs.add(tunnel.getConfigAsync().toCompletableFuture());
if (futureConfigs.isEmpty()) {
exportZipComplete(null, new IllegalArgumentException(
getContext().getString(R.string.no_tunnels_error)));
return;
}
CompletableFuture.allOf(futureConfigs.toArray(new CompletableFuture[futureConfigs.size()]))
.whenComplete((ignored1, exception) -> Application.getAsyncWorker().supplyAsync(() -> {
if (exception != null)
throw exception;
DownloadsFile outputFile = DownloadsFileSaver.save(getContext(), "wireguard-export.zip", "application/zip", true);
try (ZipOutputStream zip = new ZipOutputStream(outputFile.getOutputStream())) {
for (int i = 0; i < futureConfigs.size(); ++i) {
zip.putNextEntry(new ZipEntry(tunnels.get(i).getName() + ".conf"));
zip.write(futureConfigs.get(i).getNow(null).
toWgQuickString().getBytes(StandardCharsets.UTF_8));
}
zip.closeEntry();
} catch (final Exception e) {
outputFile.delete();
throw e;
}
return outputFile.getFileName();
}).whenComplete(this::exportZipComplete));
}
private void exportZipComplete(@Nullable final String filePath, @Nullable final Throwable throwable) {
if (throwable != null) {
final String error = ErrorMessages.get(throwable);
final String message = getContext().getString(R.string.zip_export_error, error);
Log.e(TAG, message, throwable);
Snackbar.make(
FragmentUtils.getPrefActivity(this).findViewById(android.R.id.content),
message, Snackbar.LENGTH_LONG).show();
setEnabled(true);
} else {
exportedFilePath = filePath;
notifyChanged();
}
}
@Override
public CharSequence getSummary() {
return exportedFilePath == null ?
getContext().getString(R.string.zip_export_summary) :
getContext().getString(R.string.zip_export_success, exportedFilePath);
}
@Override
public CharSequence getTitle() {
return getContext().getString(R.string.zip_export_title);
}
@Override
protected void onClick() {
FragmentUtils.getPrefActivity(this).ensurePermissions(
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
(permissions, granted) -> {
if (granted.length > 0 && granted[0] == PackageManager.PERMISSION_GRANTED) {
setEnabled(false);
exportZip();
}
});
}
}
@@ -1,63 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.os.Handler;
import java.util.concurrent.Executor;
import java9.util.concurrent.CompletableFuture;
import java9.util.concurrent.CompletionStage;
/**
* Helper class for running asynchronous tasks and ensuring they are completed on the main thread.
*/
public class AsyncWorker {
private final Executor executor;
private final Handler handler;
public AsyncWorker(final Executor executor, final Handler handler) {
this.executor = executor;
this.handler = handler;
}
public CompletionStage<Void> runAsync(final AsyncRunnable<?> runnable) {
final CompletableFuture<Void> future = new CompletableFuture<>();
executor.execute(() -> {
try {
runnable.run();
handler.post(() -> future.complete(null));
} catch (final Throwable t) {
handler.post(() -> future.completeExceptionally(t));
}
});
return future;
}
public <T> CompletionStage<T> supplyAsync(final AsyncSupplier<T, ?> supplier) {
final CompletableFuture<T> future = new CompletableFuture<>();
executor.execute(() -> {
try {
final T result = supplier.get();
handler.post(() -> future.complete(result));
} catch (final Throwable t) {
handler.post(() -> future.completeExceptionally(t));
}
});
return future;
}
@FunctionalInterface
public interface AsyncRunnable<E extends Throwable> {
void run() throws E;
}
@FunctionalInterface
public interface AsyncSupplier<T, E extends Throwable> {
T get() throws E;
}
}
@@ -1,37 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import com.google.android.material.snackbar.Snackbar;
import android.view.View;
import android.widget.TextView;
/**
* Standalone utilities for interacting with the system clipboard.
*/
public final class ClipboardUtils {
private ClipboardUtils() {
// Prevent instantiation
}
public static void copyTextView(final View view) {
if (!(view instanceof TextView))
return;
final CharSequence text = ((TextView) view).getText();
if (text == null || text.length() == 0)
return;
final Object service = view.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
if (!(service instanceof ClipboardManager))
return;
final CharSequence description = view.getContentDescription();
((ClipboardManager) service).setPrimaryClip(ClipData.newPlainText(description, text));
Snackbar.make(view, description + " copied to clipboard", Snackbar.LENGTH_LONG).show();
}
}
@@ -1,96 +0,0 @@
/*
* Copyright © 2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import android.provider.MediaStore.MediaColumns;
import com.wireguard.android.R;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class DownloadsFileSaver {
public static class DownloadsFile {
private Context context;
private OutputStream outputStream;
private String fileName;
private Uri uri;
private DownloadsFile(final Context context, final OutputStream outputStream, final String fileName, final Uri uri) {
this.context = context;
this.outputStream = outputStream;
this.fileName = fileName;
this.uri = uri;
}
public OutputStream getOutputStream() { return outputStream; }
public String getFileName() { return fileName; }
public void delete() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
context.getContentResolver().delete(uri, null, null);
else
new File(fileName).delete();
}
}
public static DownloadsFile save(final Context context, final String name, final String mimeType, final boolean overwriteExisting) throws Exception {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
final ContentResolver contentResolver = context.getContentResolver();
if (overwriteExisting)
contentResolver.delete(MediaStore.Downloads.EXTERNAL_CONTENT_URI, String.format("%s = ?", MediaColumns.DISPLAY_NAME), new String[]{name});
final ContentValues contentValues = new ContentValues();
contentValues.put(MediaColumns.DISPLAY_NAME, name);
contentValues.put(MediaColumns.MIME_TYPE, mimeType);
final Uri contentUri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues);
if (contentUri == null)
throw new IOException(context.getString(R.string.create_downloads_file_error));
final OutputStream contentStream = contentResolver.openOutputStream(contentUri);
if (contentStream == null)
throw new IOException(context.getString(R.string.create_downloads_file_error));
Cursor cursor = contentResolver.query(contentUri, new String[]{MediaColumns.DATA}, null, null, null);
String path = null;
if (cursor != null) {
try {
if (cursor.moveToFirst())
path = cursor.getString(0);
} finally {
cursor.close();
}
}
if (path == null) {
path = "Download/";
cursor = contentResolver.query(contentUri, new String[]{MediaColumns.DISPLAY_NAME}, null, null, null);
if (cursor != null) {
try {
if (cursor.moveToFirst())
path += cursor.getString(0);
} finally {
cursor.close();
}
}
}
return new DownloadsFile(context, contentStream, path, contentUri);
} else {
final File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
final File file = new File(path, name);
if (!path.isDirectory() && !path.mkdirs())
throw new IOException(context.getString(R.string.create_output_dir_error));
return new DownloadsFile(context, new FileOutputStream(file), file.getAbsolutePath(), null);
}
}
}
@@ -1,130 +0,0 @@
/*
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.content.res.Resources;
import androidx.annotation.Nullable;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.BadConfigException.Location;
import com.wireguard.config.BadConfigException.Reason;
import com.wireguard.config.InetEndpoint;
import com.wireguard.config.InetNetwork;
import com.wireguard.config.ParseException;
import com.wireguard.crypto.Key.Format;
import com.wireguard.crypto.KeyFormatException;
import com.wireguard.crypto.KeyFormatException.Type;
import java.net.InetAddress;
import java.util.EnumMap;
import java.util.Map;
import java9.util.Maps;
public final class ErrorMessages {
private static final Map<Reason, Integer> BCE_REASON_MAP = new EnumMap<>(Maps.of(
Reason.INVALID_KEY, R.string.bad_config_reason_invalid_key,
Reason.INVALID_NUMBER, R.string.bad_config_reason_invalid_number,
Reason.INVALID_VALUE, R.string.bad_config_reason_invalid_value,
Reason.MISSING_ATTRIBUTE, R.string.bad_config_reason_missing_attribute,
Reason.MISSING_SECTION, R.string.bad_config_reason_missing_section,
Reason.MISSING_VALUE, R.string.bad_config_reason_missing_value,
Reason.SYNTAX_ERROR, R.string.bad_config_reason_syntax_error,
Reason.UNKNOWN_ATTRIBUTE, R.string.bad_config_reason_unknown_attribute,
Reason.UNKNOWN_SECTION, R.string.bad_config_reason_unknown_section
));
private static final Map<Format, Integer> KFE_FORMAT_MAP = new EnumMap<>(Maps.of(
Format.BASE64, R.string.key_length_explanation_base64,
Format.BINARY, R.string.key_length_explanation_binary,
Format.HEX, R.string.key_length_explanation_hex
));
private static final Map<Type, Integer> KFE_TYPE_MAP = new EnumMap<>(Maps.of(
Type.CONTENTS, R.string.key_contents_error,
Type.LENGTH, R.string.key_length_error
));
private static final Map<Class, Integer> PE_CLASS_MAP = Maps.of(
InetAddress.class, R.string.parse_error_inet_address,
InetEndpoint.class, R.string.parse_error_inet_endpoint,
InetNetwork.class, R.string.parse_error_inet_network,
Integer.class, R.string.parse_error_integer
);
private ErrorMessages() {
// Prevent instantiation
}
public static String get(@Nullable final Throwable throwable) {
final Resources resources = Application.get().getResources();
if (throwable == null)
return resources.getString(R.string.unknown_error);
final Throwable rootCause = rootCause(throwable);
final String message;
if (rootCause instanceof BadConfigException) {
final BadConfigException bce = (BadConfigException) rootCause;
final String reason = getBadConfigExceptionReason(resources, bce);
final String context = bce.getLocation() == Location.TOP_LEVEL ?
resources.getString(R.string.bad_config_context_top_level,
bce.getSection().getName()) :
resources.getString(R.string.bad_config_context,
bce.getSection().getName(),
bce.getLocation().getName());
final String explanation = getBadConfigExceptionExplanation(resources, bce);
message = resources.getString(R.string.bad_config_error, reason, context) + explanation;
} else if (rootCause.getMessage() != null) {
message = rootCause.getMessage();
} else {
final String errorType = rootCause.getClass().getSimpleName();
message = resources.getString(R.string.generic_error, errorType);
}
return message;
}
private static String getBadConfigExceptionExplanation(final Resources resources,
final BadConfigException bce) {
if (bce.getCause() instanceof KeyFormatException) {
final KeyFormatException kfe = (KeyFormatException) bce.getCause();
if (kfe.getType() == Type.LENGTH)
return resources.getString(KFE_FORMAT_MAP.get(kfe.getFormat()));
} else if (bce.getCause() instanceof ParseException) {
final ParseException pe = (ParseException) bce.getCause();
if (pe.getMessage() != null)
return ": " + pe.getMessage();
} else if (bce.getLocation() == Location.LISTEN_PORT) {
return resources.getString(R.string.bad_config_explanation_udp_port);
} else if (bce.getLocation() == Location.MTU) {
return resources.getString(R.string.bad_config_explanation_positive_number);
} else if (bce.getLocation() == Location.PERSISTENT_KEEPALIVE) {
return resources.getString(R.string.bad_config_explanation_pka);
}
return "";
}
private static String getBadConfigExceptionReason(final Resources resources,
final BadConfigException bce) {
if (bce.getCause() instanceof KeyFormatException) {
final KeyFormatException kfe = (KeyFormatException) bce.getCause();
return resources.getString(KFE_TYPE_MAP.get(kfe.getType()));
} else if (bce.getCause() instanceof ParseException) {
final ParseException pe = (ParseException) bce.getCause();
final String type = resources.getString(PE_CLASS_MAP.containsKey(pe.getParsingClass()) ?
PE_CLASS_MAP.get(pe.getParsingClass()) : R.string.parse_error_generic);
return resources.getString(R.string.parse_error_reason, type, pe.getText());
}
return resources.getString(BCE_REASON_MAP.get(bce.getReason()), bce.getText());
}
private static Throwable rootCause(final Throwable throwable) {
Throwable cause = throwable;
while (cause.getCause() != null) {
if (cause instanceof BadConfigException)
break;
cause = cause.getCause();
}
return cause;
}
}
@@ -1,36 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import androidx.annotation.Nullable;
import android.util.Log;
import java9.util.function.BiConsumer;
/**
* Helpers for logging exceptions from asynchronous tasks. These can be passed to
* {@code CompletionStage.whenComplete()} at the end of an asynchronous future chain.
*/
public enum ExceptionLoggers implements BiConsumer<Object, Throwable> {
D(Log.DEBUG),
E(Log.ERROR);
private static final String TAG = "WireGuard/" + ExceptionLoggers.class.getSimpleName();
private final int priority;
ExceptionLoggers(final int priority) {
this.priority = priority;
}
@Override
public void accept(final Object result, @Nullable final Throwable throwable) {
if (throwable != null)
Log.println(Log.ERROR, TAG, Log.getStackTraceString(throwable));
else if (priority <= Log.DEBUG)
Log.println(priority, TAG, "Future completed successfully");
}
}
@@ -1,27 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.content.Context;
import androidx.preference.Preference;
import android.view.ContextThemeWrapper;
import com.wireguard.android.activity.SettingsActivity;
public final class FragmentUtils {
private FragmentUtils() {
// Prevent instantiation
}
public static SettingsActivity getPrefActivity(final Preference preference) {
final Context context = preference.getContext();
if (context instanceof ContextThemeWrapper) {
if (context instanceof SettingsActivity) {
return ((SettingsActivity) context);
}
}
return null;
}
}
@@ -1,109 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import androidx.databinding.ObservableArrayList;
import androidx.annotation.Nullable;
import com.wireguard.util.Keyed;
import java.util.Collection;
import java.util.ListIterator;
import java.util.Objects;
/**
* ArrayList that allows looking up elements by some key property. As the key property must always
* be retrievable, this list cannot hold {@code null} elements. Because this class places no
* restrictions on the order or duplication of keys, lookup by key, as well as all list modification
* operations, require O(n) time.
*/
public class ObservableKeyedArrayList<K, E extends Keyed<? extends K>>
extends ObservableArrayList<E> implements ObservableKeyedList<K, E> {
@Override
public boolean add(@Nullable final E e) {
if (e == null)
throw new NullPointerException("Trying to add a null element");
return super.add(e);
}
@Override
public void add(final int index, @Nullable final E e) {
if (e == null)
throw new NullPointerException("Trying to add a null element");
super.add(index, e);
}
@Override
public boolean addAll(final Collection<? extends E> c) {
if (c.contains(null))
throw new NullPointerException("Trying to add a collection with null element(s)");
return super.addAll(c);
}
@Override
public boolean addAll(final int index, final Collection<? extends E> c) {
if (c.contains(null))
throw new NullPointerException("Trying to add a collection with null element(s)");
return super.addAll(index, c);
}
@Override
public boolean containsAllKeys(final Collection<K> keys) {
for (final K key : keys)
if (!containsKey(key))
return false;
return true;
}
@Override
public boolean containsKey(final K key) {
return indexOfKey(key) >= 0;
}
@Nullable
@Override
public E get(final K key) {
final int index = indexOfKey(key);
return index >= 0 ? get(index) : null;
}
@Nullable
@Override
public E getLast(final K key) {
final int index = lastIndexOfKey(key);
return index >= 0 ? get(index) : null;
}
@Override
public int indexOfKey(final K key) {
final ListIterator<E> iterator = listIterator();
while (iterator.hasNext()) {
final int index = iterator.nextIndex();
if (Objects.equals(iterator.next().getKey(), key))
return index;
}
return -1;
}
@Override
public int lastIndexOfKey(final K key) {
final ListIterator<E> iterator = listIterator(size());
while (iterator.hasPrevious()) {
final int index = iterator.previousIndex();
if (Objects.equals(iterator.previous().getKey(), key))
return index;
}
return -1;
}
@Override
public E set(final int index, @Nullable final E e) {
if (e == null)
throw new NullPointerException("Trying to set a null key");
return super.set(index, e);
}
}
@@ -1,19 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import androidx.databinding.ObservableList;
import com.wireguard.util.Keyed;
import com.wireguard.util.KeyedList;
/**
* A list that is both keyed and observable.
*/
public interface ObservableKeyedList<K, E extends Keyed<? extends K>>
extends KeyedList<K, E>, ObservableList<E> {
}
@@ -1,198 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import androidx.annotation.Nullable;
import com.wireguard.util.Keyed;
import com.wireguard.util.SortedKeyedList;
import java.util.AbstractList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.Spliterator;
/**
* KeyedArrayList that enforces uniqueness and sorted order across the set of keys. This class uses
* binary search to improve lookup and replacement times to O(log(n)). However, due to the
* array-based nature of this class, insertion and removal of elements with anything but the largest
* key still require O(n) time.
*/
public class ObservableSortedKeyedArrayList<K, E extends Keyed<? extends K>>
extends ObservableKeyedArrayList<K, E> implements ObservableSortedKeyedList<K, E> {
@Nullable private final Comparator<? super K> comparator;
private final transient KeyList<K, E> keyList = new KeyList<>(this);
@SuppressWarnings("WeakerAccess")
public ObservableSortedKeyedArrayList() {
comparator = null;
}
public ObservableSortedKeyedArrayList(final Comparator<? super K> comparator) {
this.comparator = comparator;
}
public ObservableSortedKeyedArrayList(final Collection<? extends E> c) {
this();
addAll(c);
}
public ObservableSortedKeyedArrayList(final SortedKeyedList<K, E> other) {
this(other.comparator());
addAll(other);
}
@Override
public boolean add(final E e) {
final int insertionPoint = getInsertionPoint(e);
if (insertionPoint < 0) {
// Skipping insertion is non-destructive if the new and existing objects are the same.
if (e == get(-insertionPoint - 1))
return false;
throw new IllegalArgumentException("Element with same key already exists in list");
}
super.add(insertionPoint, e);
return true;
}
@Override
public void add(final int index, final E e) {
final int insertionPoint = getInsertionPoint(e);
if (insertionPoint < 0)
throw new IllegalArgumentException("Element with same key already exists in list");
if (insertionPoint != index)
throw new IndexOutOfBoundsException("Wrong index given for element");
super.add(index, e);
}
@Override
public boolean addAll(final Collection<? extends E> c) {
boolean didChange = false;
for (final E e : c)
if (add(e))
didChange = true;
return didChange;
}
@Override
public boolean addAll(int index, final Collection<? extends E> c) {
for (final E e : c)
add(index++, e);
return true;
}
@Nullable
@Override
public Comparator<? super K> comparator() {
return comparator;
}
@Override
public K firstKey() {
if (isEmpty())
// The parameter in the exception is only to shut
// lint up, we never care for the exception message.
throw new NoSuchElementException("Empty set");
return get(0).getKey();
}
private int getInsertionPoint(final E e) {
if (comparator != null) {
return -Collections.binarySearch(keyList, e.getKey(), comparator) - 1;
} else {
@SuppressWarnings("unchecked") final List<Comparable<? super K>> list =
(List<Comparable<? super K>>) keyList;
return -Collections.binarySearch(list, e.getKey()) - 1;
}
}
@Override
public int indexOfKey(final K key) {
final int index;
if (comparator != null) {
index = Collections.binarySearch(keyList, key, comparator);
} else {
@SuppressWarnings("unchecked") final List<Comparable<? super K>> list =
(List<Comparable<? super K>>) keyList;
index = Collections.binarySearch(list, key);
}
return index >= 0 ? index : -1;
}
@Override
public Set<K> keySet() {
return keyList;
}
@Override
public int lastIndexOfKey(final K key) {
// There can never be more than one element with the same key in the list.
return indexOfKey(key);
}
@Override
public K lastKey() {
if (isEmpty())
// The parameter in the exception is only to shut
// lint up, we never care for the exception message.
throw new NoSuchElementException("Empty set");
return get(size() - 1).getKey();
}
@Override
public E set(final int index, final E e) {
final int order;
if (comparator != null) {
order = comparator.compare(e.getKey(), get(index).getKey());
} else {
@SuppressWarnings("unchecked") final Comparable<? super K> key =
(Comparable<? super K>) e.getKey();
order = key.compareTo(get(index).getKey());
}
if (order != 0) {
// Allow replacement if the new key would be inserted adjacent to the replaced element.
final int insertionPoint = getInsertionPoint(e);
if (insertionPoint < index || insertionPoint > index + 1)
throw new IndexOutOfBoundsException("Wrong index given for element");
}
return super.set(index, e);
}
@Override
public Collection<E> values() {
return this;
}
private static final class KeyList<K, E extends Keyed<? extends K>>
extends AbstractList<K> implements Set<K> {
private final ObservableSortedKeyedArrayList<K, E> list;
private KeyList(final ObservableSortedKeyedArrayList<K, E> list) {
this.list = list;
}
@Override
public K get(final int index) {
return list.get(index).getKey();
}
@Override
public int size() {
return list.size();
}
@Override
@SuppressWarnings("EmptyMethod")
public Spliterator<K> spliterator() {
return super.spliterator();
}
}
}
@@ -1,17 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import com.wireguard.util.Keyed;
import com.wireguard.util.SortedKeyedList;
/**
* A list that is both sorted/keyed and observable.
*/
public interface ObservableSortedKeyedList<K, E extends Keyed<? extends K>>
extends ObservableKeyedList<K, E>, SortedKeyedList<K, E> {
}
@@ -1,75 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.content.Context;
import android.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,190 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util;
import android.content.Context;
import androidx.annotation.Nullable;
import android.system.OsConstants;
import android.util.Log;
import com.wireguard.android.Application;
import com.wireguard.android.BuildConfig;
import com.wireguard.android.R;
import com.wireguard.android.util.RootShell.NoRootException;
import java.io.File;
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 MAGISK = 0x4;
public static final int NO = 0x2;
public static final int SYSTEM = 0x8;
public static final int YES = 0x1;
private static final String[][] EXECUTABLES = {
{"libwg.so", "wg"},
{"libwg-quick.so", "wg-quick"},
};
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 Context context;
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);
this.context = context;
}
@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(
context.getString(R.string.tools_unavailable_error));
}
}
public int install() throws NoRootException {
return willInstallAsMagiskModule() ? installMagisk() : installSystem();
}
private int installMagisk() throws NoRootException {
final StringBuilder script = new StringBuilder("set -ex; ");
script.append("trap 'rm -rf /sbin/.magisk/img/wireguard' INT TERM EXIT; ");
script.append(String.format("rm -rf /sbin/.magisk/img/wireguard/; mkdir -p /sbin/.magisk/img/wireguard%s; ", INSTALL_DIR));
script.append(String.format("printf 'name=WireGuard Command Line Tools\nversion=%s\nversionCode=%s\nauthor=zx2c4\ndescription=Command line tools for WireGuard\nminMagisk=1500\n' > /sbin/.magisk/img/wireguard/module.prop; ", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
script.append("touch /sbin/.magisk/img/wireguard/auto_mount; ");
for (final String[] names : EXECUTABLES) {
final File destination = new File("/sbin/.magisk/img/wireguard" + INSTALL_DIR, names[1]);
script.append(String.format("cp '%s' '%s'; chmod 755 '%s'; chcon 'u:object_r:system_file:s0' '%s' || true; ",
new File(nativeLibraryDir, names[0]), destination, destination, destination));
}
script.append("trap - INT TERM EXIT;");
try {
return Application.getRootShell().run(null, script.toString()) == 0 ? YES | MAGISK : ERROR;
} catch (final IOException ignored) {
return ERROR;
}
}
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;
}
}
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;
}
}
private boolean willInstallAsMagiskModule() {
synchronized (lock) {
if (installAsMagiskModule == null) {
try {
installAsMagiskModule = Application.getRootShell().run(null, "[ -d /sbin/.magisk/mirror -a -d /sbin/.magisk/img -a ! -f /cache/.disable_magisk ]") == OsConstants.EXIT_SUCCESS;
} catch (final Exception ignored) {
installAsMagiskModule = false;
}
}
return installAsMagiskModule;
}
}
}
@@ -1,93 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.viewmodel;
import androidx.databinding.ObservableArrayList;
import androidx.databinding.ObservableList;
import android.os.Parcel;
import android.os.Parcelable;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.Config;
import com.wireguard.config.Peer;
import java.util.ArrayList;
import java.util.Collection;
public class ConfigProxy implements Parcelable {
public static final Parcelable.Creator<ConfigProxy> CREATOR = new ConfigProxyCreator();
private final InterfaceProxy interfaze;
private final ObservableList<PeerProxy> peers = new ObservableArrayList<>();
private ConfigProxy(final Parcel in) {
interfaze = in.readParcelable(InterfaceProxy.class.getClassLoader());
in.readTypedList(peers, PeerProxy.CREATOR);
for (final PeerProxy proxy : peers)
proxy.bind(this);
}
public ConfigProxy(final Config other) {
interfaze = new InterfaceProxy(other.getInterface());
for (final Peer peer : other.getPeers()) {
final PeerProxy proxy = new PeerProxy(peer);
peers.add(proxy);
proxy.bind(this);
}
}
public ConfigProxy() {
interfaze = new InterfaceProxy();
}
public PeerProxy addPeer() {
final PeerProxy proxy = new PeerProxy();
peers.add(proxy);
proxy.bind(this);
return proxy;
}
@Override
public int describeContents() {
return 0;
}
public InterfaceProxy getInterface() {
return interfaze;
}
public ObservableList<PeerProxy> getPeers() {
return peers;
}
public Config resolve() throws BadConfigException {
final Collection<Peer> resolvedPeers = new ArrayList<>();
for (final PeerProxy proxy : peers)
resolvedPeers.add(proxy.resolve());
return new Config.Builder()
.setInterface(interfaze.resolve())
.addPeers(resolvedPeers)
.build();
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeParcelable(interfaze, flags);
dest.writeTypedList(peers);
}
private static class ConfigProxyCreator implements Parcelable.Creator<ConfigProxy> {
@Override
public ConfigProxy createFromParcel(final Parcel in) {
return new ConfigProxy(in);
}
@Override
public ConfigProxy[] newArray(final int size) {
return new ConfigProxy[size];
}
}
}
@@ -1,190 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.viewmodel;
import androidx.databinding.BaseObservable;
import androidx.databinding.Bindable;
import androidx.databinding.ObservableArrayList;
import androidx.databinding.ObservableList;
import android.os.Parcel;
import android.os.Parcelable;
import com.wireguard.android.BR;
import com.wireguard.config.Attribute;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.Interface;
import com.wireguard.crypto.Key;
import com.wireguard.crypto.KeyFormatException;
import com.wireguard.crypto.KeyPair;
import java.net.InetAddress;
import java.util.List;
import java9.util.stream.Collectors;
import java9.util.stream.StreamSupport;
public class InterfaceProxy extends BaseObservable implements Parcelable {
public static final Parcelable.Creator<InterfaceProxy> CREATOR = new InterfaceProxyCreator();
private final ObservableList<String> excludedApplications = new ObservableArrayList<>();
private String addresses;
private String dnsServers;
private String listenPort;
private String mtu;
private String privateKey;
private String publicKey;
private InterfaceProxy(final Parcel in) {
addresses = in.readString();
dnsServers = in.readString();
in.readStringList(excludedApplications);
listenPort = in.readString();
mtu = in.readString();
privateKey = in.readString();
publicKey = in.readString();
}
public InterfaceProxy(final Interface other) {
addresses = Attribute.join(other.getAddresses());
final List<String> dnsServerStrings = StreamSupport.stream(other.getDnsServers())
.map(InetAddress::getHostAddress)
.collect(Collectors.toUnmodifiableList());
dnsServers = Attribute.join(dnsServerStrings);
excludedApplications.addAll(other.getExcludedApplications());
listenPort = other.getListenPort().map(String::valueOf).orElse("");
mtu = other.getMtu().map(String::valueOf).orElse("");
final KeyPair keyPair = other.getKeyPair();
privateKey = keyPair.getPrivateKey().toBase64();
publicKey = keyPair.getPublicKey().toBase64();
}
public InterfaceProxy() {
addresses = "";
dnsServers = "";
listenPort = "";
mtu = "";
privateKey = "";
publicKey = "";
}
@Override
public int describeContents() {
return 0;
}
public void generateKeyPair() {
final KeyPair keyPair = new KeyPair();
privateKey = keyPair.getPrivateKey().toBase64();
publicKey = keyPair.getPublicKey().toBase64();
notifyPropertyChanged(BR.privateKey);
notifyPropertyChanged(BR.publicKey);
}
@Bindable
public String getAddresses() {
return addresses;
}
@Bindable
public String getDnsServers() {
return dnsServers;
}
public ObservableList<String> getExcludedApplications() {
return excludedApplications;
}
@Bindable
public String getListenPort() {
return listenPort;
}
@Bindable
public String getMtu() {
return mtu;
}
@Bindable
public String getPrivateKey() {
return privateKey;
}
@Bindable
public String getPublicKey() {
return publicKey;
}
public Interface resolve() throws BadConfigException {
final Interface.Builder builder = new Interface.Builder();
if (!addresses.isEmpty())
builder.parseAddresses(addresses);
if (!dnsServers.isEmpty())
builder.parseDnsServers(dnsServers);
if (!excludedApplications.isEmpty())
builder.excludeApplications(excludedApplications);
if (!listenPort.isEmpty())
builder.parseListenPort(listenPort);
if (!mtu.isEmpty())
builder.parseMtu(mtu);
if (!privateKey.isEmpty())
builder.parsePrivateKey(privateKey);
return builder.build();
}
public void setAddresses(final String addresses) {
this.addresses = addresses;
notifyPropertyChanged(BR.addresses);
}
public void setDnsServers(final String dnsServers) {
this.dnsServers = dnsServers;
notifyPropertyChanged(BR.dnsServers);
}
public void setListenPort(final String listenPort) {
this.listenPort = listenPort;
notifyPropertyChanged(BR.listenPort);
}
public void setMtu(final String mtu) {
this.mtu = mtu;
notifyPropertyChanged(BR.mtu);
}
public void setPrivateKey(final String privateKey) {
this.privateKey = privateKey;
try {
publicKey = new KeyPair(Key.fromBase64(privateKey)).getPublicKey().toBase64();
} catch (final KeyFormatException ignored) {
publicKey = "";
}
notifyPropertyChanged(BR.privateKey);
notifyPropertyChanged(BR.publicKey);
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeString(addresses);
dest.writeString(dnsServers);
dest.writeStringList(excludedApplications);
dest.writeString(listenPort);
dest.writeString(mtu);
dest.writeString(privateKey);
dest.writeString(publicKey);
}
private static class InterfaceProxyCreator implements Parcelable.Creator<InterfaceProxy> {
@Override
public InterfaceProxy createFromParcel(final Parcel in) {
return new InterfaceProxy(in);
}
@Override
public InterfaceProxy[] newArray(final int size) {
return new InterfaceProxy[size];
}
}
}
@@ -1,380 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.viewmodel;
import androidx.databinding.BaseObservable;
import androidx.databinding.Bindable;
import androidx.databinding.Observable;
import androidx.databinding.ObservableList;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import com.wireguard.android.BR;
import com.wireguard.config.Attribute;
import com.wireguard.config.BadConfigException;
import com.wireguard.config.InetEndpoint;
import com.wireguard.config.Peer;
import com.wireguard.crypto.Key;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java9.util.Lists;
import java9.util.Sets;
import java9.util.stream.Collectors;
import java9.util.stream.Stream;
public class PeerProxy extends BaseObservable implements Parcelable {
public static final Parcelable.Creator<PeerProxy> CREATOR = new PeerProxyCreator();
private static final Set<String> IPV4_PUBLIC_NETWORKS = new LinkedHashSet<>(Lists.of(
"0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
"64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
"172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
"176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
"192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
"193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
));
private static final Set<String> IPV4_WILDCARD = Sets.of("0.0.0.0/0");
private final List<String> dnsRoutes = new ArrayList<>();
private String allowedIps;
private AllowedIpsState allowedIpsState = AllowedIpsState.INVALID;
private String endpoint;
@Nullable private InterfaceDnsListener interfaceDnsListener;
@Nullable private ConfigProxy owner;
@Nullable private PeerListListener peerListListener;
private String persistentKeepalive;
private String preSharedKey;
private String publicKey;
private int totalPeers;
private PeerProxy(final Parcel in) {
allowedIps = in.readString();
endpoint = in.readString();
persistentKeepalive = in.readString();
preSharedKey = in.readString();
publicKey = in.readString();
}
public PeerProxy(final Peer other) {
allowedIps = Attribute.join(other.getAllowedIps());
endpoint = other.getEndpoint().map(InetEndpoint::toString).orElse("");
persistentKeepalive = other.getPersistentKeepalive().map(String::valueOf).orElse("");
preSharedKey = other.getPreSharedKey().map(Key::toBase64).orElse("");
publicKey = other.getPublicKey().toBase64();
}
public PeerProxy() {
allowedIps = "";
endpoint = "";
persistentKeepalive = "";
preSharedKey = "";
publicKey = "";
}
public void bind(final ConfigProxy owner) {
final InterfaceProxy interfaze = owner.getInterface();
final ObservableList<PeerProxy> peers = owner.getPeers();
if (interfaceDnsListener == null)
interfaceDnsListener = new InterfaceDnsListener(this);
interfaze.addOnPropertyChangedCallback(interfaceDnsListener);
setInterfaceDns(interfaze.getDnsServers());
if (peerListListener == null)
peerListListener = new PeerListListener(this);
peers.addOnListChangedCallback(peerListListener);
setTotalPeers(peers.size());
this.owner = owner;
}
private void calculateAllowedIpsState() {
final AllowedIpsState newState;
if (totalPeers == 1) {
// String comparison works because we only care if allowedIps is a superset of one of
// the above sets of (valid) *networks*. We are not checking for a superset based on
// the individual addresses in each set.
final Collection<String> networkStrings = getAllowedIpsSet();
// If allowedIps contains both the wildcard and the public networks, then private
// networks aren't excluded!
if (networkStrings.containsAll(IPV4_WILDCARD))
newState = AllowedIpsState.CONTAINS_IPV4_WILDCARD;
else if (networkStrings.containsAll(IPV4_PUBLIC_NETWORKS))
newState = AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS;
else
newState = AllowedIpsState.OTHER;
} else {
newState = AllowedIpsState.INVALID;
}
if (newState != allowedIpsState) {
allowedIpsState = newState;
notifyPropertyChanged(BR.ableToExcludePrivateIps);
notifyPropertyChanged(BR.excludingPrivateIps);
}
}
@Override
public int describeContents() {
return 0;
}
@Bindable
public String getAllowedIps() {
return allowedIps;
}
private Set<String> getAllowedIpsSet() {
return new LinkedHashSet<>(Lists.of(Attribute.split(allowedIps)));
}
@Bindable
public String getEndpoint() {
return endpoint;
}
@Bindable
public String getPersistentKeepalive() {
return persistentKeepalive;
}
@Bindable
public String getPreSharedKey() {
return preSharedKey;
}
@Bindable
public String getPublicKey() {
return publicKey;
}
@Bindable
public boolean isAbleToExcludePrivateIps() {
return allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS
|| allowedIpsState == AllowedIpsState.CONTAINS_IPV4_WILDCARD;
}
@Bindable
public boolean isExcludingPrivateIps() {
return allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS;
}
public Peer resolve() throws BadConfigException {
final Peer.Builder builder = new Peer.Builder();
if (!allowedIps.isEmpty())
builder.parseAllowedIPs(allowedIps);
if (!endpoint.isEmpty())
builder.parseEndpoint(endpoint);
if (!persistentKeepalive.isEmpty())
builder.parsePersistentKeepalive(persistentKeepalive);
if (!preSharedKey.isEmpty())
builder.parsePreSharedKey(preSharedKey);
if (!publicKey.isEmpty())
builder.parsePublicKey(publicKey);
return builder.build();
}
public void setAllowedIps(final String allowedIps) {
this.allowedIps = allowedIps;
notifyPropertyChanged(BR.allowedIps);
calculateAllowedIpsState();
}
public void setEndpoint(final String endpoint) {
this.endpoint = endpoint;
notifyPropertyChanged(BR.endpoint);
}
public void setExcludingPrivateIps(final boolean excludingPrivateIps) {
if (!isAbleToExcludePrivateIps() || isExcludingPrivateIps() == excludingPrivateIps)
return;
final Set<String> oldNetworks = excludingPrivateIps ? IPV4_WILDCARD : IPV4_PUBLIC_NETWORKS;
final Set<String> newNetworks = excludingPrivateIps ? IPV4_PUBLIC_NETWORKS : IPV4_WILDCARD;
final Collection<String> input = getAllowedIpsSet();
final int outputSize = input.size() - oldNetworks.size() + newNetworks.size();
final Collection<String> output = new LinkedHashSet<>(outputSize);
boolean replaced = false;
// Replace the first instance of the wildcard with the public network list, or vice versa.
for (final String network : input) {
if (oldNetworks.contains(network)) {
if (!replaced) {
for (final String replacement : newNetworks)
if (!output.contains(replacement))
output.add(replacement);
replaced = true;
}
} else if (!output.contains(network)) {
output.add(network);
}
}
// DNS servers only need to handled specially when we're excluding private IPs.
if (excludingPrivateIps)
output.addAll(dnsRoutes);
else
output.removeAll(dnsRoutes);
allowedIps = Attribute.join(output);
allowedIpsState = excludingPrivateIps ?
AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS : AllowedIpsState.CONTAINS_IPV4_WILDCARD;
notifyPropertyChanged(BR.allowedIps);
notifyPropertyChanged(BR.excludingPrivateIps);
}
private void setInterfaceDns(final CharSequence dnsServers) {
final List<String> newDnsRoutes = Stream.of(Attribute.split(dnsServers))
.filter(server -> !server.contains(":"))
.map(server -> server + "/32")
.collect(Collectors.toUnmodifiableList());
if (allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS) {
final Collection<String> input = getAllowedIpsSet();
final Collection<String> output = new LinkedHashSet<>(input.size() + 1);
// Yes, this is quadratic in the number of DNS servers, but most users have 1 or 2.
for (final String network : input)
if (!dnsRoutes.contains(network) || newDnsRoutes.contains(network))
output.add(network);
// Since output is a Set, this does the Right Thing™ (it does not duplicate networks).
output.addAll(newDnsRoutes);
// None of the public networks are /32s, so this cannot change the AllowedIPs state.
allowedIps = Attribute.join(output);
notifyPropertyChanged(BR.allowedIps);
}
dnsRoutes.clear();
dnsRoutes.addAll(newDnsRoutes);
}
public void setPersistentKeepalive(final String persistentKeepalive) {
this.persistentKeepalive = persistentKeepalive;
notifyPropertyChanged(BR.persistentKeepalive);
}
public void setPreSharedKey(final String preSharedKey) {
this.preSharedKey = preSharedKey;
notifyPropertyChanged(BR.preSharedKey);
}
public void setPublicKey(final String publicKey) {
this.publicKey = publicKey;
notifyPropertyChanged(BR.publicKey);
}
private void setTotalPeers(final int totalPeers) {
if (this.totalPeers == totalPeers)
return;
this.totalPeers = totalPeers;
calculateAllowedIpsState();
}
public void unbind() {
if (owner == null)
return;
final InterfaceProxy interfaze = owner.getInterface();
final ObservableList<PeerProxy> peers = owner.getPeers();
if (interfaceDnsListener != null)
interfaze.removeOnPropertyChangedCallback(interfaceDnsListener);
if (peerListListener != null)
peers.removeOnListChangedCallback(peerListListener);
peers.remove(this);
setInterfaceDns("");
setTotalPeers(0);
owner = null;
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeString(allowedIps);
dest.writeString(endpoint);
dest.writeString(persistentKeepalive);
dest.writeString(preSharedKey);
dest.writeString(publicKey);
}
private enum AllowedIpsState {
CONTAINS_IPV4_PUBLIC_NETWORKS,
CONTAINS_IPV4_WILDCARD,
INVALID,
OTHER
}
private static final class InterfaceDnsListener extends Observable.OnPropertyChangedCallback {
private final WeakReference<PeerProxy> weakPeerProxy;
private InterfaceDnsListener(final PeerProxy peerProxy) {
weakPeerProxy = new WeakReference<>(peerProxy);
}
@Override
public void onPropertyChanged(final Observable sender, final int propertyId) {
@Nullable final PeerProxy peerProxy = weakPeerProxy.get();
if (peerProxy == null) {
sender.removeOnPropertyChangedCallback(this);
return;
}
// This shouldn't be possible, but try to avoid a ClassCastException anyway.
if (!(sender instanceof InterfaceProxy))
return;
if (!(propertyId == BR._all || propertyId == BR.dnsServers))
return;
peerProxy.setInterfaceDns(((InterfaceProxy) sender).getDnsServers());
}
}
private static final class PeerListListener
extends ObservableList.OnListChangedCallback<ObservableList<PeerProxy>> {
private final WeakReference<PeerProxy> weakPeerProxy;
private PeerListListener(final PeerProxy peerProxy) {
weakPeerProxy = new WeakReference<>(peerProxy);
}
@Override
public void onChanged(final ObservableList<PeerProxy> sender) {
@Nullable final PeerProxy peerProxy = weakPeerProxy.get();
if (peerProxy == null) {
sender.removeOnListChangedCallback(this);
return;
}
peerProxy.setTotalPeers(sender.size());
}
@Override
public void onItemRangeChanged(final ObservableList<PeerProxy> sender,
final int positionStart, final int itemCount) {
// Do nothing.
}
@Override
public void onItemRangeInserted(final ObservableList<PeerProxy> sender,
final int positionStart, final int itemCount) {
onChanged(sender);
}
@Override
public void onItemRangeMoved(final ObservableList<PeerProxy> sender,
final int fromPosition, final int toPosition,
final int itemCount) {
// Do nothing.
}
@Override
public void onItemRangeRemoved(final ObservableList<PeerProxy> sender,
final int positionStart, final int itemCount) {
onChanged(sender);
}
}
private static class PeerProxyCreator implements Parcelable.Creator<PeerProxy> {
@Override
public PeerProxy createFromParcel(final Parcel in) {
return new PeerProxy(in);
}
@Override
public PeerProxy[] newArray(final int size) {
return new PeerProxy[size];
}
}
}
@@ -1,54 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget;
import androidx.annotation.Nullable;
import android.text.InputFilter;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import com.wireguard.crypto.Key;
/**
* InputFilter for entering WireGuard private/public keys encoded with base64.
*/
public class KeyInputFilter implements InputFilter {
private static boolean isAllowed(final char c) {
return Character.isLetterOrDigit(c) || c == '+' || c == '/';
}
public static InputFilter newInstance() {
return new KeyInputFilter();
}
@Nullable
@Override
public CharSequence filter(final CharSequence source,
final int sStart, final int sEnd,
final Spanned dest,
final int dStart, final int dEnd) {
SpannableStringBuilder replacement = null;
int rIndex = 0;
final int dLength = dest.length();
for (int sIndex = sStart; sIndex < sEnd; ++sIndex) {
final char c = source.charAt(sIndex);
final int dIndex = dStart + (sIndex - sStart);
// Restrict characters to the base64 character set.
// Ensure adding this character does not push the length over the limit.
if (((dIndex + 1 < Key.Format.BASE64.getLength() && isAllowed(c)) ||
(dIndex + 1 == Key.Format.BASE64.getLength() && c == '=')) &&
dLength + (sIndex - sStart) < Key.Format.BASE64.getLength()) {
++rIndex;
} else {
if (replacement == null)
replacement = new SpannableStringBuilder(source, sStart, sEnd);
replacement.delete(rIndex, rIndex + 1);
}
}
return replacement;
}
}
@@ -1,59 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.RelativeLayout;
import com.wireguard.android.R;
public class MultiselectableRelativeLayout extends RelativeLayout {
private static final int[] STATE_MULTISELECTED = {R.attr.state_multiselected};
private boolean multiselected;
public MultiselectableRelativeLayout(final Context context) {
super(context);
}
public MultiselectableRelativeLayout(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
public MultiselectableRelativeLayout(final Context context, final AttributeSet attrs, final int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public MultiselectableRelativeLayout(final Context context, final AttributeSet attrs, final int defStyleAttr, final int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected int[] onCreateDrawableState(final int extraSpace) {
if (multiselected) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
mergeDrawableStates(drawableState, STATE_MULTISELECTED);
return drawableState;
}
return super.onCreateDrawableState(extraSpace);
}
public void setMultiSelected(final boolean on) {
if (!multiselected) {
multiselected = true;
refreshDrawableState();
}
setActivated(on);
}
public void setSingleSelected(final boolean on) {
if (multiselected) {
multiselected = false;
refreshDrawableState();
}
setActivated(on);
}
}
@@ -1,53 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget;
import androidx.annotation.Nullable;
import android.text.InputFilter;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import com.wireguard.android.model.Tunnel;
/**
* InputFilter for entering WireGuard configuration names (Linux interface names).
*/
public class NameInputFilter implements InputFilter {
private static boolean isAllowed(final char c) {
return Character.isLetterOrDigit(c) || "_=+.-".indexOf(c) >= 0;
}
public static InputFilter newInstance() {
return new NameInputFilter();
}
@Nullable
@Override
public CharSequence filter(final CharSequence source,
final int sStart, final int sEnd,
final Spanned dest,
final int dStart, final int dEnd) {
SpannableStringBuilder replacement = null;
int rIndex = 0;
final int dLength = dest.length();
for (int sIndex = sStart; sIndex < sEnd; ++sIndex) {
final char c = source.charAt(sIndex);
final int dIndex = dStart + (sIndex - sStart);
// Restrict characters to those valid in interfaces.
// Ensure adding this character does not push the length over the limit.
if ((dIndex < Tunnel.NAME_MAX_LENGTH && isAllowed(c)) &&
dLength + (sIndex - sStart) < Tunnel.NAME_MAX_LENGTH) {
++rIndex;
} else {
if (replacement == null)
replacement = new SpannableStringBuilder(source, sStart, sEnd);
replacement.delete(rIndex, rIndex + 1);
}
}
return replacement;
}
}
@@ -1,216 +0,0 @@
/*
* Copyright © 2018 The Android Open Source Project
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.content.res.ColorStateList;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Path.Direction;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff.Mode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.drawable.Drawable;
import android.os.Build;
import androidx.annotation.ColorInt;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import android.util.FloatProperty;
@TargetApi(Build.VERSION_CODES.N)
public class SlashDrawable extends Drawable {
private static final float CENTER_X = 10.65f;
private static final float CENTER_Y = 11.869239f;
private static final float CORNER_RADIUS = Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? 0f : 1f;
// Draw the slash washington-monument style; rotate to no-u-turn style
private static final float DEFAULT_ROTATION = -45f;
private static final long QS_ANIM_LENGTH = 350;
private static final float SCALE = 24f;
private static final float SLASH_HEIGHT = 28f;
// These values are derived in un-rotated (vertical) orientation
private static final float SLASH_WIDTH = 1.8384776f;
// Bottom is derived during animation
private static final float LEFT = (CENTER_X - (SLASH_WIDTH / 2)) / SCALE;
private static final float RIGHT = (CENTER_X + (SLASH_WIDTH / 2)) / SCALE;
private static final float TOP = (CENTER_Y - (SLASH_HEIGHT / 2)) / SCALE;
private static final FloatProperty mSlashLengthProp = new FloatProperty<SlashDrawable>("slashLength") {
@Override
public Float get(final SlashDrawable object) {
return object.mCurrentSlashLength;
}
@Override
public void setValue(final SlashDrawable object, final float value) {
object.mCurrentSlashLength = value;
}
};
private final Drawable mDrawable;
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Path mPath = new Path();
private final RectF mSlashRect = new RectF(0, 0, 0, 0);
private boolean mAnimationEnabled = true;
// Animate this value on change
private float mCurrentSlashLength;
private float mRotation;
private boolean mSlashed;
public SlashDrawable(final Drawable d) {
mDrawable = d;
}
@SuppressWarnings("deprecation")
@Override
public void draw(final Canvas canvas) {
canvas.save();
final Matrix m = new Matrix();
final int width = getBounds().width();
final int height = getBounds().height();
final float radiusX = scale(CORNER_RADIUS, width);
final float radiusY = scale(CORNER_RADIUS, height);
updateRect(
scale(LEFT, width),
scale(TOP, height),
scale(RIGHT, width),
scale(TOP + mCurrentSlashLength, height)
);
mPath.reset();
// Draw the slash vertically
mPath.addRoundRect(mSlashRect, radiusX, radiusY, Direction.CW);
// Rotate -45 + desired rotation
m.setRotate(mRotation + DEFAULT_ROTATION, width / 2, height / 2);
mPath.transform(m);
canvas.drawPath(mPath, mPaint);
// Rotate back to vertical
m.setRotate(-mRotation - DEFAULT_ROTATION, width / 2, height / 2);
mPath.transform(m);
// Draw another rect right next to the first, for clipping
m.setTranslate(mSlashRect.width(), 0);
mPath.transform(m);
mPath.addRoundRect(mSlashRect, 1.0f * width, 1.0f * height, Direction.CW);
m.setRotate(mRotation + DEFAULT_ROTATION, width / 2, height / 2);
mPath.transform(m);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
canvas.clipPath(mPath, Region.Op.DIFFERENCE);
else
canvas.clipOutPath(mPath);
mDrawable.draw(canvas);
canvas.restore();
}
@Override
public int getIntrinsicHeight() {
return mDrawable.getIntrinsicHeight();
}
@Override
public int getIntrinsicWidth() {
return mDrawable.getIntrinsicWidth();
}
@Override
public int getOpacity() {
return PixelFormat.OPAQUE;
}
@Override
protected void onBoundsChange(final Rect bounds) {
super.onBoundsChange(bounds);
mDrawable.setBounds(bounds);
}
private float scale(final float frac, final int width) {
return frac * width;
}
@Override
public void setAlpha(@IntRange(from = 0, to = 255) final int alpha) {
mDrawable.setAlpha(alpha);
mPaint.setAlpha(alpha);
}
public void setAnimationEnabled(final boolean enabled) {
mAnimationEnabled = enabled;
}
@Override
public void setColorFilter(@Nullable final ColorFilter colorFilter) {
mDrawable.setColorFilter(colorFilter);
mPaint.setColorFilter(colorFilter);
}
private void setDrawableTintList(@Nullable final ColorStateList tint) {
mDrawable.setTintList(tint);
}
public void setRotation(final float rotation) {
if (mRotation == rotation)
return;
mRotation = rotation;
invalidateSelf();
}
@SuppressWarnings("unchecked")
public void setSlashed(final boolean slashed) {
if (mSlashed == slashed) return;
mSlashed = slashed;
final float end = mSlashed ? SLASH_HEIGHT / SCALE : 0f;
final float start = mSlashed ? 0f : SLASH_HEIGHT / SCALE;
if (mAnimationEnabled) {
final ObjectAnimator anim = ObjectAnimator.ofFloat(this, mSlashLengthProp, start, end);
anim.addUpdateListener((ValueAnimator valueAnimator) -> invalidateSelf());
anim.setDuration(QS_ANIM_LENGTH);
anim.start();
} else {
mCurrentSlashLength = end;
invalidateSelf();
}
}
@Override
public void setTint(@ColorInt final int tintColor) {
super.setTint(tintColor);
mDrawable.setTint(tintColor);
mPaint.setColor(tintColor);
}
@Override
public void setTintList(@Nullable final ColorStateList tint) {
super.setTintList(tint);
setDrawableTintList(tint);
mPaint.setColor(tint == null ? 0 : tint.getDefaultColor());
invalidateSelf();
}
@Override
public void setTintMode(final Mode tintMode) {
super.setTintMode(tintMode);
mDrawable.setTintMode(tintMode);
}
private void updateRect(final float left, final float top, final float right, final float bottom) {
mSlashRect.left = left;
mSlashRect.top = top;
mSlashRect.right = right;
mSlashRect.bottom = bottom;
}
}
@@ -1,59 +0,0 @@
/*
* Copyright © 2013 The Android Open Source Project
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget;
import android.content.Context;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import android.util.AttributeSet;
import android.widget.Switch;
public class ToggleSwitch extends Switch {
private boolean isRestoringState;
@Nullable private OnBeforeCheckedChangeListener listener;
public ToggleSwitch(final Context context) {
this(context, null);
}
@SuppressWarnings({"SameParameterValue", "WeakerAccess"})
public ToggleSwitch(final Context context, @Nullable final AttributeSet attrs) {
super(context, attrs);
}
@Override
public void onRestoreInstanceState(final Parcelable state) {
isRestoringState = true;
super.onRestoreInstanceState(state);
isRestoringState = false;
}
@Override
public void setChecked(final boolean checked) {
if (checked == isChecked())
return;
if (isRestoringState || listener == null) {
super.setChecked(checked);
return;
}
setEnabled(false);
listener.onBeforeCheckedChanged(this, checked);
}
public void setCheckedInternal(final boolean checked) {
super.setChecked(checked);
setEnabled(true);
}
public void setOnBeforeCheckedChangeListener(final OnBeforeCheckedChangeListener listener) {
this.listener = listener;
}
public interface OnBeforeCheckedChangeListener {
void onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked);
}
}
@@ -1,66 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget.fab;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.content.Context;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import com.google.android.material.snackbar.Snackbar;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import android.util.AttributeSet;
import android.view.View;
public class FloatingActionButtonBehavior extends CoordinatorLayout.Behavior<FloatingActionsMenu> {
private static final long ANIMATION_DURATION = 250;
private static final TimeInterpolator FAST_OUT_SLOW_IN_INTERPOLATOR = new FastOutSlowInInterpolator();
public FloatingActionButtonBehavior(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
private static void animateChange(final FloatingActionsMenu child, final float destination, final float fullSpan) {
final float origin = child.getBehaviorYTranslation();
if (Math.abs(destination - origin) < fullSpan / 2) {
child.setBehaviorYTranslation(destination);
return;
}
final ValueAnimator animator = new ValueAnimator();
animator.setFloatValues(origin, destination);
animator.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
animator.setDuration((long) (ANIMATION_DURATION * (Math.abs(destination - origin) / fullSpan)));
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(final Animator a) {
child.setBehaviorYTranslation(destination);
}
});
animator.addUpdateListener(a -> child.setBehaviorYTranslation((float) a.getAnimatedValue()));
animator.start();
}
@Override
public boolean layoutDependsOn(final CoordinatorLayout parent, final FloatingActionsMenu child,
final View dependency) {
return dependency instanceof Snackbar.SnackbarLayout;
}
@Override
public boolean onDependentViewChanged(final CoordinatorLayout parent, final FloatingActionsMenu child,
final View dependency) {
animateChange(child, Math.min(0, dependency.getTranslationY() - dependency.getMeasuredHeight()), dependency.getMeasuredHeight());
return true;
}
@Override
public void onDependentViewRemoved(final CoordinatorLayout parent, final FloatingActionsMenu child,
final View dependency) {
animateChange(child, 0, dependency.getMeasuredHeight());
}
}
@@ -1,629 +0,0 @@
/*
* Copyright © 2014 Jerzy Chalupski
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget.fab;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Keep;
import androidx.annotation.Nullable;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import androidx.core.content.res.ResourcesCompat;
import androidx.appcompat.widget.AppCompatTextView;
import android.util.AttributeSet;
import android.view.ContextThemeWrapper;
import android.view.TouchDelegate;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.OvershootInterpolator;
import android.widget.TextView;
import com.wireguard.android.R;
public class FloatingActionsMenu extends ViewGroup {
public static final int EXPAND_DOWN = 1;
public static final int EXPAND_LEFT = 2;
public static final int EXPAND_RIGHT = 3;
public static final int EXPAND_UP = 0;
public static final int LABELS_ON_LEFT_SIDE = 0;
public static final int LABELS_ON_RIGHT_SIDE = 1;
private static final TimeInterpolator ALPHA_EXPAND_INTERPOLATOR = new DecelerateInterpolator();
private static final int ANIMATION_DURATION = 300;
private static final boolean BROKEN_LABEL_STYLE = Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1 && Build.BRAND.equals("ASUS");
private static final float COLLAPSED_PLUS_ROTATION = 0f;
private static final TimeInterpolator COLLAPSE_INTERPOLATOR = new DecelerateInterpolator(3f);
private static final float EXPANDED_PLUS_ROTATION = 90f + 45f;
private static final TimeInterpolator EXPAND_INTERPOLATOR = new OvershootInterpolator();
private final AnimatorSet mCollapseAnimation = new AnimatorSet().setDuration(ANIMATION_DURATION);
private final AnimatorSet mExpandAnimation = new AnimatorSet().setDuration(ANIMATION_DURATION);
private final Rect touchArea = new Rect(0, 0, 0, 0);
private float behaviorYTranslation;
@Nullable private FloatingActionButton mAddButton;
private int mButtonSpacing;
private int mButtonsCount;
private int mExpandDirection;
private boolean mExpanded;
private int mLabelsMargin;
private int mLabelsPosition;
private int mLabelsStyle;
private int mLabelsVerticalOffset;
@Nullable private OnFloatingActionsMenuUpdateListener mListener;
private int mMaxButtonHeight;
private int mMaxButtonWidth;
@Nullable private RotatingDrawable mRotatingDrawable;
@Nullable private TouchDelegateGroup mTouchDelegateGroup;
private float scrollYTranslation;
public FloatingActionsMenu(final Context context) {
this(context, null);
}
public FloatingActionsMenu(final Context context, @Nullable final AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public FloatingActionsMenu(final Context context, @Nullable final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
private static int adjustForOvershoot(final int dimension) {
return dimension * 12 / 10;
}
public void addButton(final LabeledFloatingActionButton button) {
addView(button, mButtonsCount - 1);
mButtonsCount++;
if (mLabelsStyle != 0) {
createLabels();
}
}
public void collapse() {
collapse(false);
}
private void collapse(final boolean immediately) {
if (mExpanded) {
mExpanded = false;
mTouchDelegateGroup.setEnabled(false);
mCollapseAnimation.setDuration(immediately ? 0 : ANIMATION_DURATION);
mCollapseAnimation.start();
mExpandAnimation.cancel();
if (mListener != null) {
mListener.onMenuCollapsed();
}
}
}
public void collapseImmediately() {
collapse(true);
}
private void createAddButton(final Context context) {
final RotatingDrawable rotatingDrawable = new RotatingDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_action_add_white, context.getTheme()));
mRotatingDrawable = rotatingDrawable;
final TimeInterpolator interpolator = new OvershootInterpolator();
final ObjectAnimator collapseAnimator = ObjectAnimator.ofFloat(rotatingDrawable, "rotation", EXPANDED_PLUS_ROTATION, COLLAPSED_PLUS_ROTATION);
final ObjectAnimator expandAnimator = ObjectAnimator.ofFloat(rotatingDrawable, "rotation", COLLAPSED_PLUS_ROTATION, EXPANDED_PLUS_ROTATION);
collapseAnimator.setInterpolator(interpolator);
expandAnimator.setInterpolator(interpolator);
mExpandAnimation.play(expandAnimator);
mCollapseAnimation.play(collapseAnimator);
mAddButton = new FloatingActionButton(context);
mAddButton.setImageDrawable(rotatingDrawable);
mAddButton.setId(R.id.fab_expand_menu_button);
mAddButton.setOnClickListener(v -> toggle());
addView(mAddButton, super.generateDefaultLayoutParams());
mButtonsCount++;
}
private void createLabels() {
final Context context = BROKEN_LABEL_STYLE ? getContext() : new ContextThemeWrapper(getContext(), mLabelsStyle);
for (int i = 0; i < mButtonsCount; i++) {
final FloatingActionButton button = (FloatingActionButton) getChildAt(i);
if (button instanceof LabeledFloatingActionButton) {
final String title = ((LabeledFloatingActionButton) button).getTitle();
final AppCompatTextView label = new AppCompatTextView(context);
if (!BROKEN_LABEL_STYLE)
label.setTextAppearance(context, mLabelsStyle);
label.setText(title);
addView(label);
button.setTag(R.id.fab_label, label);
}
}
}
public void expand() {
if (!mExpanded) {
mExpanded = true;
mTouchDelegateGroup.setEnabled(true);
mCollapseAnimation.cancel();
mExpandAnimation.start();
if (mListener != null) {
mListener.onMenuExpanded();
}
}
}
private boolean expandsHorizontally() {
return mExpandDirection == EXPAND_LEFT || mExpandDirection == EXPAND_RIGHT;
}
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(super.generateDefaultLayoutParams());
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(final AttributeSet attrs) {
return new LayoutParams(super.generateLayoutParams(attrs));
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(final ViewGroup.LayoutParams p) {
return new LayoutParams(super.generateLayoutParams(p));
}
public float getBehaviorYTranslation() {
return behaviorYTranslation;
}
public float getScrollYTranslation() {
return scrollYTranslation;
}
private void init(final Context context, @Nullable final AttributeSet attributeSet) {
mButtonSpacing = (int) (getResources().getDimension(R.dimen.fab_actions_spacing));
mLabelsMargin = getResources().getDimensionPixelSize(R.dimen.fab_labels_margin);
mLabelsVerticalOffset = getResources().getDimensionPixelSize(R.dimen.fab_shadow_offset);
mTouchDelegateGroup = new TouchDelegateGroup(this);
setTouchDelegate(mTouchDelegateGroup);
final TypedArray attr = context.obtainStyledAttributes(attributeSet, R.styleable.FloatingActionsMenu, 0, 0);
mExpandDirection = attr.getInt(R.styleable.FloatingActionsMenu_fab_expandDirection, EXPAND_UP);
mLabelsStyle = attr.getResourceId(R.styleable.FloatingActionsMenu_fab_labelStyle, 0);
mLabelsPosition = attr.getInt(R.styleable.FloatingActionsMenu_fab_labelsPosition, LABELS_ON_LEFT_SIDE);
attr.recycle();
if (mLabelsStyle != 0 && expandsHorizontally()) {
throw new IllegalStateException("Action labels in horizontal expand orientation are not supported");
}
createAddButton(context);
}
public boolean isExpanded() {
return mExpanded;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
bringChildToFront(mAddButton);
mButtonsCount = getChildCount();
if (mLabelsStyle != 0) {
createLabels();
}
}
@Override
protected void onLayout(final boolean changed, final int l, final int t, final int r, final int b) {
switch (mExpandDirection) {
case EXPAND_UP:
case EXPAND_DOWN:
final boolean expandUp = mExpandDirection == EXPAND_UP;
if (changed) {
mTouchDelegateGroup.clearTouchDelegates();
}
final int addButtonY = expandUp ? b - t - mAddButton.getMeasuredHeight() : 0;
// Ensure mAddButton is centered on the line where the buttons should be
final int buttonsHorizontalCenter = (mLabelsPosition == LABELS_ON_LEFT_SIDE
? r - l - mMaxButtonWidth / 2
: mMaxButtonWidth / 2);
final int addButtonLeft = buttonsHorizontalCenter - mAddButton.getMeasuredWidth() / 2;
mAddButton.layout(addButtonLeft, addButtonY, addButtonLeft + mAddButton.getMeasuredWidth(), addButtonY + mAddButton.getMeasuredHeight());
final int labelsOffset = mMaxButtonWidth / 2 + mLabelsMargin;
final int labelsXNearButton = mLabelsPosition == LABELS_ON_LEFT_SIDE
? buttonsHorizontalCenter - labelsOffset
: buttonsHorizontalCenter + labelsOffset;
int nextY = expandUp ?
addButtonY - mButtonSpacing :
addButtonY + mAddButton.getMeasuredHeight() + mButtonSpacing;
for (int i = mButtonsCount - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child == mAddButton || child.getVisibility() == GONE) continue;
final int childX = buttonsHorizontalCenter - child.getMeasuredWidth() / 2;
final int childY = expandUp ? nextY - child.getMeasuredHeight() : nextY;
child.layout(childX, childY, childX + child.getMeasuredWidth(), childY + child.getMeasuredHeight());
final float collapsedTranslation = addButtonY - childY;
final float expandedTranslation = 0f;
child.setTranslationY(mExpanded ? expandedTranslation : collapsedTranslation);
child.setAlpha(mExpanded ? 1f : 0f);
final LayoutParams params = (LayoutParams) child.getLayoutParams();
params.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation);
params.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation);
params.setAnimationsTarget(child);
final View label = (View) child.getTag(R.id.fab_label);
if (label != null) {
final int labelXAwayFromButton = mLabelsPosition == LABELS_ON_LEFT_SIDE
? labelsXNearButton - label.getMeasuredWidth()
: labelsXNearButton + label.getMeasuredWidth();
final int labelLeft = mLabelsPosition == LABELS_ON_LEFT_SIDE
? labelXAwayFromButton
: labelsXNearButton;
final int labelRight = mLabelsPosition == LABELS_ON_LEFT_SIDE
? labelsXNearButton
: labelXAwayFromButton;
final int labelTop = childY - mLabelsVerticalOffset + (child.getMeasuredHeight() - label.getMeasuredHeight()) / 2;
label.layout(labelLeft, labelTop, labelRight, labelTop + label.getMeasuredHeight());
touchArea.set(Math.min(childX, labelLeft),
childY - mButtonSpacing / 2,
Math.max(childX + child.getMeasuredWidth(), labelRight),
childY + child.getMeasuredHeight() + mButtonSpacing / 2);
mTouchDelegateGroup.addTouchDelegate(new TouchDelegate(new Rect(touchArea), child));
label.setTranslationY(mExpanded ? expandedTranslation : collapsedTranslation);
label.setAlpha(mExpanded ? 1f : 0f);
final LayoutParams labelParams = (LayoutParams) label.getLayoutParams();
labelParams.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation);
labelParams.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation);
labelParams.setAnimationsTarget(label);
}
nextY = expandUp ?
childY - mButtonSpacing :
childY + child.getMeasuredHeight() + mButtonSpacing;
}
break;
case EXPAND_LEFT:
case EXPAND_RIGHT:
final boolean expandLeft = mExpandDirection == EXPAND_LEFT;
final int addButtonX = expandLeft ? r - l - mAddButton.getMeasuredWidth() : 0;
// Ensure mAddButton is centered on the line where the buttons should be
final int addButtonTop = b - t - mMaxButtonHeight + (mMaxButtonHeight - mAddButton.getMeasuredHeight()) / 2;
mAddButton.layout(addButtonX, addButtonTop, addButtonX + mAddButton.getMeasuredWidth(), addButtonTop + mAddButton.getMeasuredHeight());
int nextX = expandLeft ?
addButtonX - mButtonSpacing :
addButtonX + mAddButton.getMeasuredWidth() + mButtonSpacing;
for (int i = mButtonsCount - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child == mAddButton || child.getVisibility() == GONE) continue;
final int childX = expandLeft ? nextX - child.getMeasuredWidth() : nextX;
final int childY = addButtonTop + (mAddButton.getMeasuredHeight() - child.getMeasuredHeight()) / 2;
child.layout(childX, childY, childX + child.getMeasuredWidth(), childY + child.getMeasuredHeight());
final float collapsedTranslation = addButtonX - childX;
final float expandedTranslation = 0f;
child.setTranslationX(mExpanded ? expandedTranslation : collapsedTranslation);
child.setAlpha(mExpanded ? 1f : 0f);
final LayoutParams params = (LayoutParams) child.getLayoutParams();
params.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation);
params.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation);
params.setAnimationsTarget(child);
nextX = expandLeft ?
childX - mButtonSpacing :
childX + child.getMeasuredWidth() + mButtonSpacing;
}
break;
}
}
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
int width = 0;
int height = 0;
mMaxButtonWidth = 0;
mMaxButtonHeight = 0;
int maxLabelWidth = 0;
for (int i = 0; i < mButtonsCount; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
switch (mExpandDirection) {
case EXPAND_UP:
case EXPAND_DOWN:
mMaxButtonWidth = Math.max(mMaxButtonWidth, child.getMeasuredWidth());
height += child.getMeasuredHeight();
break;
case EXPAND_LEFT:
case EXPAND_RIGHT:
width += child.getMeasuredWidth();
mMaxButtonHeight = Math.max(mMaxButtonHeight, child.getMeasuredHeight());
break;
}
if (!expandsHorizontally()) {
final TextView label = (TextView) child.getTag(R.id.fab_label);
if (label != null) {
maxLabelWidth = Math.max(maxLabelWidth, label.getMeasuredWidth());
}
}
}
if (expandsHorizontally()) {
height = mMaxButtonHeight;
} else {
width = mMaxButtonWidth + (maxLabelWidth > 0 ? maxLabelWidth + mLabelsMargin : 0);
}
switch (mExpandDirection) {
case EXPAND_UP:
case EXPAND_DOWN:
height += mButtonSpacing * (mButtonsCount - 1);
height = adjustForOvershoot(height);
break;
case EXPAND_LEFT:
case EXPAND_RIGHT:
width += mButtonSpacing * (mButtonsCount - 1);
width = adjustForOvershoot(width);
break;
}
setMeasuredDimension(width, height);
}
@Override
public void onRestoreInstanceState(final Parcelable state) {
if (state instanceof SavedState) {
final SavedState savedState = (SavedState) state;
mExpanded = savedState.mExpanded;
mTouchDelegateGroup.setEnabled(mExpanded);
if (mRotatingDrawable != null) {
mRotatingDrawable.setRotation(mExpanded ? EXPANDED_PLUS_ROTATION : COLLAPSED_PLUS_ROTATION);
}
super.onRestoreInstanceState(savedState.getSuperState());
} else {
super.onRestoreInstanceState(state);
}
}
@Override
public Parcelable onSaveInstanceState() {
final Parcelable superState = super.onSaveInstanceState();
final SavedState savedState = new SavedState(superState);
savedState.mExpanded = mExpanded;
return savedState;
}
public void removeButton(final LabeledFloatingActionButton button) {
removeView(button.getLabelView());
removeView(button);
button.setTag(R.id.fab_label, null);
mButtonsCount--;
}
public void setBehaviorYTranslation(final float behaviorYTranslation) {
this.behaviorYTranslation = behaviorYTranslation;
setTranslationY(behaviorYTranslation + scrollYTranslation);
}
@Override
public void setEnabled(final boolean enabled) {
super.setEnabled(enabled);
mAddButton.setEnabled(enabled);
}
public void setOnFloatingActionsMenuUpdateListener(final OnFloatingActionsMenuUpdateListener listener) {
mListener = listener;
}
public void setScrollYTranslation(final float scrollYTranslation) {
this.scrollYTranslation = scrollYTranslation;
setTranslationY(behaviorYTranslation + scrollYTranslation);
}
public void toggle() {
if (mExpanded) {
collapse();
} else {
expand();
}
}
public interface OnFloatingActionsMenuUpdateListener {
void onMenuCollapsed();
void onMenuExpanded();
}
private static class RotatingDrawable extends LayerDrawable {
private float mRotation;
RotatingDrawable(final Drawable drawable) {
super(new Drawable[]{drawable});
}
@Override
public void draw(final Canvas canvas) {
canvas.save();
canvas.rotate(mRotation, getBounds().centerX(), getBounds().centerY());
super.draw(canvas);
canvas.restore();
}
@SuppressWarnings("UnusedDeclaration")
public float getRotation() {
return mRotation;
}
@Keep
@SuppressWarnings("UnusedDeclaration")
public void setRotation(final float rotation) {
mRotation = rotation;
invalidateSelf();
}
}
public static class SavedState extends BaseSavedState {
public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
@Override
public SavedState createFromParcel(final Parcel in) {
return new SavedState(in);
}
@Override
public SavedState[] newArray(final int size) {
return new SavedState[size];
}
};
private boolean mExpanded;
public SavedState(final Parcelable parcel) {
super(parcel);
}
private SavedState(final Parcel in) {
super(in);
mExpanded = in.readInt() == 1;
}
@Override
public void writeToParcel(final Parcel out, final int flags) {
super.writeToParcel(out, flags);
out.writeInt(mExpanded ? 1 : 0);
}
}
private class LayoutParams extends ViewGroup.LayoutParams {
private final ObjectAnimator mCollapseAlpha = new ObjectAnimator();
private final ObjectAnimator mCollapseDir = new ObjectAnimator();
private final ObjectAnimator mExpandAlpha = new ObjectAnimator();
private final ObjectAnimator mExpandDir = new ObjectAnimator();
private boolean animationsSetToPlay;
LayoutParams(final ViewGroup.LayoutParams source) {
super(source);
mExpandDir.setInterpolator(EXPAND_INTERPOLATOR);
mExpandAlpha.setInterpolator(ALPHA_EXPAND_INTERPOLATOR);
mCollapseDir.setInterpolator(COLLAPSE_INTERPOLATOR);
mCollapseAlpha.setInterpolator(COLLAPSE_INTERPOLATOR);
mCollapseAlpha.setProperty(View.ALPHA);
mCollapseAlpha.setFloatValues(1f, 0f);
mExpandAlpha.setProperty(View.ALPHA);
mExpandAlpha.setFloatValues(0f, 1f);
switch (mExpandDirection) {
case EXPAND_UP:
case EXPAND_DOWN:
mCollapseDir.setProperty(View.TRANSLATION_Y);
mExpandDir.setProperty(View.TRANSLATION_Y);
break;
case EXPAND_LEFT:
case EXPAND_RIGHT:
mCollapseDir.setProperty(View.TRANSLATION_X);
mExpandDir.setProperty(View.TRANSLATION_X);
break;
}
}
private void addLayerTypeListener(final Animator animator, final View view) {
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(final Animator animation) {
view.setLayerType(LAYER_TYPE_NONE, null);
}
@Override
public void onAnimationStart(final Animator animation) {
view.setLayerType(LAYER_TYPE_HARDWARE, null);
}
});
}
public void setAnimationsTarget(final View view) {
mCollapseAlpha.setTarget(view);
mCollapseDir.setTarget(view);
mExpandAlpha.setTarget(view);
mExpandDir.setTarget(view);
// Now that the animations have targets, set them to be played
if (!animationsSetToPlay) {
addLayerTypeListener(mExpandDir, view);
addLayerTypeListener(mCollapseDir, view);
mCollapseAnimation.play(mCollapseAlpha);
mCollapseAnimation.play(mCollapseDir);
mExpandAnimation.play(mExpandAlpha);
mExpandAnimation.play(mExpandDir);
animationsSetToPlay = true;
}
}
}
}
@@ -1,27 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget.fab;
import androidx.recyclerview.widget.RecyclerView;
public class FloatingActionsMenuRecyclerViewScrollListener extends RecyclerView.OnScrollListener {
private static final float SCALE_FACTOR = 1.5f;
private final FloatingActionsMenu menu;
public FloatingActionsMenuRecyclerViewScrollListener(final FloatingActionsMenu menu) {
this.menu = menu;
}
private static float bound(final float min, final float proposal, final float max) {
return Math.min(max, Math.max(min, proposal));
}
@Override
public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) {
super.onScrolled(recyclerView, dx, dy);
menu.setScrollYTranslation(bound(0, menu.getScrollYTranslation() + dy * SCALE_FACTOR, menu.getMeasuredHeight() - menu.getTranslationY()));
}
}
@@ -1,58 +0,0 @@
/*
* Copyright © 2014 Jerzy Chalupski
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget.fab;
import android.content.Context;
import android.content.res.TypedArray;
import androidx.annotation.Nullable;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import android.util.AttributeSet;
import android.widget.TextView;
import com.wireguard.android.R;
public class LabeledFloatingActionButton extends FloatingActionButton {
@Nullable private final String title;
public LabeledFloatingActionButton(final Context context) {
this(context, null);
}
public LabeledFloatingActionButton(final Context context, @Nullable final AttributeSet attrs) {
this(context, attrs, 0);
}
public LabeledFloatingActionButton(final Context context, @Nullable final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
final TypedArray attr = context.obtainStyledAttributes(attrs, R.styleable.LabeledFloatingActionButton, 0, 0);
title = attr.getString(R.styleable.LabeledFloatingActionButton_fab_title);
attr.recycle();
}
@Nullable
TextView getLabelView() {
return (TextView) getTag(R.id.fab_label);
}
@Nullable
public String getTitle() {
return title;
}
@Override
public void setVisibility(final int visibility) {
final TextView label = getLabelView();
if (label != null) {
label.setVisibility(visibility);
}
super.setVisibility(visibility);
}
}
@@ -1,78 +0,0 @@
/*
* Copyright © 2014 Jerzy Chalupski
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget.fab;
import android.graphics.Rect;
import androidx.annotation.Nullable;
import android.view.MotionEvent;
import android.view.TouchDelegate;
import android.view.View;
import java.util.ArrayList;
import java.util.Collection;
public class TouchDelegateGroup extends TouchDelegate {
private static final Rect USELESS_HACKY_RECT = new Rect();
private final Collection<TouchDelegate> mTouchDelegates = new ArrayList<>();
@Nullable private TouchDelegate mCurrentTouchDelegate;
private boolean mEnabled;
public TouchDelegateGroup(final View uselessHackyView) {
super(USELESS_HACKY_RECT, uselessHackyView);
}
public void addTouchDelegate(final TouchDelegate touchDelegate) {
mTouchDelegates.add(touchDelegate);
}
public void clearTouchDelegates() {
mTouchDelegates.clear();
mCurrentTouchDelegate = null;
}
@Override
public boolean onTouchEvent(final MotionEvent event) {
if (!mEnabled)
return false;
TouchDelegate delegate = null;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
for (final TouchDelegate touchDelegate : mTouchDelegates) {
if (touchDelegate.onTouchEvent(event)) {
mCurrentTouchDelegate = touchDelegate;
return true;
}
}
break;
case MotionEvent.ACTION_MOVE:
delegate = mCurrentTouchDelegate;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
delegate = mCurrentTouchDelegate;
mCurrentTouchDelegate = null;
break;
}
return delegate != null && delegate.onTouchEvent(event);
}
public void removeTouchDelegate(final TouchDelegate touchDelegate) {
mTouchDelegates.remove(touchDelegate);
if (mCurrentTouchDelegate == touchDelegate) {
mCurrentTouchDelegate = null;
}
}
public void setEnabled(final boolean enabled) {
mEnabled = enabled;
}
}
@@ -1,64 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.config;
import android.os.Build;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
/**
* Utility methods for creating instances of {@link InetAddress}.
*/
public final class InetAddresses {
private static Method PARSER_METHOD;
private static Method getParserMethod() {
if (PARSER_METHOD != null)
return PARSER_METHOD;
try {
// This method is only present on Android.
// noinspection JavaReflectionMemberAccess
PARSER_METHOD = InetAddress.class.getMethod("parseNumericAddress", String.class);
} catch (final NoSuchMethodException e) {
throw new RuntimeException(e);
}
return PARSER_METHOD;
}
private InetAddresses() {
// Prevent instantiation.
}
/**
* Parses a numeric IPv4 or IPv6 address without performing any DNS lookups.
*
* @param address a string representing the IP address
* @return an instance of {@link Inet4Address} or {@link Inet6Address}, as appropriate
*/
public static InetAddress parse(final String address) throws ParseException {
if (address.isEmpty())
throw new ParseException(InetAddress.class, address, "Empty address");
try {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
return (InetAddress) getParserMethod().invoke(null, address);
else
return android.net.InetAddresses.parseNumericAddress(address);
} catch (final IllegalAccessException | InvocationTargetException e) {
final Throwable cause = e.getCause();
// Re-throw parsing exceptions with the original type, as callers might try to catch
// them. On the other hand, callers cannot be expected to handle reflection failures.
if (cause instanceof IllegalArgumentException)
throw new ParseException(InetAddress.class, address, cause);
throw new RuntimeException(e);
} catch (final IllegalArgumentException e) {
throw new ParseException(InetAddress.class, address, e);
}
}
}
@@ -1,14 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.util;
/**
* Interface for objects that have a identifying key of the given type.
*/
public interface Keyed<K> {
K getKey();
}
@@ -1,32 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.util;
import androidx.annotation.Nullable;
import java.util.Collection;
import java.util.List;
/**
* A list containing elements that can be looked up by key. A {@code KeyedList} cannot contain
* {@code null} elements.
*/
public interface KeyedList<K, E extends Keyed<? extends K>> extends List<E> {
boolean containsAllKeys(Collection<K> keys);
boolean containsKey(K key);
@Nullable
E get(K key);
@Nullable
E getLast(K key);
int indexOfKey(K key);
int lastIndexOfKey(K key);
}
@@ -1,31 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.util;
import androidx.annotation.Nullable;
import java.util.Collection;
import java.util.Comparator;
import java.util.Set;
/**
* A keyed list where all elements are sorted by the comparator returned by {@code comparator()}
* applied to their keys.
*/
public interface SortedKeyedList<K, E extends Keyed<? extends K>> extends KeyedList<K, E> {
Comparator<? super K> comparator();
@Nullable
K firstKey();
Set<K> keySet();
@Nullable
K lastKey();
Collection<E> values();
}
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<padding
android:bottom="4dp"
android:left="8dp"
android:right="8dp"
android:top="4dp" />
<solid android:color="@color/fab_label_background_color" />
</shape>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="?android:attr/colorForeground"
android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6L6,2zM13,9L13,3.5L18.5,9L13,9z" />
</vector>
@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/master_detail_wrapper"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:divider="?attr/dividerHorizontal"
android:orientation="horizontal"
android:showDividers="middle"
tools:context=".activity.MainActivity">
<fragment
android:name="com.wireguard.android.fragment.TunnelListFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
android:tag="LIST" />
<FrameLayout
android:id="@+id/detail_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3" />
</LinearLayout>
@@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<import type="com.wireguard.android.model.ApplicationData" />
<variable
name="fragment"
type="com.wireguard.android.fragment.AppListDialogFragment" />
<variable
name="appData"
type="com.wireguard.android.util.ObservableKeyedList&lt;String, ApplicationData&gt;" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="200dp">
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="@{appData.isEmpty() ? View.VISIBLE : View.GONE}" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/app_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:items="@{appData}"
app:layout="@{@layout/app_list_item}" />
</FrameLayout>
</layout>
@@ -1,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" />
</data>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/colorBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:background="?android:attr/colorBackground"
app:cardCornerRadius="4dp"
app:cardElevation="2dp"
app:contentPadding="8dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/interface_title"
style="?android:attr/textAppearanceMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:text="@string/interface_title" />
<com.wireguard.android.widget.ToggleSwitch
android:id="@+id/tunnel_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/interface_title"
android:layout_alignParentEnd="true"
app:checked="@{tunnel.state == State.UP}"
app:onBeforeCheckedChanged="@{fragment::setTunnelState}" />
<TextView
android:id="@+id/interface_name_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/interface_title"
android:layout_marginTop="8dp"
android:labelFor="@+id/interface_name_text"
android:text="@string/name" />
<TextView
android:id="@+id/interface_name_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/interface_name_label"
android:text="@{tunnel.name}" />
<TextView
android:id="@+id/public_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/interface_name_text"
android:layout_marginTop="8dp"
android:labelFor="@+id/public_key_text"
android:text="@string/public_key" />
<TextView
android:id="@+id/public_key_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/public_key_label"
android:contentDescription="@string/public_key_description"
android:ellipsize="end"
android:maxLines="1"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{config.interface.keyPair.publicKey.toBase64}" />
<TextView
android:id="@+id/addresses_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/public_key_text"
android:layout_marginTop="8dp"
android:labelFor="@+id/addresses_text"
android:text="@string/addresses" />
<TextView
android:id="@+id/addresses_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/addresses_label"
android:contentDescription="@string/addresses"
android:text="@{config.interface.addresses}" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
<LinearLayout
android: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" />
</data>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:background="?android:attr/colorBackground"
app:cardCornerRadius="4dp"
app:cardElevation="2dp"
app:contentPadding="8dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/peer_title"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:text="@string/peer" />
<TextView
android:id="@+id/public_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/peer_title"
android:layout_marginTop="8dp"
android:labelFor="@+id/public_key_text"
android:text="@string/public_key" />
<TextView
android:id="@+id/public_key_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/public_key_label"
android:contentDescription="@string/public_key_description"
android:ellipsize="end"
android:maxLines="1"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{item.publicKey.toBase64}" />
<TextView
android:id="@+id/allowed_ips_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/public_key_text"
android:layout_marginTop="8dp"
android:labelFor="@+id/allowed_ips_text"
android:text="@string/allowed_ips" />
<TextView
android:id="@+id/allowed_ips_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/allowed_ips_label"
android:text="@{item.allowedIps}" />
<TextView
android:id="@+id/endpoint_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/allowed_ips_text"
android:layout_marginTop="8dp"
android:labelFor="@+id/endpoint_text"
android:text="@string/endpoint" />
<TextView
android:id="@+id/endpoint_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/endpoint_label"
android:text="@{item.endpoint}" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
</layout>
@@ -1,254 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.wireguard.android.util.ClipboardUtils" />
<import type="com.wireguard.android.widget.KeyInputFilter" />
<import type="com.wireguard.android.widget.NameInputFilter" />
<variable
name="fragment"
type="com.wireguard.android.fragment.TunnelEditorFragment" />
<variable
name="config"
type="com.wireguard.android.viewmodel.ConfigProxy" />
<variable
name="name"
type="String" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/main_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/colorBackground">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:background="?android:attr/colorBackground"
app:cardCornerRadius="4dp"
app:cardElevation="2dp"
app:contentPadding="8dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/interface_title"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:text="@string/interface_title" />
<TextView
android:id="@+id/interface_name_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/interface_title"
android:layout_marginTop="8dp"
android:labelFor="@+id/interface_name_text"
android:text="@string/name" />
<EditText
android:id="@+id/interface_name_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/interface_name_label"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={name}"
app:filter="@{NameInputFilter.newInstance()}" />
<TextView
android:id="@+id/private_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/interface_name_text"
android:labelFor="@+id/private_key_text"
android:text="@string/private_key" />
<EditText
android:id="@+id/private_key_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/private_key_label"
android:layout_toStartOf="@+id/generate_private_key_button"
android:contentDescription="@string/public_key_description"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={config.interface.privateKey}"
app:filter="@{KeyInputFilter.newInstance()}" />
<Button
android:id="@+id/generate_private_key_button"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="96dp"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/private_key_text"
android:layout_alignParentEnd="true"
android:layout_below="@+id/private_key_label"
android:onClick="@{() -> config.interface.generateKeyPair()}"
android:text="@string/generate" />
<TextView
android:id="@+id/public_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/private_key_text"
android:labelFor="@+id/public_key_text"
android:text="@string/public_key" />
<TextView
android:id="@+id/public_key_text"
style="?attr/editTextStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/public_key_label"
android:contentDescription="@string/public_key_description"
android:ellipsize="end"
android:focusable="false"
android:hint="@string/hint_generated"
android:maxLines="1"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{config.interface.publicKey}" />
<TextView
android:id="@+id/addresses_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/public_key_text"
android:layout_toStartOf="@+id/listen_port_label"
android:labelFor="@+id/addresses_text"
android:text="@string/addresses" />
<EditText
android:id="@+id/addresses_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/addresses_label"
android:layout_toStartOf="@+id/listen_port_text"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={config.interface.addresses}" />
<TextView
android:id="@+id/listen_port_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/addresses_label"
android:layout_alignParentEnd="true"
android:layout_alignStart="@+id/generate_private_key_button"
android:labelFor="@+id/listen_port_text"
android:text="@string/listen_port" />
<EditText
android:id="@+id/listen_port_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/addresses_text"
android:layout_alignParentEnd="true"
android:layout_alignStart="@+id/generate_private_key_button"
android:hint="@string/hint_random"
android:inputType="number"
android:text="@={config.interface.listenPort}"
android:textAlignment="center" />
<TextView
android:id="@+id/dns_servers_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/addresses_text"
android:layout_toStartOf="@+id/mtu_label"
android:labelFor="@+id/dns_servers_text"
android:text="@string/dns_servers" />
<EditText
android:id="@+id/dns_servers_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/dns_servers_label"
android:layout_toStartOf="@+id/mtu_text"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={config.interface.dnsServers}" />
<TextView
android:id="@+id/mtu_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/dns_servers_label"
android:layout_alignParentEnd="true"
android:layout_alignStart="@+id/generate_private_key_button"
android:labelFor="@+id/mtu_text"
android:text="@string/mtu" />
<EditText
android:id="@+id/mtu_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/dns_servers_text"
android:layout_alignParentEnd="true"
android:layout_alignStart="@+id/generate_private_key_button"
android:hint="@string/hint_automatic"
android:inputType="number"
android:text="@={config.interface.mtu}"
android:textAlignment="center" />
<Button
android:id="@+id/set_excluded_applications"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/dns_servers_text"
android:layout_marginLeft="-8dp"
android:onClick="@{fragment::onRequestSetExcludedApplications}"
android:text="@{@plurals/set_excluded_applications(config.interface.excludedApplications.size, config.interface.excludedApplications.size)}" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="@null"
android:orientation="vertical"
app:items="@{config.peers}"
app:layout="@{@layout/tunnel_editor_peer}"
tools:ignore="UselessLeaf" />
<Button
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginEnd="4dp"
android:layout_marginStart="4dp"
android:onClick="@{() -> config.addPeer()}"
android:text="@string/add_peer" />
</LinearLayout>
</ScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
@@ -1,161 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<import type="com.wireguard.android.widget.KeyInputFilter" />
<variable
name="collection"
type="androidx.databinding.ObservableList&lt;com.wireguard.android.viewmodel.PeerProxy&gt;" />
<variable
name="item"
type="com.wireguard.android.viewmodel.PeerProxy" />
</data>
<androidx.cardview.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:background="?android:attr/colorBackground"
app:cardCornerRadius="4dp"
app:cardElevation="2dp"
app:contentPadding="8dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/peer_title"
style="?android:attr/textAppearanceMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginBottom="8dp"
android:layout_toStartOf="@+id/peer_action_delete"
android:text="@string/peer" />
<ImageButton
android:id="@+id/peer_action_delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:background="@null"
android:contentDescription="@string/delete"
android:onClick="@{() -> item.unbind()}"
android:src="@drawable/ic_action_delete" />
<TextView
android:id="@+id/public_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/peer_title"
android:labelFor="@+id/public_key_text"
android:text="@string/public_key" />
<EditText
android:id="@+id/public_key_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/public_key_label"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={item.publicKey}"
app:filter="@{KeyInputFilter.newInstance()}" />
<TextView
android:id="@+id/pre_shared_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/public_key_text"
android:labelFor="@+id/pre_shared_key_text"
android:text="@string/pre_shared_key" />
<EditText
android:id="@+id/pre_shared_key_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/pre_shared_key_label"
android:hint="@string/hint_optional"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={item.preSharedKey}" />
<TextView
android:id="@+id/allowed_ips_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/pre_shared_key_text"
android:layout_toStartOf="@+id/exclude_private_ips"
android:labelFor="@+id/allowed_ips_text"
android:text="@string/allowed_ips" />
<CheckBox
android:id="@+id/exclude_private_ips"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/allowed_ips_label"
android:layout_alignParentEnd="true"
android:checked="@={item.excludingPrivateIps}"
android:text="@string/exclude_private_ips"
android:visibility="@{item.ableToExcludePrivateIps ? View.VISIBLE : View.GONE}" />
<EditText
android:id="@+id/allowed_ips_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/allowed_ips_label"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={item.allowedIps}" />
<TextView
android:id="@+id/endpoint_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/allowed_ips_text"
android:layout_toStartOf="@+id/persistent_keepalive_label"
android:labelFor="@+id/endpoint_text"
android:text="@string/endpoint" />
<EditText
android:id="@+id/endpoint_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/endpoint_label"
android:layout_toStartOf="@+id/persistent_keepalive_text"
android:inputType="textNoSuggestions|textVisiblePassword"
android:text="@={item.endpoint}" />
<TextView
android:id="@+id/persistent_keepalive_label"
android:layout_width="96dp"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/endpoint_label"
android:layout_alignParentEnd="true"
android:labelFor="@+id/persistent_keepalive_text"
android:text="@string/persistent_keepalive" />
<EditText
android:id="@+id/persistent_keepalive_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/endpoint_text"
android:layout_alignParentEnd="true"
android:layout_alignStart="@+id/persistent_keepalive_label"
android:hint="@string/hint_optional"
android:inputType="number"
android:text="@={item.persistentKeepalive}"
android:textAlignment="center" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
</layout>
-4
View File
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="label_position">1</integer>
</resources>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- TODO(msf): remove these 2 hard-coded colors and replace with theme colors -->
<color name="fab_label_text_color">#000000</color>
<color name="fab_label_background_color">#bbbbbb</color>
<color name="accent">#5e97f6</color>
</resources>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="Multiselected">
<attr name="state_multiselected" format="boolean" />
</declare-styleable>
</resources>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- TODO(msf): remove these 2 hard-coded colors and replace with theme colors -->
<color name="fab_label_text_color">#ffffff</color>
<color name="fab_label_background_color">#444444</color>
<color name="accent">#2196F3</color>
</resources>
-4
View File
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="fab_margin">16dp</dimen>
</resources>
-31
View File
@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="fab_expand_menu_button" type="id" />
<item name="fab_label" type="id" />
<dimen name="fab_shadow_offset">3dp</dimen>
<dimen name="fab_shadow_radius">9dp</dimen>
<dimen name="fab_stroke_width">1dp</dimen>
<dimen name="fab_actions_spacing">24dp</dimen>
<dimen name="fab_labels_margin">8dp</dimen>
<declare-styleable name="LabeledFloatingActionButton">
<attr name="fab_title" format="string" />
</declare-styleable>
<declare-styleable name="FloatingActionsMenu">
<attr name="fab_labelStyle" format="reference" />
<attr name="fab_labelsPosition" format="enum">
<enum name="left" value="0" />
<enum name="right" value="1" />
</attr>
<attr name="fab_expandDirection" format="enum">
<enum name="up" value="0" />
<enum name="down" value="1" />
<enum name="left" value="2" />
<enum name="right" value="3" />
</attr>
</declare-styleable>
<integer name="label_position">0</integer>
</resources>
-13
View File
@@ -1,13 +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="fab_label" parent="TextAppearance.AppCompat.Inverse">
<item name="android:background">@drawable/fab_label_background</item>
<item name="android:textColor">@color/fab_label_text_color</item>
</style>
</resources>
-19
View File
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<com.wireguard.android.preference.VersionPreference android:icon="@mipmap/ic_launcher" />
<CheckBoxPreference
android:defaultValue="false"
android:key="restore_on_boot"
android:summary="@string/restore_on_boot_summary"
android:title="@string/restore_on_boot_title" />
<com.wireguard.android.preference.ToolsInstallerPreference android:key="tools_installer" />
<com.wireguard.android.preference.ZipExporterPreference />
<com.wireguard.android.preference.LogExporterPreference />
<CheckBoxPreference
android:defaultValue="false"
android:key="dark_theme"
android:summaryOff="@string/dark_theme_summary_off"
android:summaryOn="@string/dark_theme_summary_on"
android:title="@string/dark_theme_title" />
<com.wireguard.android.preference.DonatePreference />
</androidx.preference.PreferenceScreen>
-35
View File
@@ -1,35 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
#
# Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
BUILDDIR ?= $(CURDIR)/build
DESTDIR ?= $(CURDIR)/out
NDK_GO_ARCH_MAP_x86 := 386
NDK_GO_ARCH_MAP_x86_64 := amd64
NDK_GO_ARCH_MAP_arm := arm
NDK_GO_ARCH_MAP_arm64 := arm64
NDK_GO_ARCH_MAP_mips := mipsx
NDK_GO_ARCH_MAP_mips64 := mips64x
CLANG_FLAGS := --target=$(ANDROID_LLVM_TRIPLE) --gcc-toolchain=$(ANDROID_TOOLCHAIN_ROOT) --sysroot=$(ANDROID_SYSROOT)
export CGO_CFLAGS := $(CLANG_FLAGS) $(CFLAGS)
export CGO_LDFLAGS := $(CLANG_FLAGS) $(LDFLAGS)
export CC := $(ANDROID_C_COMPILER)
export GOARCH := $(NDK_GO_ARCH_MAP_$(ANDROID_ARCH_NAME))
export GOOS := android
export CGO_ENABLED := 1
DESIRED_GO_VERSION := 1.13.1
default: $(DESTDIR)/libwg-go.so
$(BUILDDIR)/go-$(DESIRED_GO_VERSION)/.prepared:
mkdir -p "$(dir $@)"
curl "https://dl.google.com/go/go$(DESIRED_GO_VERSION).$(shell uname -s | tr '[:upper:]' '[:lower:]')-$(NDK_GO_ARCH_MAP_$(shell uname -m)).tar.gz" | tar -C "$(dir $@)" --strip-components=1 -xzf -
patch -p1 -f -N -r- -d "$(dir $@)" < goruntime-boottime-over-monotonic.diff
touch "$@"
$(DESTDIR)/libwg-go.so: export PATH := $(BUILDDIR)/go-$(DESIRED_GO_VERSION)/bin/:$(PATH)
$(DESTDIR)/libwg-go.so: $(BUILDDIR)/go-$(DESIRED_GO_VERSION)/.prepared go.mod
go build -tags linux -ldflags="-X main.socketDirectory=/data/data/$(ANDROID_PACKAGE_NAME)/cache/wireguard" -v -trimpath -o "$@" -buildmode c-shared
-10
View File
@@ -1,10 +0,0 @@
module golang.zx2c4.com/wireguard/android
go 1.12
require (
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect
golang.org/x/net v0.0.0-20191011234655-491137f69257 // indirect
golang.org/x/sys v0.0.0-20191010194322-b09406accb47
golang.zx2c4.com/wireguard v0.0.20190908
)
-19
View File
@@ -1,19 +0,0 @@
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191011234655-491137f69257 h1:ry8e2D+cwaV6hk7lb3aRTjjZo24shrbK0e11QEOkTIg=
golang.org/x/net v0.0.0-20191011234655-491137f69257/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190830023255-19e00faab6ad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.zx2c4.com/wireguard v0.0.20190908 h1:SUoXDdwSMtomLdvke+zz83/u9tNvl4hHmcTIWp38tow=
golang.zx2c4.com/wireguard v0.0.20190908/go.mod h1:LhfXh5z6bLC2lW2ve6BzYZFwnnsXK3OQjySR0Yh2dO8=
+66 -5
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.5.0'
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()
@@ -21,7 +79,10 @@ task clean(type: Delete) {
tasks {
wrapper {
gradleVersion = "5.6.2"
gradleVersion = "6.7.1"
distributionType = Wrapper.DistributionType.ALL
distributionSha256Sum = "22449f5231796abd892c98b2a07c9ceebe4688d192cd2d6763f8e3bf8acbedeb"
}
}
apply from: "version.gradle"
+11
View File
@@ -17,3 +17,14 @@ org.gradle.jvmargs=-Xmx1536m
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# 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-5.6.2-all.zip
distributionSha256Sum=22449f5231796abd892c98b2a07c9ceebe4688d192cd2d6763f8e3bf8acbedeb
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+14 -17
View File
@@ -82,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -129,6 +130,7 @@ fi
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
@@ -154,19 +156,19 @@ if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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
@@ -175,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'
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
set -ex
curl -Lo - https://crowdin.com/backend/download/project/wireguard.zip | bsdtar -C ui/src/main/res -x -f - --strip-components 5 wireguard-android
find ui/src/main/res -name strings.xml -exec bash -c '[[ $(xmllint --xpath "count(//resources/*)" {}) -ne 0 ]] || rm -rf "$(dirname {})"' \;
+60
View File
@@ -0,0 +1,60 @@
apply plugin: 'com.android.library'
version wireguardVersionName
group groupName
android {
compileSdkVersion 30
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion 21
targetSdkVersion 30
versionCode wireguardVersionCode
versionName wireguardVersionName
}
externalNativeBuild {
cmake {
path 'tools/CMakeLists.txt'
}
}
libraryVariants.all {
it.generateBuildConfigProvider.configure { enabled = false }
}
testOptions.unitTests.all {
testLogging {
events 'passed', 'skipped', 'failed', 'standardOut', 'standardError'
}
}
buildTypes {
release {
externalNativeBuild {
cmake {
arguments "-DANDROID_PACKAGE_NAME=${groupName}", "-DGRADLE_USER_HOME=${project.gradle.gradleUserHomeDir}"
}
}
}
debug {
externalNativeBuild {
cmake {
arguments "-DANDROID_PACKAGE_NAME=${groupName}.debug", "-DGRADLE_USER_HOME=${project.gradle.gradleUserHomeDir}"
}
}
}
}
lintOptions {
disable('LongLogTag')
disable('NewApi') // Desugaring!
}
}
dependencies {
implementation "androidx.annotation:annotation:$annotationsVersion"
implementation "androidx.collection:collection:$collectionVersion"
implementation "com.google.code.findbugs:jsr305:$jsr305Version"
testImplementation "junit:junit:$junitVersion"
}
apply from: "publish.gradle"
+72
View File
@@ -0,0 +1,72 @@
apply plugin: 'maven-publish'
afterEvaluate {
publishing {
publications {
release(MavenPublication) {
groupId = groupName
artifactId = 'tunnel'
version wireguardVersionName
artifact sourcesJar
artifact javadocJar
from components.getByName("release")
pom {
name = 'WireGuard Tunnel Library'
description = 'Embeddable tunnel library for WireGuard for Android'
url = 'https://www.wireguard.com/'
licenses {
license {
name = 'The Apache Software License, Version 2.0'
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
distribution = 'repo'
}
}
scm {
connection = 'scm:git:https://git.zx2c4.com/wireguard-android'
developerConnection = 'scm:git:https://git.zx2c4.com/wireguard-android'
url = 'https://git.zx2c4.com/wireguard-android'
}
developers {
organization {
name = 'WireGuard'
url = 'https://www.wireguard.com/'
}
}
}
}
}
repositories {
maven {
name = "bintray"
url = uri("https://api.bintray.com/maven/wireguard/wireguard-android/wireguard-android/;publish=1;override=0")
credentials {
username = hasProperty('BINTRAY_USER') ? getProperty('BINTRAY_USER') : System.getenv('BINTRAY_USER')
password = hasProperty('BINTRAY_KEY') ? getProperty('BINTRAY_KEY') : System.getenv('BINTRAY_KEY')
}
}
}
}
}
android.libraryVariants.all { variant ->
if (variant.name == 'release') {
task javadoc(type: Javadoc) {
source = variant.javaCompileProvider.get().source
classpath = files((android.bootClasspath.join(File.pathSeparator)))
classpath += variant.javaCompileProvider.get().classpath
title = 'Embeddable WireGuard Tunnel for Android v$wireguardVersionName'
}
task javadocJar(type: Jar, dependsOn: javadoc) {
archiveClassifier = 'javadoc'
from javadoc.destinationDir
}
task sourcesJar(type: Jar) {
archiveClassifier = 'sources'
from android.sourceSets.main.java.srcDirs
}
}
}
+18
View File
@@ -0,0 +1,18 @@
<!--
~ Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
~ SPDX-License-Identifier: Apache-2.0
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.wireguard.android.tunnel">
<application>
<service
android:name="com.wireguard.android.backend.GoBackend$VpnService"
android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
</application>
</manifest>
@@ -5,43 +5,34 @@
package com.wireguard.android.backend;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.model.Tunnel.Statistics;
import com.wireguard.config.Config;
import com.wireguard.util.NonNullForAll;
import java.util.Set;
import androidx.annotation.Nullable;
/**
* Interface for implementations of the WireGuard secure network tunnel.
*/
@NonNullForAll
public interface Backend {
/**
* Update the volatile configuration of a running tunnel and return the resulting configuration.
* If the tunnel is not up, return the configuration that would result (if known), or else
* simply return the given configuration.
*
* @param tunnel The tunnel to apply the configuration to.
* @param config The new configuration for this tunnel.
* @return The updated configuration of the tunnel.
*/
Config applyConfig(Tunnel tunnel, Config config) throws Exception;
/**
* Enumerate the names of currently-running tunnels.
* Enumerate names of currently-running tunnels.
*
* @return The set of running tunnel names.
*/
Set<String> enumerate();
Set<String> getRunningTunnelNames();
/**
* Get the actual state of a tunnel.
* Get the state of a tunnel.
*
* @param tunnel The tunnel to examine the state of.
* @return The state of the tunnel.
* @throws Exception Exception raised when retrieving tunnel's state.
*/
State getState(Tunnel tunnel) throws Exception;
Tunnel.State getState(Tunnel tunnel) throws Exception;
/**
* Get statistics about traffic and errors on this tunnel. If the tunnel is not running, the
@@ -49,31 +40,28 @@ public interface Backend {
*
* @param tunnel The tunnel to retrieve statistics for.
* @return The statistics for the tunnel.
* @throws Exception Exception raised when retrieving statistics.
*/
Statistics getStatistics(Tunnel tunnel) throws Exception;
/**
* Determine type name of underlying backend.
*
* @return Type name
*/
String getTypePrettyName();
/**
* Determine version of underlying backend.
*
* @return The version of the backend.
* @throws Exception
* @throws Exception Exception raised while retrieving version.
*/
String getVersion() throws Exception;
/**
* Set the state of a tunnel.
* Set the state of a tunnel, updating it's configuration. If the tunnel is already up, config
* may update the running configuration; config may be null when setting the tunnel down.
*
* @param tunnel The tunnel to control the state of.
* @param state The new state for this tunnel. Must be {@code UP}, {@code DOWN}, or
* {@code TOGGLE}.
* @param config The configuration for this tunnel, may be null if state is {@code DOWN}.
* @return The updated state of the tunnel.
* @throws Exception Exception raised while changing state.
*/
State setState(Tunnel tunnel, State state) throws Exception;
Tunnel.State setState(Tunnel tunnel, Tunnel.State state, @Nullable Config config) throws Exception;
}
@@ -0,0 +1,60 @@
/*
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.backend;
import com.wireguard.util.NonNullForAll;
/**
* A subclass of {@link Exception} that encapsulates the reasons for a failure originating in
* implementations of {@link Backend}.
*/
@NonNullForAll
public final class BackendException extends Exception {
private final Object[] format;
private final Reason reason;
/**
* Public constructor for BackendException.
*
* @param reason The {@link Reason} which caused this exception to be thrown
* @param format Format string values used when converting exceptions to user-facing strings.
*/
public BackendException(final Reason reason, final Object... format) {
this.reason = reason;
this.format = format;
}
/**
* Get the format string values associated with the instance.
*
* @return Array of {@link Object} for string formatting purposes
*/
public Object[] getFormat() {
return format;
}
/**
* Get the reason for this exception.
*
* @return Associated {@link Reason} for this exception.
*/
public Reason getReason() {
return reason;
}
/**
* Enum class containing all known reasons for why a {@link BackendException} might be thrown.
*/
public enum Reason {
UNKNOWN_KERNEL_MODULE_NAME,
WG_QUICK_CONFIG_ERROR_CODE,
TUNNEL_MISSING_CONFIG,
VPN_NOT_AUTHORIZED,
UNABLE_TO_START_VPN,
TUN_CREATION_ERROR,
GO_ACTIVATION_ERROR_CODE
}
}
@@ -0,0 +1,392 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.backend;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.system.OsConstants;
import android.util.Log;
import com.wireguard.android.backend.BackendException.Reason;
import com.wireguard.android.backend.Tunnel.State;
import com.wireguard.android.util.SharedLibraryLoader;
import com.wireguard.config.Config;
import com.wireguard.config.InetNetwork;
import com.wireguard.config.Peer;
import com.wireguard.crypto.Key;
import com.wireguard.crypto.KeyFormatException;
import com.wireguard.util.NonNullForAll;
import java.net.InetAddress;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import androidx.annotation.Nullable;
import androidx.collection.ArraySet;
/**
* Implementation of {@link Backend} that uses the wireguard-go userspace implementation to provide
* WireGuard tunnels.
*/
@NonNullForAll
public final class GoBackend implements Backend {
private static final String TAG = "WireGuard/GoBackend";
@Nullable private static AlwaysOnCallback alwaysOnCallback;
private static GhettoCompletableFuture<VpnService> vpnService = new GhettoCompletableFuture<>();
private final Context context;
@Nullable private Config currentConfig;
@Nullable private Tunnel currentTunnel;
private int currentTunnelHandle = -1;
/**
* Public constructor for GoBackend.
*
* @param context An Android {@link Context}
*/
public GoBackend(final Context context) {
SharedLibraryLoader.loadSharedLibrary(context, "wg-go");
this.context = context;
}
/**
* Set a {@link AlwaysOnCallback} to be invoked when {@link VpnService} is started by the
* system's Always-On VPN mode.
*
* @param cb Callback to be invoked
*/
public static void setAlwaysOnCallback(final AlwaysOnCallback cb) {
alwaysOnCallback = cb;
}
private static native String wgGetConfig(int handle);
private static native int wgGetSocketV4(int handle);
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();
/**
* Method to get the names of running tunnels.
*
* @return A set of string values denoting names of running tunnels.
*/
@Override
public Set<String> getRunningTunnelNames() {
if (currentTunnel != null) {
final Set<String> runningTunnels = new ArraySet<>();
runningTunnels.add(currentTunnel.getName());
return runningTunnels;
}
return Collections.emptySet();
}
/**
* Get the associated {@link State} for a given {@link Tunnel}.
*
* @param tunnel The tunnel to examine the state of.
* @return {@link State} associated with the given tunnel.
*/
@Override
public State getState(final Tunnel tunnel) {
return currentTunnel == tunnel ? State.UP : State.DOWN;
}
/**
* Get the associated {@link Statistics} for a given {@link Tunnel}.
*
* @param tunnel The tunnel to retrieve statistics for.
* @return {@link Statistics} associated with the given tunnel.
*/
@Override
public Statistics getStatistics(final Tunnel tunnel) {
final Statistics stats = new Statistics();
if (tunnel != currentTunnel) {
return stats;
}
final String config = wgGetConfig(currentTunnelHandle);
Key key = null;
long rx = 0;
long tx = 0;
for (final String line : config.split("\\n")) {
if (line.startsWith("public_key=")) {
if (key != null)
stats.add(key, rx, tx);
rx = 0;
tx = 0;
try {
key = Key.fromHex(line.substring(11));
} catch (final KeyFormatException ignored) {
key = null;
}
} else if (line.startsWith("rx_bytes=")) {
if (key == null)
continue;
try {
rx = Long.parseLong(line.substring(9));
} catch (final NumberFormatException ignored) {
rx = 0;
}
} else if (line.startsWith("tx_bytes=")) {
if (key == null)
continue;
try {
tx = Long.parseLong(line.substring(9));
} catch (final NumberFormatException ignored) {
tx = 0;
}
}
}
if (key != null)
stats.add(key, rx, tx);
return stats;
}
/**
* Get the version of the underlying wireguard-go library.
*
* @return {@link String} value of the version of the wireguard-go library.
*/
@Override
public String getVersion() {
return wgVersion();
}
/**
* Change the state of a given {@link Tunnel}, optionally applying a given {@link Config}.
*
* @param tunnel The tunnel to control the state of.
* @param state The new state for this tunnel. Must be {@code UP}, {@code DOWN}, or
* {@code TOGGLE}.
* @param config The configuration for this tunnel, may be null if state is {@code DOWN}.
* @return {@link State} of the tunnel after state changes are applied.
* @throws Exception Exception raised while changing tunnel state.
*/
@Override
public State setState(final Tunnel tunnel, State state, @Nullable final Config config) throws Exception {
final State originalState = getState(tunnel);
if (state == State.TOGGLE)
state = originalState == State.UP ? State.DOWN : State.UP;
if (state == originalState && tunnel == currentTunnel && config == currentConfig)
return originalState;
if (state == State.UP) {
final Config originalConfig = currentConfig;
final Tunnel originalTunnel = currentTunnel;
if (currentTunnel != null)
setStateInternal(currentTunnel, null, State.DOWN);
try {
setStateInternal(tunnel, config, state);
} catch (final Exception e) {
if (originalTunnel != null)
setStateInternal(originalTunnel, originalConfig, State.UP);
throw e;
}
} else if (state == State.DOWN && tunnel == currentTunnel) {
setStateInternal(tunnel, null, State.DOWN);
}
return getState(tunnel);
}
private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state)
throws Exception {
Log.i(TAG, "Bringing tunnel " + tunnel.getName() + ' ' + state);
if (state == State.UP) {
if (config == null)
throw new BackendException(Reason.TUNNEL_MISSING_CONFIG);
if (VpnService.prepare(context) != null)
throw new BackendException(Reason.VPN_NOT_AUTHORIZED);
final VpnService service;
if (!vpnService.isDone()) {
Log.d(TAG, "Requesting to start VpnService");
context.startService(new Intent(context, VpnService.class));
}
try {
service = vpnService.get(2, TimeUnit.SECONDS);
} catch (final TimeoutException e) {
final Exception be = new BackendException(Reason.UNABLE_TO_START_VPN);
be.initCause(e);
throw be;
}
service.setOwner(this);
if (currentTunnelHandle != -1) {
Log.w(TAG, "Tunnel already up");
return;
}
// Build config
final String goConfig = config.toWgUserspaceString();
// Create the vpn tunnel with android API
final VpnService.Builder builder = service.getBuilder();
builder.setSession(tunnel.getName());
for (final String excludedApplication : config.getInterface().getExcludedApplications())
builder.addDisallowedApplication(excludedApplication);
for (final String includedApplication : config.getInterface().getIncludedApplications())
builder.addAllowedApplication(includedApplication);
for (final InetNetwork addr : config.getInterface().getAddresses())
builder.addAddress(addr.getAddress(), addr.getMask());
for (final InetAddress addr : config.getInterface().getDnsServers())
builder.addDnsServer(addr.getHostAddress());
boolean sawDefaultRoute = false;
for (final Peer peer : config.getPeers()) {
for (final InetNetwork addr : peer.getAllowedIps()) {
if (addr.getMask() == 0)
sawDefaultRoute = true;
builder.addRoute(addr.getAddress(), addr.getMask());
}
}
// "Kill-switch" semantics
if (!(sawDefaultRoute && config.getPeers().size() == 1)) {
builder.allowFamily(OsConstants.AF_INET);
builder.allowFamily(OsConstants.AF_INET6);
}
builder.setMtu(config.getInterface().getMtu().orElse(1280));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
builder.setMetered(false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
service.setUnderlyingNetworks(null);
builder.setBlocking(true);
try (final ParcelFileDescriptor tun = builder.establish()) {
if (tun == null)
throw new BackendException(Reason.TUN_CREATION_ERROR);
Log.d(TAG, "Go backend v" + wgVersion());
currentTunnelHandle = wgTurnOn(tunnel.getName(), tun.detachFd(), goConfig);
}
if (currentTunnelHandle < 0)
throw new BackendException(Reason.GO_ACTIVATION_ERROR_CODE, currentTunnelHandle);
currentTunnel = tunnel;
currentConfig = config;
service.protect(wgGetSocketV4(currentTunnelHandle));
service.protect(wgGetSocketV6(currentTunnelHandle));
} else {
if (currentTunnelHandle == -1) {
Log.w(TAG, "Tunnel already down");
return;
}
wgTurnOff(currentTunnelHandle);
currentTunnel = null;
currentTunnelHandle = -1;
currentConfig = null;
}
tunnel.onStateChange(state);
}
/**
* Callback for {@link GoBackend} that is invoked when {@link VpnService} is started by the
* system's Always-On VPN mode.
*/
public interface AlwaysOnCallback {
void alwaysOnTriggered();
}
// TODO: When we finally drop API 21 and move to API 24, delete this and replace with the ordinary CompletableFuture.
private static final class GhettoCompletableFuture<V> {
private final LinkedBlockingQueue<V> completion = new LinkedBlockingQueue<>(1);
private final FutureTask<V> result = new FutureTask<>(completion::peek);
public boolean complete(final V value) {
final boolean offered = completion.offer(value);
if (offered)
result.run();
return offered;
}
public V get() throws ExecutionException, InterruptedException {
return result.get();
}
public V get(final long timeout, final TimeUnit unit) throws ExecutionException, InterruptedException, TimeoutException {
return result.get(timeout, unit);
}
public boolean isDone() {
return !completion.isEmpty();
}
public GhettoCompletableFuture<V> newIncompleteFuture() {
return new GhettoCompletableFuture<>();
}
}
/**
* {@link android.net.VpnService} implementation for {@link GoBackend}
*/
public static class VpnService extends android.net.VpnService {
@Nullable private GoBackend owner;
public Builder getBuilder() {
return new Builder();
}
@Override
public void onCreate() {
vpnService.complete(this);
super.onCreate();
}
@Override
public void onDestroy() {
if (owner != null) {
final Tunnel tunnel = owner.currentTunnel;
if (tunnel != null) {
if (owner.currentTunnelHandle != -1)
wgTurnOff(owner.currentTunnelHandle);
owner.currentTunnel = null;
owner.currentTunnelHandle = -1;
owner.currentConfig = null;
tunnel.onStateChange(State.DOWN);
}
}
vpnService = vpnService.newIncompleteFuture();
super.onDestroy();
}
@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");
if (alwaysOnCallback != null)
alwaysOnCallback.alwaysOnTriggered();
}
return super.onStartCommand(intent, flags, startId);
}
public void setOwner(final GoBackend owner) {
this.owner = owner;
}
}
}
@@ -0,0 +1,114 @@
/*
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.backend;
import android.os.SystemClock;
import android.util.Pair;
import com.wireguard.crypto.Key;
import com.wireguard.util.NonNullForAll;
import java.util.HashMap;
import java.util.Map;
/**
* Class representing transfer statistics for a {@link Tunnel} instance.
*/
@NonNullForAll
public class Statistics {
private final Map<Key, Pair<Long, Long>> peerBytes = new HashMap<>();
private long lastTouched = SystemClock.elapsedRealtime();
Statistics() {
}
/**
* Add a peer and its current data usage to the internal map.
*
* @param key A WireGuard public key bound to a particular peer
* @param rx The received traffic for the {@link com.wireguard.config.Peer} referenced by
* the provided {@link Key}. This value is in bytes
* @param tx The transmitted traffic for the {@link com.wireguard.config.Peer} referenced by
* the provided {@link Key}. This value is in bytes.
*/
void add(final Key key, final long rx, final long tx) {
peerBytes.put(key, Pair.create(rx, tx));
lastTouched = SystemClock.elapsedRealtime();
}
/**
* Check if the statistics are stale, indicating the need for the {@link Backend} to update them.
*
* @return boolean indicating if the current statistics instance has stale values.
*/
public boolean isStale() {
return SystemClock.elapsedRealtime() - lastTouched > 900;
}
/**
* Get the received traffic (in bytes) for the {@link com.wireguard.config.Peer} referenced by
* the provided {@link Key}
*
* @param peer A {@link Key} representing a {@link com.wireguard.config.Peer}.
* @return a long representing the number of bytes received by this peer.
*/
public long peerRx(final Key peer) {
final Pair<Long, Long> rxTx = peerBytes.get(peer);
if (rxTx == null)
return 0;
return rxTx.first;
}
/**
* Get the transmitted traffic (in bytes) for the {@link com.wireguard.config.Peer} referenced by
* the provided {@link Key}
*
* @param peer A {@link Key} representing a {@link com.wireguard.config.Peer}.
* @return a long representing the number of bytes transmitted by this peer.
*/
public long peerTx(final Key peer) {
final Pair<Long, Long> rxTx = peerBytes.get(peer);
if (rxTx == null)
return 0;
return rxTx.second;
}
/**
* Get the list of peers being tracked by this instance.
*
* @return An array of {@link Key} instances representing WireGuard
* {@link com.wireguard.config.Peer}s
*/
public Key[] peers() {
return peerBytes.keySet().toArray(new Key[0]);
}
/**
* Get the total received traffic by all the peers being tracked by this instance
*
* @return a long representing the number of bytes received by the peers being tracked.
*/
public long totalRx() {
long rx = 0;
for (final Pair<Long, Long> val : peerBytes.values()) {
rx += val.first;
}
return rx;
}
/**
* Get the total transmitted traffic by all the peers being tracked by this instance
*
* @return a long representing the number of bytes transmitted by the peers being tracked.
*/
public long totalTx() {
long tx = 0;
for (final Pair<Long, Long> val : peerBytes.values()) {
tx += val.second;
}
return tx;
}
}

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