Compare commits

...

198 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
135 changed files with 8663 additions and 1797 deletions
+1
View File
@@ -15,3 +15,4 @@ build/
*.iml
*.jks
keystore.properties
gradlew.bat
+10 -1
View File
@@ -59,7 +59,16 @@
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="kotlinx.android.synthetic" withSubpackages="true" static="false" />
<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" />
+7 -1
View File
@@ -12,6 +12,8 @@ $ 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).
@@ -20,12 +22,16 @@ The tunnel library is [on JCenter](https://bintray.com/wireguard/wireguard-andro
implementation 'com.wireguard.android:tunnel:$wireguardTunnelVersion'
```
The library makes use of Java 8 features, so be sure to support those in your gradle configuration:
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"
}
```
+57 -27
View File
@@ -1,33 +1,24 @@
allprojects {
repositories {
google()
jcenter()
}
}
buildscript {
ext {
agpVersion = '3.6.1'
activityVersion = '1.2.0-beta02'
agpVersion = '4.1.1'
annotationsVersion = '1.1.0'
appcompatVersion = '1.1.0'
bintrayPluginVersion = '1.8.4'
biometricVersion = '1.0.1'
cardviewVersion = '1.0.0'
appcompatVersion = '1.2.0'
biometricVersion = '1.1.0-rc01'
collectionVersion = '1.1.0'
constraintLayoutVersion = '1.1.3'
constraintLayoutVersion = '2.0.4'
coordinatorLayoutVersion = '1.1.0'
coreKtxVersion = '1.2.0'
coroutinesVersion = '1.3.5'
eddsaVersion = '0.3.0'
fragmentVersion = '1.2.3'
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'
kotlinVersion = '1.3.71'
materialComponentsVersion = '1.1.0'
mavenPluginVersion = '2.1'
preferenceVersion = '1.1.0'
streamsupportVersion = '1.7.2'
threetenabpVersion = '1.2.3'
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'
@@ -35,8 +26,6 @@ buildscript {
dependencies {
classpath "com.android.tools.build:gradle:$agpVersion"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath "com.github.dcendents:android-maven-gradle-plugin:$mavenPluginVersion"
classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:$bintrayPluginVersion"
}
repositories {
google()
@@ -44,14 +33,55 @@ buildscript {
}
}
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()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
tasks {
wrapper {
gradleVersion = "6.2.2"
gradleVersion = "6.7.1"
distributionType = Wrapper.DistributionType.ALL
distributionSha256Sum = "22449f5231796abd892c98b2a07c9ceebe4688d192cd2d6763f8e3bf8acbedeb"
}
}
-3
View File
@@ -18,9 +18,6 @@ org.gradle.jvmargs=-Xmx1536m
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# R8 Full mode
android.enableR8.fullMode=true
# https://jakewharton.com/increased-accuracy-of-aapt2-keep-rules/
android.useMinimalKeepRules=true
Binary file not shown.
+2 -1
View File
@@ -1,5 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip
distributionSha256Sum=22449f5231796abd892c98b2a07c9ceebe4688d192cd2d6763f8e3bf8acbedeb
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+2
View File
@@ -82,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -129,6 +130,7 @@ fi
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
+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 {})"' \;
+9 -6
View File
@@ -4,15 +4,14 @@ version wireguardVersionName
group groupName
android {
buildToolsVersion '29.0.3'
compileSdkVersion 29
compileSdkVersion 30
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion 21
targetSdkVersion 29
targetSdkVersion 30
versionCode wireguardVersionCode
versionName wireguardVersionName
}
@@ -21,6 +20,9 @@ android {
path 'tools/CMakeLists.txt'
}
}
libraryVariants.all {
it.generateBuildConfigProvider.configure { enabled = false }
}
testOptions.unitTests.all {
testLogging {
events 'passed', 'skipped', 'failed', 'standardOut', 'standardError'
@@ -42,15 +44,16 @@ android {
}
}
}
lintOptions {
disable('LongLogTag')
disable('NewApi') // Desugaring!
}
}
dependencies {
api "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion"
implementation "androidx.annotation:annotation:$annotationsVersion"
implementation "androidx.collection:collection:$collectionVersion"
implementation "com.google.code.findbugs:jsr305:$jsr305Version"
implementation "com.jakewharton.threetenabp:threetenabp:$threetenabpVersion"
implementation "net.i2p.crypto:eddsa:$eddsaVersion"
testImplementation "junit:junit:$junitVersion"
}
+44 -48
View File
@@ -1,53 +1,53 @@
apply plugin: 'com.github.dcendents.android-maven'
apply plugin: 'com.jfrog.bintray'
apply plugin: 'maven-publish'
install {
repositories.mavenInstaller {
pom.project {
name 'WireGuard Tunnel Library'
description 'Embeddable tunnel library for WireGuard for Android'
url 'https://www.wireguard.com/'
afterEvaluate {
publishing {
publications {
release(MavenPublication) {
groupId = groupName
artifactId = 'tunnel'
version wireguardVersionName
packaging 'aar'
groupId groupName
artifactId 'tunnel'
version wireguardVersionName
artifact sourcesJar
artifact javadocJar
licenses {
license {
name 'The Apache Software License, Version 2.0'
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
distribution 'repo'
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/'
}
}
}
}
scm {
connection 'scm:git:https://git.zx2c4.com/wireguard-android'
url 'https://git.zx2c4.com/wireguard-android'
}
organization {
name 'WireGuard'
url 'https://www.wireguard.com/'
}
}
}
}
bintray {
user = hasProperty('BINTRAY_USER') ? getProperty('BINTRAY_USER') : System.getenv('BINTRAY_USER')
key = hasProperty('BINTRAY_KEY') ? getProperty('BINTRAY_KEY') : System.getenv('BINTRAY_KEY')
configurations = [ 'archives' ]
pkg {
repo = 'wireguard-android'
name = 'wireguard-android'
userOrg = 'wireguard'
licenses = [ 'Apache-2.0' ]
vcsUrl = 'https://git.zx2c4.com/wireguard-android'
publish = true
version {
name = wireguardVersionName
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')
}
}
}
}
}
@@ -68,9 +68,5 @@ android.libraryVariants.all { variant ->
archiveClassifier = 'sources'
from android.sourceSets.main.java.srcDirs
}
artifacts {
archives sourcesJar
archives javadocJar
}
}
}
@@ -30,6 +30,7 @@ public interface Backend {
*
* @param tunnel The tunnel to examine the state of.
* @return The state of the tunnel.
* @throws Exception Exception raised when retrieving tunnel's state.
*/
Tunnel.State getState(Tunnel tunnel) throws Exception;
@@ -39,6 +40,7 @@ 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;
@@ -46,7 +48,7 @@ public interface Backend {
* Determine version of underlying backend.
*
* @return The version of the backend.
* @throws Exception
* @throws Exception Exception raised while retrieving version.
*/
String getVersion() throws Exception;
@@ -59,6 +61,7 @@ public interface Backend {
* {@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.
*/
Tunnel.State setState(Tunnel tunnel, Tunnel.State state, @Nullable Config config) throws Exception;
}
@@ -7,24 +7,47 @@ 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,
@@ -9,6 +9,7 @@ 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;
@@ -33,6 +34,10 @@ 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";
@@ -43,11 +48,22 @@ public final class GoBackend implements Backend {
@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;
}
@@ -64,6 +80,11 @@ public final class GoBackend implements Backend {
private static native String wgVersion();
/**
* Method to get the names of running tunnels.
*
* @return A set of string values denoting names of running tunnels.
*/
@Override
public Set<String> getRunningTunnelNames() {
if (currentTunnel != null) {
@@ -74,11 +95,23 @@ public final class GoBackend implements Backend {
return Collections.emptySet();
}
/**
* Get the associated {@link State} for a given {@link Tunnel}.
*
* @param tunnel The tunnel to examine the state of.
* @return {@link State} associated with the given tunnel.
*/
@Override
public State getState(final Tunnel tunnel) {
return currentTunnel == tunnel ? State.UP : State.DOWN;
}
/**
* Get the associated {@link Statistics} for a given {@link Tunnel}.
*
* @param tunnel The tunnel to retrieve statistics for.
* @return {@link Statistics} associated with the given tunnel.
*/
@Override
public Statistics getStatistics(final Tunnel tunnel) {
final Statistics stats = new Statistics();
@@ -123,11 +156,26 @@ public final class GoBackend implements Backend {
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);
@@ -166,8 +214,10 @@ public final class GoBackend implements Backend {
throw new BackendException(Reason.VPN_NOT_AUTHORIZED);
final VpnService service;
if (!vpnService.isDone())
startVpnService();
if (!vpnService.isDone()) {
Log.d(TAG, "Requesting to start VpnService");
context.startService(new Intent(context, VpnService.class));
}
try {
service = vpnService.get(2, TimeUnit.SECONDS);
@@ -193,15 +243,28 @@ public final class GoBackend implements Backend {
for (final String excludedApplication : config.getInterface().getExcludedApplications())
builder.addDisallowedApplication(excludedApplication);
for (final String includedApplication : config.getInterface().getIncludedApplications())
builder.addAllowedApplication(includedApplication);
for (final InetNetwork addr : config.getInterface().getAddresses())
builder.addAddress(addr.getAddress(), addr.getMask());
for (final InetAddress addr : config.getInterface().getDnsServers())
builder.addDnsServer(addr.getHostAddress());
boolean sawDefaultRoute = false;
for (final Peer peer : config.getPeers()) {
for (final InetNetwork addr : peer.getAllowedIps())
for (final InetNetwork addr : peer.getAllowedIps()) {
if (addr.getMask() == 0)
sawDefaultRoute = true;
builder.addRoute(addr.getAddress(), addr.getMask());
}
}
// "Kill-switch" semantics
if (!(sawDefaultRoute && config.getPeers().size() == 1)) {
builder.allowFamily(OsConstants.AF_INET);
builder.allowFamily(OsConstants.AF_INET6);
}
builder.setMtu(config.getInterface().getMtu().orElse(1280));
@@ -241,11 +304,10 @@ public final class GoBackend implements Backend {
tunnel.onStateChange(state);
}
private void startVpnService() {
Log.d(TAG, "Requesting to start VpnService");
context.startService(new Intent(context, VpnService.class));
}
/**
* Callback for {@link GoBackend} that is invoked when {@link VpnService} is started by the
* system's Always-On VPN mode.
*/
public interface AlwaysOnCallback {
void alwaysOnTriggered();
}
@@ -279,6 +341,9 @@ public final class GoBackend implements Backend {
}
}
/**
* {@link android.net.VpnService} implementation for {@link GoBackend}
*/
public static class VpnService extends android.net.VpnService {
@Nullable private GoBackend owner;
@@ -14,6 +14,9 @@ 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<>();
@@ -22,31 +25,72 @@ public class Statistics {
Statistics() {
}
/**
* Add a peer and its current data usage to the internal map.
*
* @param key A WireGuard public key bound to a particular peer
* @param rx The received traffic for the {@link com.wireguard.config.Peer} referenced by
* the provided {@link Key}. This value is in bytes
* @param tx The transmitted traffic for the {@link com.wireguard.config.Peer} referenced by
* the provided {@link Key}. This value is in bytes.
*/
void add(final Key key, final long rx, final long tx) {
peerBytes.put(key, Pair.create(rx, tx));
lastTouched = SystemClock.elapsedRealtime();
}
/**
* Check if the statistics are stale, indicating the need for the {@link Backend} to update them.
*
* @return boolean indicating if the current statistics instance has stale values.
*/
public boolean isStale() {
return SystemClock.elapsedRealtime() - lastTouched > 900;
}
/**
* Get the received traffic (in bytes) for the {@link com.wireguard.config.Peer} referenced by
* the provided {@link Key}
*
* @param peer A {@link Key} representing a {@link com.wireguard.config.Peer}.
* @return a long representing the number of bytes received by this peer.
*/
public long peerRx(final Key peer) {
if (!peerBytes.containsKey(peer))
final Pair<Long, Long> rxTx = peerBytes.get(peer);
if (rxTx == null)
return 0;
return peerBytes.get(peer).first;
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) {
if (!peerBytes.containsKey(peer))
final Pair<Long, Long> rxTx = peerBytes.get(peer);
if (rxTx == null)
return 0;
return peerBytes.get(peer).second;
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()) {
@@ -55,6 +99,11 @@ public class Statistics {
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()) {
@@ -36,11 +36,20 @@ public interface Tunnel {
*/
void onStateChange(State newState);
/**
* Enum class to represent all possible states of a {@link Tunnel}.
*/
enum State {
DOWN,
TOGGLE,
UP;
/**
* Get the state of a {@link Tunnel}
*
* @param running boolean indicating if the tunnel is running.
* @return State of the tunnel based on whether or not it is running.
*/
public static State of(final boolean running) {
return running ? UP : DOWN;
}
@@ -32,11 +32,10 @@ import java.util.Objects;
import java.util.Set;
import androidx.annotation.Nullable;
import java9.util.stream.Collectors;
import java9.util.stream.Stream;
/**
* WireGuard backend that uses {@code wg-quick} to implement tunnel configuration.
* Implementation of {@link Backend} that uses the kernel module and {@code wg-quick} to provide
* WireGuard tunnels.
*/
@NonNullForAll
@@ -67,7 +66,7 @@ public final class WgQuickBackend implements Backend {
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());
return Set.of(output.get(0).split(" "));
}
@Override
@@ -10,14 +10,9 @@ import android.system.OsConstants;
import android.util.Base64;
import com.wireguard.android.util.RootShell.RootShellException;
import com.wireguard.crypto.Ed25519;
import com.wireguard.util.NonNullForAll;
import net.i2p.crypto.eddsa.EdDSAEngine;
import net.i2p.crypto.eddsa.EdDSAPublicKey;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec;
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -28,7 +23,6 @@ import java.nio.charset.StandardCharsets;
import java.security.InvalidParameterException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -37,6 +31,10 @@ import java.util.Map;
import androidx.annotation.Nullable;
/**
* Class that implements the logic for downloading and loading signed, prebuilt modules for
* WireGuard into the running kernel.
*/
@NonNullForAll
@SuppressWarnings("MagicNumber")
public class ModuleLoader {
@@ -49,6 +47,15 @@ public class ModuleLoader {
private final File tmpDir;
private final String userAgent;
/**
* Public constructor for ModuleLoader
*
* @param context A {@link Context} instance.
* @param rootShell A {@link RootShell} instance used to run elevated commands required for module
* loading.
* @param userAgent A {@link String} that represents the User-Agent string used for connections
* to the upstream server.
*/
public ModuleLoader(final Context context, final RootShell rootShell, final String userAgent) {
moduleDir = new File(context.getCacheDir(), "kmod");
tmpDir = new File(context.getCacheDir(), "tmp");
@@ -56,10 +63,23 @@ public class ModuleLoader {
this.userAgent = userAgent;
}
/**
* Check whether a WireGuard module is already loaded into the kernel.
*
* @return boolean indicating if WireGuard is already enabled in the kernel.
*/
public static boolean isModuleLoaded() {
return new File("/sys/module/wireguard").exists();
}
/**
* Download the correct WireGuard module for the device
*
* @return {@link OsConstants}.EXIT_SUCCESS if everything succeeds, ENOENT otherwise.
* @throws IOException if the remote hash list was not found or empty.
* @throws RootShellException if {@link RootShell} has a failure executing elevated commands.
* @throws NoSuchAlgorithmException if SHA256 algorithm is not available in device JDK.
*/
public Integer download() throws IOException, RootShellException, NoSuchAlgorithmException {
final List<String> output = new ArrayList<>();
rootShell.run(output, "sha256sum /proc/version|cut -d ' ' -f 1");
@@ -119,17 +139,28 @@ public class ModuleLoader {
return OsConstants.EXIT_SUCCESS;
}
/**
* Load the downloaded module. ModuleLoader#download must be called before this.
*
* @throws IOException if {@link RootShell} has a failure executing elevated commands.
* @throws RootShellException if {@link RootShell} has a failure executing elevated commands.
*/
public void loadModule() throws IOException, RootShellException {
rootShell.run(null, String.format("insmod \"%s/wireguard-$(sha256sum /proc/version|cut -d ' ' -f 1).ko\"", moduleDir.getAbsolutePath()));
}
/**
* Check if the module might already exist in the app's data.
*
* @return boolean indicating whether downloadable module might exist already.
*/
public boolean moduleMightExist() {
return moduleDir.exists() && moduleDir.isDirectory();
}
@Nullable
private Map<String, Sha256Digest> verifySignedHashes(final String signifyDigest) {
final byte[] publicKeyBytes = Base64.decode(MODULE_PUBLIC_KEY_BASE64, Base64.DEFAULT);
byte[] publicKeyBytes = Base64.decode(MODULE_PUBLIC_KEY_BASE64, Base64.DEFAULT);
if (publicKeyBytes == null || publicKeyBytes.length != 32 + 10 || publicKeyBytes[0] != 'E' || publicKeyBytes[1] != 'd')
return null;
@@ -140,26 +171,17 @@ public class ModuleLoader {
if (!lines[0].startsWith("untrusted comment: "))
return null;
final byte[] signatureBytes = Base64.decode(lines[1], Base64.DEFAULT);
byte[] signatureBytes = Base64.decode(lines[1], Base64.DEFAULT);
if (signatureBytes == null || signatureBytes.length != 64 + 10)
return null;
for (int i = 0; i < 10; ++i) {
if (signatureBytes[i] != publicKeyBytes[i])
return null;
}
try {
final EdDSAParameterSpec parameterSpec = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519);
final Signature signature = new EdDSAEngine(MessageDigest.getInstance(parameterSpec.getHashAlgorithm()));
final byte[] rawPublicKeyBytes = new byte[32];
System.arraycopy(publicKeyBytes, 10, rawPublicKeyBytes, 0, 32);
signature.initVerify(new EdDSAPublicKey(new EdDSAPublicKeySpec(rawPublicKeyBytes, parameterSpec)));
signature.update(lines[2].getBytes(StandardCharsets.UTF_8));
if (!signature.verify(signatureBytes, 10, 64))
return null;
} catch (final Exception ignored) {
publicKeyBytes = Arrays.copyOfRange(publicKeyBytes, 10, 10 + 32);
signatureBytes = Arrays.copyOfRange(signatureBytes, 10, 10 + 64);
if (!Ed25519.verify(lines[2].getBytes(StandardCharsets.UTF_8), signatureBytes, publicKeyBytes))
return null;
}
final Map<String, Sha256Digest> hashes = new HashMap<>();
for (final String line : lines[2].split("\n")) {
@@ -43,8 +43,11 @@ public class RootShell {
public RootShell(final Context context) {
localBinaryDir = new File(context.getCodeCacheDir(), "bin");
localTemporaryDir = new File(context.getCacheDir(), "tmp");
preamble = String.format("export CALLING_PACKAGE=%s PATH=\"%s:$PATH\" TMPDIR='%s'; id -u\n",
context.getPackageName(), localBinaryDir, localTemporaryDir);
final String packageName = context.getPackageName();
if (packageName.contains("'"))
throw new RuntimeException("Impossibly invalid package name contains a single quote");
preamble = String.format("export CALLING_PACKAGE=%s PATH=\"%s:$PATH\" TMPDIR='%s'; magisk --sqlite \"UPDATE policies SET notification=0, logging=0 WHERE package_name='%s'\" >/dev/null 2>&1; id -u\n",
packageName, localBinaryDir, localTemporaryDir, packageName);
}
private static boolean isExecutableInPath(final String name) {
@@ -143,12 +143,12 @@ public final class ToolsInstaller {
extract();
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("printf 'name=WireGuard Command Line Tools\nversion=1.0\nversionCode=1\nauthor=zx2c4\ndescription=Command line tools for WireGuard\nminMagisk=1500\n' > /sbin/.magisk/img/wireguard/module.prop; ");
script.append("touch /sbin/.magisk/img/wireguard/auto_mount; ");
script.append("trap 'rm -rf /data/adb/modules/wireguard' INT TERM EXIT; ");
script.append(String.format("rm -rf /data/adb/modules/wireguard/; mkdir -p /data/adb/modules/wireguard%s; ", INSTALL_DIR));
script.append("printf 'name=WireGuard Command Line Tools\nversion=1.0\nversionCode=1\nauthor=zx2c4\ndescription=Command line tools for WireGuard\nminMagisk=1500\n' > /data/adb/modules/wireguard/module.prop; ");
script.append("touch /data/adb/modules/wireguard/auto_mount; ");
for (final String name : EXECUTABLES) {
final File destination = new File("/sbin/.magisk/img/wireguard" + INSTALL_DIR, name);
final File destination = new File("/data/adb/modules/wireguard" + INSTALL_DIR, name);
script.append(String.format("cp '%s' '%s'; chmod 755 '%s'; chcon 'u:object_r:system_file:s0' '%s' || true; ",
new File(localBinaryDir, name), destination, destination, destination));
}
@@ -191,7 +191,7 @@ public final class ToolsInstaller {
synchronized (lock) {
if (installAsMagiskModule == null) {
try {
installAsMagiskModule = rootShell.run(null, "[ -d /sbin/.magisk/mirror -a -d /sbin/.magisk/img -a ! -f /cache/.disable_magisk ]") == OsConstants.EXIT_SUCCESS;
installAsMagiskModule = rootShell.run(null, "[ -d /data/adb/modules -a ! -f /cache/.disable_magisk ]") == OsConstants.EXIT_SUCCESS;
} catch (final Exception ignored) {
installAsMagiskModule = false;
}
@@ -8,11 +8,10 @@ package com.wireguard.config;
import com.wireguard.util.NonNullForAll;
import java.util.Iterator;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java9.util.Optional;
@NonNullForAll
public final class Attribute {
private static final Pattern LINE_PATTERN = Pattern.compile("(\\w+)\\s*=\\s*([^\\s#][^#]*)");
@@ -72,6 +72,7 @@ public class BadConfigException extends Exception {
DNS("DNS"),
ENDPOINT("Endpoint"),
EXCLUDED_APPLICATIONS("ExcludedApplications"),
INCLUDED_APPLICATIONS("IncludedApplications"),
LISTEN_PORT("ListenPort"),
MTU("MTU"),
PERSISTENT_KEEPALIVE("PersistentKeepalive"),
@@ -17,10 +17,8 @@ import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import androidx.annotation.Nullable;
@@ -187,7 +185,7 @@ public final class Config {
@SuppressWarnings("UnusedReturnValue")
public static final class Builder {
// Defaults to an empty set.
private final Set<Peer> peers = new LinkedHashSet<>();
private final ArrayList<Peer> peers = new ArrayList<>();
// No default; must be provided before building.
@Nullable private Interface interfaze;
@@ -7,18 +7,17 @@ package com.wireguard.config;
import com.wireguard.util.NonNullForAll;
import org.threeten.bp.Duration;
import org.threeten.bp.Instant;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.regex.Pattern;
import androidx.annotation.Nullable;
import java9.util.Optional;
/**
@@ -20,13 +20,11 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import androidx.annotation.Nullable;
import java9.util.Lists;
import java9.util.Optional;
import java9.util.stream.Collectors;
import java9.util.stream.StreamSupport;
/**
* Represents the configuration for a WireGuard interface (an [Interface] block). Interfaces must
@@ -43,6 +41,7 @@ public final class Interface {
private final Set<InetNetwork> addresses;
private final Set<InetAddress> dnsServers;
private final Set<String> excludedApplications;
private final Set<String> includedApplications;
private final KeyPair keyPair;
private final Optional<Integer> listenPort;
private final Optional<Integer> mtu;
@@ -52,6 +51,7 @@ public final class Interface {
addresses = Collections.unmodifiableSet(new LinkedHashSet<>(builder.addresses));
dnsServers = Collections.unmodifiableSet(new LinkedHashSet<>(builder.dnsServers));
excludedApplications = Collections.unmodifiableSet(new LinkedHashSet<>(builder.excludedApplications));
includedApplications = Collections.unmodifiableSet(new LinkedHashSet<>(builder.includedApplications));
keyPair = Objects.requireNonNull(builder.keyPair, "Interfaces must have a private key");
listenPort = builder.listenPort;
mtu = builder.mtu;
@@ -81,6 +81,9 @@ public final class Interface {
case "excludedapplications":
builder.parseExcludedApplications(attribute.getValue());
break;
case "includedapplications":
builder.parseIncludedApplications(attribute.getValue());
break;
case "listenport":
builder.parseListenPort(attribute.getValue());
break;
@@ -106,6 +109,7 @@ public final class Interface {
return addresses.equals(other.addresses)
&& dnsServers.equals(other.dnsServers)
&& excludedApplications.equals(other.excludedApplications)
&& includedApplications.equals(other.includedApplications)
&& keyPair.equals(other.keyPair)
&& listenPort.equals(other.listenPort)
&& mtu.equals(other.mtu);
@@ -141,6 +145,16 @@ public final class Interface {
return excludedApplications;
}
/**
* Returns the set of applications included exclusively for using the interface.
*
* @return a set of package names
*/
public Set<String> getIncludedApplications() {
// The collection is already immutable.
return includedApplications;
}
/**
* Returns the public/private key pair used by the interface.
*
@@ -174,6 +188,7 @@ public final class Interface {
hash = 31 * hash + addresses.hashCode();
hash = 31 * hash + dnsServers.hashCode();
hash = 31 * hash + excludedApplications.hashCode();
hash = 31 * hash + includedApplications.hashCode();
hash = 31 * hash + keyPair.hashCode();
hash = 31 * hash + listenPort.hashCode();
hash = 31 * hash + mtu.hashCode();
@@ -206,13 +221,13 @@ public final class Interface {
if (!addresses.isEmpty())
sb.append("Address = ").append(Attribute.join(addresses)).append('\n');
if (!dnsServers.isEmpty()) {
final List<String> dnsServerStrings = StreamSupport.stream(dnsServers)
.map(InetAddress::getHostAddress)
.collect(Collectors.toUnmodifiableList());
final List<String> dnsServerStrings = dnsServers.stream().map(InetAddress::getHostAddress).collect(Collectors.toList());
sb.append("DNS = ").append(Attribute.join(dnsServerStrings)).append('\n');
}
if (!excludedApplications.isEmpty())
sb.append("ExcludedApplications = ").append(Attribute.join(excludedApplications)).append('\n');
if (!includedApplications.isEmpty())
sb.append("IncludedApplications = ").append(Attribute.join(includedApplications)).append('\n');
listenPort.ifPresent(lp -> sb.append("ListenPort = ").append(lp).append('\n'));
mtu.ifPresent(m -> sb.append("MTU = ").append(m).append('\n'));
sb.append("PrivateKey = ").append(keyPair.getPrivateKey().toBase64()).append('\n');
@@ -240,6 +255,8 @@ public final class Interface {
private final Set<InetAddress> dnsServers = new LinkedHashSet<>();
// Defaults to an empty set.
private final Set<String> excludedApplications = new LinkedHashSet<>();
// Defaults to an empty set.
private final Set<String> includedApplications = new LinkedHashSet<>();
// No default; must be provided before building.
@Nullable private KeyPair keyPair;
// Defaults to not present.
@@ -271,6 +288,9 @@ public final class Interface {
if (keyPair == null)
throw new BadConfigException(Section.INTERFACE, Location.PRIVATE_KEY,
Reason.MISSING_ATTRIBUTE, null);
if (!includedApplications.isEmpty() && !excludedApplications.isEmpty())
throw new BadConfigException(Section.INTERFACE, Location.INCLUDED_APPLICATIONS,
Reason.INVALID_KEY, null);
return new Interface(this);
}
@@ -284,6 +304,16 @@ public final class Interface {
return this;
}
public Builder includeApplication(final String application) {
includedApplications.add(application);
return this;
}
public Builder includeApplications(final Collection<String> applications) {
includedApplications.addAll(applications);
return this;
}
public Builder parseAddresses(final CharSequence addresses) throws BadConfigException {
try {
for (final String address : Attribute.split(addresses))
@@ -305,7 +335,11 @@ public final class Interface {
}
public Builder parseExcludedApplications(final CharSequence apps) {
return excludeApplications(Lists.of(Attribute.split(apps)));
return excludeApplications(List.of(Attribute.split(apps)));
}
public Builder parseIncludedApplications(final CharSequence apps) {
return includeApplications(List.of(Attribute.split(apps)));
}
public Builder parseListenPort(final String listenPort) throws BadConfigException {
@@ -17,10 +17,10 @@ import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import androidx.annotation.Nullable;
import java9.util.Optional;
/**
* Represents the configuration for a WireGuard peer (a [Peer] block). Peers must have a public key,
@@ -13,7 +13,7 @@ import java.util.Arrays;
import androidx.annotation.Nullable;
/**
* Implementation of the Curve25519 elliptic curve algorithm.
* Implementation of Curve25519 ECDH.
* <p>
* This implementation was imported to WireGuard from noise-java:
* https://github.com/rweather/noise-java
@@ -28,7 +28,7 @@ import androidx.annotation.Nullable;
*/
@SuppressWarnings({"MagicNumber", "NonConstantFieldWithUpperCaseName", "SuspiciousNameCombination"})
@NonNullForAll
final class Curve25519 {
public final class Curve25519 {
// Numbers modulo 2^255 - 19 are broken up into ten 26-bit words.
private static final int NUM_LIMBS_255BIT = 10;
private static final int NUM_LIMBS_510BIT = 20;
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -14,7 +14,7 @@ target_link_libraries(libwg-quick.so -ldl)
file(GLOB WG_SOURCES wireguard-tools/src/*.c ndk-compat/compat.c)
add_executable(libwg.so ${WG_SOURCES})
target_include_directories(libwg.so PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/uapi/" "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/")
target_include_directories(libwg.so PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/uapi/linux/" "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/")
target_compile_options(libwg.so PUBLIC -O3 -std=gnu11 -D_GNU_SOURCE -include ${CMAKE_CURRENT_SOURCE_DIR}/ndk-compat/compat.h -DHAVE_VISIBILITY_HIDDEN -DRUNSTATEDIR=\"/data/data/${ANDROID_PACKAGE_NAME}/cache\")
add_custom_target(libwg-go.so WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/libwg-go" COMMENT "Building wireguard-go" VERBATIM COMMAND make
+6 -6
View File
@@ -14,23 +14,23 @@ 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 CGO_LDFLAGS := $(CLANG_FLAGS) $(LDFLAGS) -Wl,-soname=libwg-go.so
export CC := $(ANDROID_C_COMPILER)
export GOARCH := $(NDK_GO_ARCH_MAP_$(ANDROID_ARCH_NAME))
export GOOS := android
export CGO_ENABLED := 1
GO_VERSION := 1.14.1
GO_VERSION := 1.15.2
GO_PLATFORM := $(shell uname -s | tr '[:upper:]' '[:lower:]')-$(NDK_GO_ARCH_MAP_$(shell uname -m))
GO_TARBALL := go$(GO_VERSION).$(GO_PLATFORM).tar.gz
GO_HASH_darwin-amd64 := 6632f9d53fd95632e431e8c34295349cca3f0a124e3a28b760ae5c42b32816e3
GO_HASH_linux-amd64 := 2f49eb17ce8b48c680cdb166ffd7389702c0dec6effa090c324804a5cac8a7f8
GO_HASH_darwin-amd64 := 9bd39600d9fa1fa4a5ccce8761d249f7421cffe671376f791293c4138f3d7c62
GO_HASH_linux-amd64 := b49fda1ca29a1946d6bb2a5a6982cf07ccd2aba849289508ee0f9918f6bb4552
default: $(DESTDIR)/libwg-go.so
$(GRADLE_USER_HOME)/caches/golang/$(GO_TARBALL):
mkdir -p "$(dir $@)"
flock -x "$@.lock" -c ' \
flock "$@.lock" -c ' \
[ -f "$@" ] && exit 0; \
curl -o "$@.tmp" "https://dl.google.com/go/$(GO_TARBALL)" && \
echo "$(GO_HASH_$(GO_PLATFORM)) $@.tmp" | sha256sum -c && \
@@ -38,7 +38,7 @@ $(GRADLE_USER_HOME)/caches/golang/$(GO_TARBALL):
$(BUILDDIR)/go-$(GO_VERSION)/.prepared: $(GRADLE_USER_HOME)/caches/golang/$(GO_TARBALL)
mkdir -p "$(dir $@)"
flock -x "$@.lock" -c ' \
flock "$@.lock" -c ' \
[ -f "$@" ] && exit 0; \
tar -C "$(dir $@)" --strip-components=1 -xzf "$^" && \
patch -p1 -f -N -r- -d "$(dir $@)" < goruntime-boottime-over-monotonic.diff && \
+17 -7
View File
@@ -11,10 +11,6 @@ import "C"
import (
"bufio"
"golang.org/x/sys/unix"
"golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/ipc"
"golang.zx2c4.com/wireguard/tun"
"bytes"
"log"
"math"
@@ -24,6 +20,12 @@ import (
"runtime"
"strings"
"unsafe"
"golang.org/x/sys/unix"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/ipc"
"golang.zx2c4.com/wireguard/tun"
)
type AndroidLogger struct {
@@ -44,7 +46,6 @@ type TunnelHandle struct {
var tunnelHandles map[int32]TunnelHandle
func init() {
device.RoamingDisabled = true
tunnelHandles = make(map[int32]TunnelHandle)
signals := make(chan os.Signal)
signal.Notify(signals, unix.SIGUSR2)
@@ -89,6 +90,7 @@ func wgTurnOn(ifnameRef string, tunFd int32, settings string) int32 {
logger.Error.Println(setError)
return -1
}
device.DisableSomeRoamingForBrokenMobileSemantics()
var uapi net.Listener
@@ -149,7 +151,11 @@ func wgGetSocketV4(tunnelHandle int32) int32 {
if !ok {
return -1
}
fd, err := handle.device.PeekLookAtSocketFd4()
bind, _ := handle.device.Bind().(conn.PeekLookAtSocketFd)
if bind == nil {
return -1
}
fd, err := bind.PeekLookAtSocketFd4()
if err != nil {
return -1
}
@@ -162,7 +168,11 @@ func wgGetSocketV6(tunnelHandle int32) int32 {
if !ok {
return -1
}
fd, err := handle.device.PeekLookAtSocketFd6()
bind, _ := handle.device.Bind().(conn.PeekLookAtSocketFd)
if bind == nil {
return -1
}
fd, err := bind.PeekLookAtSocketFd6()
if err != nil {
return -1
}
+5 -5
View File
@@ -1,10 +1,10 @@
module golang.zx2c4.com/wireguard/android
go 1.13
go 1.15
require (
golang.org/x/crypto v0.0.0-20200320181102-891825fb96df // indirect
golang.org/x/net v0.0.0-20200320220750-118fecf932d8 // indirect
golang.org/x/sys v0.0.0-20200321134203-328b4cd54aae
golang.zx2c4.com/wireguard v0.0.20200320
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
golang.org/x/net v0.0.0-20201216054612-986b41b23924 // indirect
golang.org/x/sys v0.0.0-20201223074533-0d417f636930
golang.zx2c4.com/wireguard v0.0.20201119-0.20201223215156-09728dc6b340
)
+18 -13
View File
@@ -1,19 +1,24 @@
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200320181102-891825fb96df h1:lDWgvUvNnaTnNBc/dwOty86cFeKoKWbwy2wQj0gIxbU=
golang.org/x/crypto v0.0.0-20200320181102-891825fb96df/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200320220750-118fecf932d8 h1:1+zQlQqEEhUeStBTi653GZAnAuivZq/2hz+Iz+OP7rg=
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY=
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
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-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200321134203-328b4cd54aae h1:3tcmuaB7wwSZtelmiv479UjUB+vviwABz7a133ZwOKQ=
golang.org/x/sys v0.0.0-20200321134203-328b4cd54aae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201223074533-0d417f636930 h1:vRgIt+nup/B/BwIS0g2oC0haq0iqbV3ZA+u6+0TlNCo=
golang.org/x/sys v0.0.0-20201223074533-0d417f636930/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
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/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.zx2c4.com/wireguard v0.0.20200320 h1:1vE6zVeO7fix9cJX1Z9ZQ+ikPIIx7vIyU0o0tLDD88g=
golang.zx2c4.com/wireguard v0.0.20200320/go.mod h1:lDian4Sw4poJ04SgHh35nzMVwGSYlPumkdnHcucAQoY=
golang.zx2c4.com/wireguard v0.0.20201119-0.20201223215156-09728dc6b340 h1:X6jrf2sUEj3n+q2oB/I3C088vQFKREz2UzgVJ8wENtI=
golang.zx2c4.com/wireguard v0.0.20201119-0.20201223215156-09728dc6b340/go.mod h1:ITsWNpkFv78VPB7f8MiyuxeEMcHR4jfxHGCJLPP3GHs=
@@ -1,6 +1,6 @@
From e44f456f1d0e429e08afed64a161175ff493f3ac Mon Sep 17 00:00:00 2001
From 1d1ba1da11afd73008c0e942db7621697055a6b6 Mon Sep 17 00:00:00 2001
From: "Jason A. Donenfeld" <Jason@zx2c4.com>
Date: Wed, 27 Feb 2019 05:05:44 +0100
Date: Tue, 15 Sep 2020 13:39:22 +0200
Subject: [PATCH] runtime: use CLOCK_BOOTTIME in nanotime on Linux
This makes timers account for having expired while a computer was
@@ -28,10 +28,10 @@ Change-Id: I7b2a6ca0c5bc5fce57ec0eeafe7b68270b429321
8 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/src/runtime/sys_linux_386.s b/src/runtime/sys_linux_386.s
index 1b28098ad9..46b7071ed8 100644
index 5b9b638ad7..448ad8b2e6 100644
--- a/src/runtime/sys_linux_386.s
+++ b/src/runtime/sys_linux_386.s
@@ -317,13 +317,13 @@ noswitch:
@@ -339,13 +339,13 @@ noswitch:
LEAL 8(SP), BX // &ts (struct timespec)
MOVL BX, 4(SP)
@@ -48,10 +48,10 @@ index 1b28098ad9..46b7071ed8 100644
INVOKE_SYSCALL
diff --git a/src/runtime/sys_linux_amd64.s b/src/runtime/sys_linux_amd64.s
index 58d3bc54b4..4bb9bde3d0 100644
index fe9c6bce85..4836a7c774 100644
--- a/src/runtime/sys_linux_amd64.s
+++ b/src/runtime/sys_linux_amd64.s
@@ -293,7 +293,7 @@ noswitch:
@@ -311,7 +311,7 @@ noswitch:
MOVQ runtime·vdsoClockgettimeSym(SB), AX
CMPQ AX, $0
JEQ fallback
@@ -61,7 +61,7 @@ index 58d3bc54b4..4bb9bde3d0 100644
CALL AX
MOVQ 0(SP), AX // sec
diff --git a/src/runtime/sys_linux_arm.s b/src/runtime/sys_linux_arm.s
index e103da56dc..0b872b90a6 100644
index 475f52344c..bb567abcf4 100644
--- a/src/runtime/sys_linux_arm.s
+++ b/src/runtime/sys_linux_arm.s
@@ -11,7 +11,7 @@
@@ -73,7 +73,7 @@ index e103da56dc..0b872b90a6 100644
// for EABI, as we don't support OABI
#define SYS_BASE 0x0
@@ -345,7 +345,7 @@ noswitch:
@@ -366,7 +366,7 @@ noswitch:
SUB $24, R13 // Space for results
BIC $0x7, R13 // Align for C code
@@ -83,7 +83,7 @@ index e103da56dc..0b872b90a6 100644
MOVW runtime·vdsoClockgettimeSym(SB), R2
CMP $0, R2
diff --git a/src/runtime/sys_linux_arm64.s b/src/runtime/sys_linux_arm64.s
index b9588cec30..e444d50df4 100644
index 198a5bacef..9715387f36 100644
--- a/src/runtime/sys_linux_arm64.s
+++ b/src/runtime/sys_linux_arm64.s
@@ -13,7 +13,7 @@
@@ -95,7 +95,7 @@ index b9588cec30..e444d50df4 100644
#define SYS_exit 93
#define SYS_read 63
@@ -297,7 +297,7 @@ noswitch:
@@ -319,7 +319,7 @@ noswitch:
BIC $15, R1
MOVD R1, RSP
@@ -105,10 +105,10 @@ index b9588cec30..e444d50df4 100644
CBZ R2, fallback
diff --git a/src/runtime/sys_linux_mips64x.s b/src/runtime/sys_linux_mips64x.s
index 723cfe43d9..edd7a195eb 100644
index afad056d06..2c9162b903 100644
--- a/src/runtime/sys_linux_mips64x.s
+++ b/src/runtime/sys_linux_mips64x.s
@@ -278,7 +278,7 @@ noswitch:
@@ -304,7 +304,7 @@ noswitch:
AND $~15, R1 // Align for C code
MOVV R1, R29
@@ -118,10 +118,10 @@ index 723cfe43d9..edd7a195eb 100644
MOVV runtime·vdsoClockgettimeSym(SB), R25
diff --git a/src/runtime/sys_linux_mipsx.s b/src/runtime/sys_linux_mipsx.s
index 15893a7a28..f3edf9a83a 100644
index fab2ab3892..f9af103594 100644
--- a/src/runtime/sys_linux_mipsx.s
+++ b/src/runtime/sys_linux_mipsx.s
@@ -235,7 +235,7 @@ TEXT runtime·walltime1(SB),NOSPLIT,$8-12
@@ -238,7 +238,7 @@ TEXT runtime·walltime1(SB),NOSPLIT,$8-12
RET
TEXT runtime·nanotime1(SB),NOSPLIT,$8-8
@@ -131,13 +131,13 @@ index 15893a7a28..f3edf9a83a 100644
MOVW $SYS_clock_gettime, R2
SYSCALL
diff --git a/src/runtime/sys_linux_ppc64x.s b/src/runtime/sys_linux_ppc64x.s
index 8629fe3233..2402e2623a 100644
index fd69ee70a5..ff6bc8355b 100644
--- a/src/runtime/sys_linux_ppc64x.s
+++ b/src/runtime/sys_linux_ppc64x.s
@@ -233,7 +233,7 @@ fallback:
@@ -249,7 +249,7 @@ fallback:
JMP finish
TEXT runtime·nanotime1(SB),NOSPLIT,$16
TEXT runtime·nanotime1(SB),NOSPLIT,$16-8
- MOVD $1, R3 // CLOCK_MONOTONIC
+ MOVD $7, R3 // CLOCK_BOOTTIME
@@ -157,5 +157,5 @@ index c15a1d5364..f52c4d5098 100644
MOVW $SYS_clock_gettime, R1
SYSCALL
--
2.25.1
2.28.0
+24 -23
View File
@@ -10,33 +10,24 @@ group groupName
final def keystorePropertiesFile = rootProject.file("keystore.properties")
android {
buildToolsVersion '29.0.3'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = '1.8'
}
}
compileSdkVersion 29
dataBinding.enabled true
viewBinding.enabled true
compileSdkVersion 30
buildFeatures.dataBinding = true
buildFeatures.viewBinding = true
defaultConfig {
applicationId 'com.wireguard.android'
minSdkVersion 21
targetSdkVersion 29
targetSdkVersion 30
versionCode wireguardVersionCode
versionName wireguardVersionName
buildConfigField 'int', 'MIN_SDK_VERSION', "$minSdkVersion.apiLevel"
}
// If the keystore file exists
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
coreLibraryDesugaringEnabled = true
}
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 {
@@ -59,29 +50,39 @@ android {
versionNameSuffix "-debug"
}
}
lintOptions {
disable('LongLogTag')
}
}
dependencies {
implementation project(":tunnel")
implementation "androidx.activity:activity-ktx:$activityVersion"
implementation "androidx.annotation:annotation:$annotationsVersion"
implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation "androidx.cardview:cardview:$cardviewVersion"
implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$coordinatorLayoutVersion"
implementation "androidx.biometric:biometric:$biometricVersion"
implementation "androidx.core:core-ktx:$coreKtxVersion"
implementation "androidx.databinding:databinding-runtime:$agpVersion"
implementation "androidx.fragment:fragment:$fragmentVersion"
implementation "androidx.preference:preference:$preferenceVersion"
implementation "androidx.fragment:fragment-ktx:$fragmentVersion"
implementation "androidx.preference:preference-ktx:$preferenceVersion"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleRuntimeKtxVersion"
implementation "androidx.datastore:datastore-preferences:$datastoreVersion"
implementation "com.google.android.material:material:$materialComponentsVersion"
implementation "com.journeyapps:zxing-android-embedded:$zxingEmbeddedVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
implementation "net.sourceforge.streamsupport:android-retrofuture:$streamsupportVersion"
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$desugarVersion"
}
tasks.withType(JavaCompile) {
options.compilerArgs << '-Xlint:unchecked'
options.deprecation = true
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
}
+10 -2
View File
@@ -2,5 +2,13 @@
# escalates them as errors.
-dontwarn sun.misc.Unsafe
# Retain some information to keep stacktraces usable
-keepattributes SourceFile,LineNumberTable
# Fragment 1.2.4 allows Fragment classes to be obfuscated but
# databinding references in XML seem to not be rewritten to
# match, so we preserve the names as 1.2.3 did.
-if public class ** extends androidx.fragment.app.Fragment
-keep public class <1> {
public <init>();
}
# Don't obfuscate
-dontobfuscate
+24 -2
View File
@@ -7,7 +7,10 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"
tools:ignore="ScopedStorage" />
<uses-feature
android:name="android.hardware.touchscreen"
@@ -45,7 +48,6 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
@@ -53,6 +55,15 @@
</intent-filter>
</activity>
<activity
android:name=".activity.TvMainActivity"
android:theme="@style/TvTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".activity.SettingsActivity"
android:label="@string/settings"
@@ -112,5 +123,16 @@
android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="false" />
</service>
<meta-data
android:name="android.content.APP_RESTRICTIONS"
android:resource="@xml/app_restrictions" />
</application>
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent>
</queries>
</manifest>
@@ -6,38 +6,44 @@ package com.wireguard.android
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.AsyncTask
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import android.os.StrictMode.VmPolicy
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.PreferenceManager
import androidx.datastore.DataStore
import androidx.datastore.preferences.Preferences
import androidx.datastore.preferences.createDataStore
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.ExceptionLoggers
import com.wireguard.android.util.ModuleLoader
import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller
import java9.util.concurrent.CompletableFuture
import com.wireguard.android.util.UserKnobs
import com.wireguard.android.util.applicationScope
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import java.lang.ref.WeakReference
import java.util.Locale
class Application : android.app.Application(), OnSharedPreferenceChangeListener {
private val futureBackend = CompletableFuture<Backend>()
private lateinit var asyncWorker: AsyncWorker
class Application : android.app.Application() {
private val futureBackend = CompletableDeferred<Backend>()
private val coroutineScope = CoroutineScope(Job() + Dispatchers.Main.immediate)
private var backend: Backend? = null
private lateinit var moduleLoader: ModuleLoader
private lateinit var rootShell: RootShell
private lateinit var sharedPreferences: SharedPreferences
private lateinit var preferencesDataStore: DataStore<Preferences>
private lateinit var toolsInstaller: ToolsInstaller
private lateinit var tunnelManager: TunnelManager
@@ -53,36 +59,70 @@ class Application : android.app.Application(), OnSharedPreferenceChangeListener
}
if (BuildConfig.DEBUG) {
StrictMode.setVmPolicy(VmPolicy.Builder().detectAll().penaltyLog().build())
StrictMode.setThreadPolicy(ThreadPolicy.Builder().detectAll().penaltyLog().build())
}
}
private suspend fun determineBackend(): Backend {
var backend: Backend? = null
var didStartRootShell = false
if (!ModuleLoader.isModuleLoaded() && moduleLoader.moduleMightExist()) {
try {
rootShell.start()
didStartRootShell = true
moduleLoader.loadModule()
} catch (ignored: Exception) {
}
}
if (!UserKnobs.disableKernelModule.first() && ModuleLoader.isModuleLoaded()) {
try {
if (!didStartRootShell)
rootShell.start()
val wgQuickBackend = WgQuickBackend(applicationContext, rootShell, toolsInstaller)
wgQuickBackend.setMultipleTunnels(UserKnobs.multipleTunnels.first())
backend = wgQuickBackend
UserKnobs.multipleTunnels.onEach {
wgQuickBackend.setMultipleTunnels(it)
}.launchIn(coroutineScope)
} catch (ignored: Exception) {
}
}
if (backend == null) {
backend = GoBackend(applicationContext)
GoBackend.setAlwaysOnCallback { get().applicationScope.launch { get().tunnelManager.restoreState(true) } }
}
return backend
}
override fun onCreate() {
Log.i(TAG, USER_AGENT)
super.onCreate()
asyncWorker = AsyncWorker(AsyncTask.SERIAL_EXECUTOR, Handler(Looper.getMainLooper()))
rootShell = RootShell(applicationContext)
toolsInstaller = ToolsInstaller(applicationContext, rootShell)
moduleLoader = ModuleLoader(applicationContext, rootShell, USER_AGENT)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
preferencesDataStore = applicationContext.createDataStore(name = "settings")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
AppCompatDelegate.setDefaultNightMode(
if (sharedPreferences.getBoolean("dark_theme", false)) AppCompatDelegate.MODE_NIGHT_YES else AppCompatDelegate.MODE_NIGHT_NO)
coroutineScope.launch {
AppCompatDelegate.setDefaultNightMode(
if (UserKnobs.darkTheme.first()) AppCompatDelegate.MODE_NIGHT_YES else AppCompatDelegate.MODE_NIGHT_NO)
}
} else {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
}
tunnelManager = TunnelManager(FileConfigStore(applicationContext))
tunnelManager.onCreate()
asyncWorker.supplyAsync(Companion::getBackend).thenAccept { futureBackend.complete(it) }
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
if ("multiple_tunnels" == key && backend != null && backend is WgQuickBackend)
(backend as WgQuickBackend).setMultipleTunnels(sharedPreferences.getBoolean(key, false))
coroutineScope.launch(Dispatchers.IO) {
try {
backend = determineBackend()
futureBackend.complete(backend!!)
} catch (e: Throwable) {
Log.e(TAG, Log.getStackTraceString(e))
}
}
}
override fun onTerminate() {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
coroutineScope.cancel()
super.onTerminate()
}
@@ -97,45 +137,7 @@ class Application : android.app.Application(), OnSharedPreferenceChangeListener
}
@JvmStatic
fun getAsyncWorker() = get().asyncWorker
@JvmStatic
fun getBackend(): Backend {
val app = get()
synchronized(app.futureBackend) {
if (app.backend == null) {
var backend: Backend? = null
var didStartRootShell = false
if (!ModuleLoader.isModuleLoaded() && app.moduleLoader.moduleMightExist()) {
try {
app.rootShell.start()
didStartRootShell = true
app.moduleLoader.loadModule()
} catch (ignored: Exception) {
}
}
if (!app.sharedPreferences.getBoolean("disable_kernel_module", false) && ModuleLoader.isModuleLoaded()) {
try {
if (!didStartRootShell)
app.rootShell.start()
val wgQuickBackend = WgQuickBackend(app.applicationContext, app.rootShell, app.toolsInstaller)
wgQuickBackend.setMultipleTunnels(app.sharedPreferences.getBoolean("multiple_tunnels", false))
backend = wgQuickBackend
} catch (ignored: Exception) {
}
}
if (backend == null) {
backend = GoBackend(app.applicationContext)
GoBackend.setAlwaysOnCallback { get().tunnelManager.restoreState(true).whenComplete(ExceptionLoggers.D) }
}
app.backend = backend
}
return app.backend!!
}
}
@JvmStatic
fun getBackendAsync() = get().futureBackend
suspend fun getBackend() = get().futureBackend.await()
@JvmStatic
fun getModuleLoader() = get().moduleLoader
@@ -144,13 +146,16 @@ class Application : android.app.Application(), OnSharedPreferenceChangeListener
fun getRootShell() = get().rootShell
@JvmStatic
fun getSharedPreferences() = get().sharedPreferences
fun getPreferencesDataStore() = get().preferencesDataStore
@JvmStatic
fun getToolsInstaller() = get().toolsInstaller
@JvmStatic
fun getTunnelManager() = get().tunnelManager
@JvmStatic
fun getCoroutineScope() = get().coroutineScope
}
init {
@@ -8,19 +8,19 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.ExceptionLoggers
import com.wireguard.android.util.applicationScope
import kotlinx.coroutines.launch
class BootShutdownReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Application.getBackendAsync().thenAccept { backend: Backend? ->
if (backend !is WgQuickBackend) return@thenAccept
val action = intent.action ?: return@thenAccept
applicationScope.launch {
if (Application.getBackend() !is WgQuickBackend) return@launch
val action = intent.action ?: return@launch
val tunnelManager = Application.getTunnelManager()
if (Intent.ACTION_BOOT_COMPLETED == action) {
Log.i(TAG, "Broadcast receiver restoring state (boot)")
tunnelManager.restoreState(false).whenComplete(ExceptionLoggers.D)
tunnelManager.restoreState(false)
} else if (Intent.ACTION_SHUTDOWN == action) {
Log.i(TAG, "Broadcast receiver saving state (shutdown)")
tunnelManager.saveState()
@@ -20,7 +20,9 @@ import com.wireguard.android.activity.MainActivity
import com.wireguard.android.activity.TunnelToggleActivity
import com.wireguard.android.backend.Tunnel
import com.wireguard.android.model.ObservableTunnel
import com.wireguard.android.util.applicationScope
import com.wireguard.android.widget.SlashDrawable
import kotlinx.coroutines.launch
/**
* Service that maintains the application's custom Quick Settings tile. This service is bound by the
@@ -40,7 +42,7 @@ class QuickTileService : TileService() {
var ret: IBinder? = null
try {
ret = super.onBind(intent)
} catch (e: Exception) {
} catch (e: Throwable) {
Log.d(TAG, "Failed to bind to TileService", e)
}
return ret
@@ -54,11 +56,12 @@ class QuickTileService : TileService() {
tile.icon = if (tile.icon == iconOn) iconOff else iconOn
tile.updateTile()
}
tunnel!!.setStateAsync(Tunnel.State.TOGGLE).whenComplete { _, t ->
if (t == null) {
applicationScope.launch {
try {
tunnel!!.setStateAsync(Tunnel.State.TOGGLE)
updateTile()
} else {
val toggleIntent = Intent(this, TunnelToggleActivity::class.java)
} catch (_: Throwable) {
val toggleIntent = Intent(this@QuickTileService, TunnelToggleActivity::class.java)
toggleIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(toggleIntent)
}
@@ -7,6 +7,7 @@ package com.wireguard.android.activity
import android.os.Bundle
import androidx.databinding.CallbackRegistry
import androidx.databinding.CallbackRegistry.NotifierCallback
import androidx.lifecycle.lifecycleScope
import com.wireguard.android.Application
import com.wireguard.android.model.ObservableTunnel
@@ -35,11 +36,8 @@ abstract class BaseActivity : ThemeChangeAwareActivity() {
intent != null -> intent.getStringExtra(KEY_SELECTED_TUNNEL)
else -> null
}
if (savedTunnelName != null) {
Application.getTunnelManager()
.tunnels
.thenAccept { selectedTunnel = it[savedTunnelName] }
}
if (savedTunnelName != null)
lifecycleScope.launchWhenCreated { selectedTunnel = Application.getTunnelManager().getTunnels()[savedTunnelName] }
// The selected tunnel must be set before the superclass method recreates fragments.
super.onCreate(savedInstanceState)
@@ -51,6 +49,7 @@ abstract class BaseActivity : ThemeChangeAwareActivity() {
}
protected abstract fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?)
fun removeOnSelectedTunnelChangedListener(
listener: OnSelectedTunnelChangedListener) {
selectionChangeRegistry.remove(listener)
@@ -19,13 +19,17 @@ import android.text.Spannable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
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 androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ShareCompat
import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -36,13 +40,8 @@ import com.wireguard.android.R
import com.wireguard.android.databinding.LogViewerActivityBinding
import com.wireguard.android.util.DownloadsFileSaver
import com.wireguard.android.util.ErrorMessages
import com.wireguard.android.widget.EdgeToEdge.setUpFAB
import com.wireguard.android.widget.EdgeToEdge.setUpRoot
import com.wireguard.android.widget.EdgeToEdge.setUpScrollingContent
import com.wireguard.crypto.KeyPair
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.BufferedReader
@@ -60,33 +59,26 @@ import java.util.regex.Matcher
import java.util.regex.Pattern
class LogViewerActivity : AppCompatActivity() {
private lateinit var binding: LogViewerActivityBinding
private lateinit var logAdapter: LogEntryAdapter
private var logLines = arrayListOf<LogLine>()
private var rawLogLines = StringBuffer()
private var recyclerView: RecyclerView? = null
private var saveButton: MenuItem? = null
private val coroutineScope = CoroutineScope(Dispatchers.Default)
private val year by lazy {
val yearFormatter: DateFormat = SimpleDateFormat("yyyy", Locale.US)
yearFormatter.format(Date())
}
@Suppress("Deprecation")
private val defaultColor by lazy { resources.getColor(R.color.primary_text_color) }
private val defaultColor by lazy { ResourcesCompat.getColor(resources, R.color.primary_text_color, theme) }
@Suppress("Deprecation")
private val debugColor by lazy { resources.getColor(R.color.debug_tag_color) }
private val debugColor by lazy { ResourcesCompat.getColor(resources, R.color.debug_tag_color, theme) }
@Suppress("Deprecation")
private val errorColor by lazy { resources.getColor(R.color.error_tag_color) }
private val errorColor by lazy { ResourcesCompat.getColor(resources, R.color.error_tag_color, theme) }
@Suppress("Deprecation")
private val infoColor by lazy { resources.getColor(R.color.info_tag_color) }
private val infoColor by lazy { ResourcesCompat.getColor(resources, R.color.info_tag_color, theme) }
@Suppress("Deprecation")
private val warningColor by lazy { resources.getColor(R.color.warning_tag_color) }
private val warningColor by lazy { ResourcesCompat.getColor(resources, R.color.warning_tag_color, theme) }
private var lastUri: Uri? = null
@@ -103,9 +95,6 @@ class LogViewerActivity : AppCompatActivity() {
binding = LogViewerActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
setUpFAB(binding.shareFab)
setUpRoot(binding.root)
setUpScrollingContent(binding.recyclerView, binding.shareFab)
logAdapter = LogEntryAdapter()
binding.recyclerView.apply {
recyclerView = this
@@ -114,7 +103,11 @@ class LogViewerActivity : AppCompatActivity() {
addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL))
}
coroutineScope.launch { streamingLog() }
lifecycleScope.launch(Dispatchers.IO) { streamingLog() }
val revokeLastActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
revokeLastUri()
}
binding.shareFab.setOnClickListener {
revokeLastUri()
@@ -129,17 +122,10 @@ class LogViewerActivity : AppCompatActivity() {
.createChooserIntent()
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
grantUriPermission("android", lastUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivityForResult(shareIntent, SHARE_ACTIVITY_REQUEST)
revokeLastActivityResultLauncher.launch(shareIntent)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == SHARE_ACTIVITY_REQUEST) {
revokeLastUri()
}
super.onActivityResult(requestCode, resultCode, data)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.log_viewer, menu)
saveButton = menu?.findItem(R.id.save_log)
@@ -153,93 +139,90 @@ class LogViewerActivity : AppCompatActivity() {
true
}
R.id.save_log -> {
coroutineScope.launch { saveLog() }
saveButton?.isEnabled = false
lifecycleScope.launch { saveLog() }
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onDestroy() {
super.onDestroy()
coroutineScope.cancel()
}
private val downloadsFileSaver = DownloadsFileSaver(this)
private suspend fun saveLog() {
val context = this
withContext(Dispatchers.Main) {
saveButton?.isEnabled = false
withContext(Dispatchers.IO) {
var exception: Throwable? = null
var outputFile: DownloadsFileSaver.DownloadsFile? = null
try {
outputFile = DownloadsFileSaver.save(context, "wireguard-log.txt", "text/plain", true)
outputFile.outputStream.use {
it.write(rawLogLines.toString().toByteArray(Charsets.UTF_8))
}
} catch (e: Throwable) {
outputFile?.delete()
exception = e
}
withContext(Dispatchers.Main) {
saveButton?.isEnabled = true
Snackbar.make(findViewById(android.R.id.content),
if (exception == null) getString(R.string.log_export_success, outputFile?.fileName)
else getString(R.string.log_export_error, ErrorMessages[exception]),
if (exception == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG)
.setAnchorView(binding.shareFab)
.show()
}
var exception: Throwable? = null
var outputFile: DownloadsFileSaver.DownloadsFile? = null
withContext(Dispatchers.IO) {
try {
outputFile = downloadsFileSaver.save("wireguard-log.txt", "text/plain", true)
outputFile?.outputStream?.write(rawLogLines.toString().toByteArray(Charsets.UTF_8))
} catch (e: Throwable) {
outputFile?.delete()
exception = e
}
}
saveButton?.isEnabled = true
if (outputFile == null)
return
Snackbar.make(findViewById(android.R.id.content),
if (exception == null) getString(R.string.log_export_success, outputFile?.fileName)
else getString(R.string.log_export_error, ErrorMessages[exception]),
if (exception == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG)
.setAnchorView(binding.shareFab)
.show()
}
private suspend fun streamingLog() = withContext(Dispatchers.IO) {
val builder = ProcessBuilder().command("logcat", "-b", "all", "-v", "threadtime", "*:V")
builder.environment()["LC_ALL"] = "C"
val process = try {
builder.start()
} catch (e: IOException) {
e.printStackTrace()
return@withContext
}
val stdout = BufferedReader(InputStreamReader(process!!.inputStream, StandardCharsets.UTF_8))
var haveScrolled = false
val start = System.nanoTime()
var startPeriod = start
while (true) {
val line = stdout.readLine() ?: break
rawLogLines.append(line)
rawLogLines.append('\n')
val logLine = parseLine(line)
withContext(Dispatchers.Main) {
if (logLine != null) {
recyclerView?.let {
val shouldScroll = haveScrolled && !it.canScrollVertically(1)
logLines.add(logLine)
var process: Process? = null
try {
process = try {
builder.start()
} catch (e: IOException) {
Log.e(TAG, Log.getStackTraceString(e))
return@withContext
}
val stdout = BufferedReader(InputStreamReader(process!!.inputStream, StandardCharsets.UTF_8))
var haveScrolled = false
val start = System.nanoTime()
var startPeriod = start
while (true) {
val line = stdout.readLine() ?: break
rawLogLines.append(line)
rawLogLines.append('\n')
val logLine = parseLine(line)
withContext(Dispatchers.Main.immediate) {
if (logLine != null) {
recyclerView?.let {
val shouldScroll = haveScrolled && !it.canScrollVertically(1)
logLines.add(logLine)
if (haveScrolled) logAdapter.notifyDataSetChanged()
if (shouldScroll)
it.scrollToPosition(logLines.size - 1)
}
} else {
/* TODO: I'd prefer for the next line to be:
* logLines.lastOrNull()?.msg += "\n$line"
* However, as of writing, that causes the kotlin compiler to freak out and crash, spewing bytecode.
*/
logLines.lastOrNull()?.apply { msg += "\n$line" }
if (haveScrolled) logAdapter.notifyDataSetChanged()
if (shouldScroll)
it.scrollToPosition(logLines.size - 1)
}
} else {
/* I'd prefer for the next line to be:
* logLines.lastOrNull()?.msg += "\n$line"
* However, as of writing, that causes the kotlin compiler to freak out and crash, spewing bytecode.
*/
logLines.lastOrNull()?.apply { msg += "\n$line" }
if (haveScrolled) logAdapter.notifyDataSetChanged()
}
if (!haveScrolled) {
val end = System.nanoTime()
val scroll = (end - start) > 1000000000L * 2.5 || !stdout.ready()
if (logLines.isNotEmpty() && (scroll || (end - startPeriod) > 1000000000L / 4)) {
logAdapter.notifyDataSetChanged()
recyclerView?.scrollToPosition(logLines.size - 1)
startPeriod = end
if (!haveScrolled) {
val end = System.nanoTime()
val scroll = (end - start) > 1000000000L * 2.5 || !stdout.ready()
if (logLines.isNotEmpty() && (scroll || (end - startPeriod) > 1000000000L / 4)) {
logAdapter.notifyDataSetChanged()
recyclerView?.scrollToPosition(logLines.size - 1)
startPeriod = end
}
if (scroll) haveScrolled = true
}
if (scroll) haveScrolled = true
}
}
} finally {
process?.destroy()
}
}
@@ -271,7 +254,7 @@ class LogViewerActivity : AppCompatActivity() {
*/
private val THREADTIME_LINE: Pattern = Pattern.compile("^(\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3})(?:\\s+[0-9A-Za-z]+)?\\s+(\\d+)\\s+(\\d+)\\s+([A-Z])\\s+(.+?)\\s*: (.*)$")
private val LOGS: MutableMap<String, ByteArray> = ConcurrentHashMap()
private const val SHARE_ACTIVITY_REQUEST = 49133
private const val TAG = "WireGuard/LogViewerActivity"
}
private inner class LogEntryAdapter : RecyclerView.Adapter<LogEntryAdapter.ViewHolder>() {
@@ -348,7 +331,7 @@ class LogViewerActivity : AppCompatActivity() {
return openPipeHelper(uri, "text/plain", null, log) { output, _, _, _, l ->
try {
FileOutputStream(output.fileDescriptor).write(l!!)
} catch (_: Exception) {
} catch (_: Throwable) {
}
}
}
@@ -9,10 +9,10 @@ import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.LinearLayout
import androidx.appcompat.app.ActionBar
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import androidx.fragment.app.commit
import com.wireguard.android.R
import com.wireguard.android.fragment.TunnelDetailFragment
import com.wireguard.android.fragment.TunnelEditorFragment
@@ -55,20 +55,9 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
actionBar = supportActionBar
isTwoPaneLayout = findViewById<View>(R.id.master_detail_wrapper) is LinearLayout
isTwoPaneLayout = findViewById<View?>(R.id.master_detail_wrapper) != null
supportFragmentManager.addOnBackStackChangedListener(this)
onBackStackChanged()
// Dispatch insets on back stack change
// This is required to ensure replaced fragments are also able to consume insets
findViewById<View>(R.id.master_detail_wrapper).setOnApplyWindowInsetsListener { _, insets ->
val fragmentManager = supportFragmentManager
fragmentManager.addOnBackStackChangedListener {
fragmentManager.fragments.forEach {
it.requireView().dispatchApplyWindowInsets(insets)
}
}
insets
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -84,11 +73,11 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener
true
}
R.id.menu_action_edit -> {
supportFragmentManager.beginTransaction()
.replace(R.id.detail_container, TunnelEditorFragment())
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.addToBackStack(null)
.commit()
supportFragmentManager.commit {
replace(R.id.detail_container, TunnelEditorFragment())
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
addToBackStack(null)
}
true
}
// This menu item is handled by the editor fragment.
@@ -116,11 +105,11 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener
fragmentManager.popBackStackImmediate()
} else if (backStackEntries == 0) {
// Create and show a new detail fragment.
fragmentManager.beginTransaction()
.add(R.id.detail_container, TunnelDetailFragment())
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.addToBackStack(null)
.commit()
fragmentManager.commit {
add(R.id.detail_container, TunnelDetailFragment())
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
addToBackStack(null)
}
}
}
}
@@ -5,54 +5,33 @@
package com.wireguard.android.activity
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.util.SparseArray
import android.view.MenuItem
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.wireguard.android.Application
import com.wireguard.android.R
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.preference.PreferencesPreferenceDataStore
import com.wireguard.android.util.AdminKnobs
import com.wireguard.android.util.ModuleLoader
import java.util.ArrayList
import java.util.Arrays
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* Interface for changing application-global persistent settings.
*/
class SettingsActivity : ThemeChangeAwareActivity() {
private val permissionRequestCallbacks = SparseArray<(permissions: Array<String>, granted: IntArray) -> Unit>()
private var permissionRequestCounter = 0
fun ensurePermissions(permissions: Array<String>, cb: (permissions: Array<String>, granted: IntArray) -> Unit) {
val needPermissions: MutableList<String> = ArrayList(permissions.size)
permissions.forEach {
if (ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED) {
needPermissions.add(it)
}
}
if (needPermissions.isEmpty()) {
val granted = IntArray(permissions.size)
Arrays.fill(granted, PackageManager.PERMISSION_GRANTED)
cb.invoke(permissions, granted)
return
}
val idx = permissionRequestCounter++
permissionRequestCallbacks.put(idx, cb)
ActivityCompat.requestPermissions(this,
needPermissions.toTypedArray(), idx)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (supportFragmentManager.findFragmentById(android.R.id.content) == null) {
supportFragmentManager.beginTransaction()
.add(android.R.id.content, SettingsFragment())
.commit()
supportFragmentManager.commit {
add(android.R.id.content, SettingsFragment())
}
}
}
@@ -64,18 +43,9 @@ class SettingsActivity : ThemeChangeAwareActivity() {
return super.onOptionsItemSelected(item)
}
override fun onRequestPermissionsResult(requestCode: Int,
permissions: Array<String>,
grantResults: IntArray) {
val f = permissionRequestCallbacks[requestCode]
if (f != null) {
permissionRequestCallbacks.remove(requestCode)
f.invoke(permissions, grantResults)
}
}
class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, key: String?) {
preferenceManager.preferenceDataStore = PreferencesPreferenceDataStore(lifecycleScope, Application.getPreferencesDataStore())
addPreferencesFromResource(R.xml.preferences)
preferenceScreen.initialExpandedChildrenCount = 4
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@@ -87,14 +57,18 @@ class SettingsActivity : ThemeChangeAwareActivity() {
val remoteApps = preferenceManager.findPreference<Preference>("allow_remote_control_intents")
remoteApps?.parent?.removePreference(remoteApps)
}
if (AdminKnobs.disableConfigExport) {
val zipExporter = preferenceManager.findPreference<Preference>("zip_exporter")
zipExporter?.parent?.removePreference(zipExporter)
}
val wgQuickOnlyPrefs = arrayOf(
preferenceManager.findPreference("tools_installer"),
preferenceManager.findPreference("restore_on_boot"),
preferenceManager.findPreference<Preference>("multiple_tunnels")
).filterNotNull()
wgQuickOnlyPrefs.forEach { it.isVisible = false }
Application.getBackendAsync().thenAccept { backend ->
if (backend is WgQuickBackend) {
lifecycleScope.launch {
if (Application.getBackend() is WgQuickBackend) {
++preferenceScreen.initialExpandedChildrenCount
wgQuickOnlyPrefs.forEach { it.isVisible = true }
} else {
@@ -110,13 +84,24 @@ class SettingsActivity : ThemeChangeAwareActivity() {
moduleInstaller?.isVisible = false
if (ModuleLoader.isModuleLoaded()) {
moduleInstaller?.parent?.removePreference(moduleInstaller)
lifecycleScope.launch {
if (Application.getBackend() !is WgQuickBackend) {
try {
withContext(Dispatchers.IO) { Application.getRootShell().start() }
} catch (_: Throwable) {
kernelModuleDisabler?.parent?.removePreference(kernelModuleDisabler)
}
}
}
} else {
kernelModuleDisabler?.parent?.removePreference(kernelModuleDisabler)
Application.getAsyncWorker().runAsync(Application.getRootShell()::start).whenComplete { _, e ->
if (e == null)
lifecycleScope.launch {
try {
withContext(Dispatchers.IO) { Application.getRootShell().start() }
moduleInstaller?.isVisible = true
else
} catch (_: Throwable) {
moduleInstaller?.parent?.removePreference(moduleInstaller)
}
}
}
}
@@ -4,39 +4,30 @@
*/
package com.wireguard.android.activity
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import com.wireguard.android.Application
import androidx.lifecycle.lifecycleScope
import com.wireguard.android.util.UserKnobs
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
abstract class ThemeChangeAwareActivity : AppCompatActivity(), OnSharedPreferenceChangeListener {
abstract class ThemeChangeAwareActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
Application.getSharedPreferences().registerOnSharedPreferenceChangeListener(this)
}
}
override fun onDestroy() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
Application.getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this)
}
super.onDestroy()
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
when (key) {
"dark_theme" -> {
AppCompatDelegate.setDefaultNightMode(if (sharedPreferences.getBoolean(key, false)) {
UserKnobs.darkTheme.onEach {
val newMode = if (it) {
AppCompatDelegate.MODE_NIGHT_YES
} else {
AppCompatDelegate.MODE_NIGHT_NO
})
recreate()
}
}
if (AppCompatDelegate.getDefaultNightMode() != newMode) {
AppCompatDelegate.setDefaultNightMode(newMode)
recreate()
}
}.launchIn(lifecycleScope)
}
}
}
@@ -5,6 +5,7 @@
package com.wireguard.android.activity
import android.os.Bundle
import androidx.fragment.app.commit
import com.wireguard.android.fragment.TunnelEditorFragment
import com.wireguard.android.model.ObservableTunnel
@@ -15,9 +16,9 @@ class TunnelCreatorActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (supportFragmentManager.findFragmentById(android.R.id.content) == null) {
supportFragmentManager.beginTransaction()
.add(android.R.id.content, TunnelEditorFragment())
.commit()
supportFragmentManager.commit {
add(android.R.id.content, TunnelEditorFragment())
}
}
}
@@ -10,32 +10,53 @@ import android.os.Bundle
import android.service.quicksettings.TileService
import android.util.Log
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.wireguard.android.Application
import com.wireguard.android.QuickTileService
import com.wireguard.android.R
import com.wireguard.android.backend.GoBackend
import com.wireguard.android.backend.Tunnel
import com.wireguard.android.util.ErrorMessages
import kotlinx.coroutines.launch
@RequiresApi(Build.VERSION_CODES.N)
class TunnelToggleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { toggleTunnelWithPermissionsResult() }
private fun toggleTunnelWithPermissionsResult() {
val tunnel = Application.getTunnelManager().lastUsedTunnel ?: return
tunnel.setStateAsync(Tunnel.State.TOGGLE).whenComplete { _, t ->
TileService.requestListeningState(this, ComponentName(this, QuickTileService::class.java))
onToggleFinished(t)
lifecycleScope.launch {
try {
tunnel.setStateAsync(Tunnel.State.TOGGLE)
} catch (e: Throwable) {
TileService.requestListeningState(this@TunnelToggleActivity, ComponentName(this@TunnelToggleActivity, QuickTileService::class.java))
val error = ErrorMessages[e]
val message = getString(R.string.toggle_error, error)
Log.e(TAG, message, e)
Toast.makeText(this@TunnelToggleActivity, message, Toast.LENGTH_LONG).show()
finishAffinity()
return@launch
}
TileService.requestListeningState(this@TunnelToggleActivity, ComponentName(this@TunnelToggleActivity, QuickTileService::class.java))
finishAffinity()
}
}
private fun onToggleFinished(throwable: Throwable?) {
if (throwable == null) return
val error = ErrorMessages[throwable]
val message = getString(R.string.toggle_error, error)
Log.e(TAG, message, throwable)
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
if (Application.getBackend() is GoBackend) {
val intent = GoBackend.VpnService.prepare(this@TunnelToggleActivity)
if (intent != null) {
permissionActivityResultLauncher.launch(intent)
return@launch
}
}
toggleTunnelWithPermissionsResult()
}
}
companion object {
@@ -0,0 +1,391 @@
/*
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity
import android.Manifest
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.storage.StorageManager
import android.os.storage.StorageVolume
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.view.forEach
import androidx.databinding.DataBindingUtil
import androidx.databinding.ObservableBoolean
import androidx.databinding.ObservableField
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup
import com.wireguard.android.Application
import com.wireguard.android.R
import com.wireguard.android.backend.GoBackend
import com.wireguard.android.backend.Tunnel
import com.wireguard.android.databinding.Keyed
import com.wireguard.android.databinding.ObservableKeyedArrayList
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter
import com.wireguard.android.databinding.TvActivityBinding
import com.wireguard.android.databinding.TvFileListItemBinding
import com.wireguard.android.databinding.TvTunnelListItemBinding
import com.wireguard.android.model.ObservableTunnel
import com.wireguard.android.util.ErrorMessages
import com.wireguard.android.util.QuantityFormatter
import com.wireguard.android.util.TunnelImporter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class TvMainActivity : AppCompatActivity() {
private val tunnelFileImportResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { data ->
if (data == null) return@registerForActivityResult
lifecycleScope.launch {
TunnelImporter.importTunnel(contentResolver, data) {
Toast.makeText(this@TvMainActivity, it, Toast.LENGTH_LONG).show()
}
}
}
private var pendingTunnel: ObservableTunnel? = null
private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val tunnel = pendingTunnel
if (tunnel != null)
setTunnelStateWithPermissionsResult(tunnel)
pendingTunnel = null
}
private fun setTunnelStateWithPermissionsResult(tunnel: ObservableTunnel) {
lifecycleScope.launch {
try {
tunnel.setStateAsync(Tunnel.State.TOGGLE)
} catch (e: Throwable) {
val error = ErrorMessages[e]
val message = getString(R.string.error_up, error)
Toast.makeText(this@TvMainActivity, message, Toast.LENGTH_LONG).show()
Log.e(TAG, message, e)
}
updateStats()
}
}
private lateinit var binding: TvActivityBinding
private val isDeleting = ObservableBoolean()
private val files = ObservableKeyedArrayList<String, KeyedFile>()
private val filesRoot = ObservableField("")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = TvActivityBinding.inflate(layoutInflater)
lifecycleScope.launch {
binding.tunnels = Application.getTunnelManager().getTunnels()
if (binding.tunnels?.isEmpty() == true)
binding.importButton.requestFocus()
else
binding.tunnelList.requestFocus()
}
binding.isDeleting = isDeleting
binding.files = files
binding.filesRoot = filesRoot
val gridManager = binding.tunnelList.layoutManager as GridLayoutManager
gridManager.spanSizeLookup = SlatedSpanSizeLookup(gridManager)
binding.tunnelRowConfigurationHandler = object : ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TvTunnelListItemBinding, ObservableTunnel> {
override fun onConfigureRow(binding: TvTunnelListItemBinding, item: ObservableTunnel, position: Int) {
binding.isDeleting = isDeleting
binding.isFocused = ObservableBoolean()
binding.root.setOnFocusChangeListener { _, focused ->
binding.isFocused?.set(focused)
}
binding.root.setOnClickListener {
lifecycleScope.launch {
if (isDeleting.get()) {
try {
item.deleteAsync()
if (this@TvMainActivity.binding.tunnels?.isEmpty() != false)
isDeleting.set(false)
} catch (e: Throwable) {
val error = ErrorMessages[e]
val message = getString(R.string.config_delete_error, error)
Toast.makeText(this@TvMainActivity, message, Toast.LENGTH_LONG).show()
Log.e(TAG, message, e)
}
} else {
if (Application.getBackend() is GoBackend) {
val intent = GoBackend.VpnService.prepare(binding.root.context)
if (intent != null) {
pendingTunnel = item
permissionActivityResultLauncher.launch(intent)
return@launch
}
}
setTunnelStateWithPermissionsResult(item)
}
}
}
}
}
binding.filesRowConfigurationHandler = object : ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TvFileListItemBinding, KeyedFile> {
override fun onConfigureRow(binding: TvFileListItemBinding, item: KeyedFile, position: Int) {
binding.root.setOnClickListener {
if (item.file.isDirectory)
navigateTo(item.file)
else {
val uri = Uri.fromFile(item.file)
files.clear()
filesRoot.set("")
lifecycleScope.launch {
TunnelImporter.importTunnel(contentResolver, uri) {
Toast.makeText(this@TvMainActivity, it, Toast.LENGTH_LONG).show()
}
}
runOnUiThread {
this@TvMainActivity.binding.tunnelList.requestFocus()
}
}
}
}
}
binding.importButton.setOnClickListener {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (filesRoot.get()?.isEmpty() != false) {
navigateTo(File("/"))
runOnUiThread {
binding.filesList.requestFocus()
}
} else {
files.clear()
filesRoot.set("")
runOnUiThread {
binding.tunnelList.requestFocus()
}
}
} else {
try {
tunnelFileImportResultLauncher.launch("*/*")
} catch (_: Throwable) {
Toast.makeText(this@TvMainActivity, getString(R.string.tv_no_file_picker), Toast.LENGTH_LONG).show()
}
}
}
binding.deleteButton.setOnClickListener {
isDeleting.set(!isDeleting.get())
runOnUiThread {
binding.tunnelList.requestFocus()
}
}
binding.executePendingBindings()
setContentView(binding.root)
lifecycleScope.launch {
while (true) {
updateStats()
delay(1000)
}
}
}
private var pendingNavigation: File? = null
private val permissionRequestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
val to = pendingNavigation
if (it && to != null)
navigateTo(to)
pendingNavigation = null
}
private var cachedRoots: Collection<KeyedFile>? = null
private suspend fun makeStorageRoots(): Collection<KeyedFile> = withContext(Dispatchers.IO) {
cachedRoots?.let { return@withContext it }
val list = HashSet<KeyedFile>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val storageManager: StorageManager = getSystemService() ?: return@withContext list
list.addAll(storageManager.storageVolumes.mapNotNull { volume ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
volume.directory?.let { KeyedFile(it, volume.getDescription(this@TvMainActivity)) }
} else {
KeyedFile((StorageVolume::class.java.getMethod("getPathFile").invoke(volume) as File), volume.getDescription(this@TvMainActivity))
}
})
} else {
@Suppress("DEPRECATION")
list.add(KeyedFile(Environment.getExternalStorageDirectory()))
try {
File("/storage").listFiles()?.forEach {
if (!it.isDirectory) return@forEach
try {
if (Environment.isExternalStorageRemovable(it)) {
list.add(KeyedFile(it))
}
} catch (_: Throwable) {
}
}
} catch (_: Throwable) {
}
}
cachedRoots = list
list
}
private fun isBelowCachedRoots(maybeChild: File): Boolean {
val cachedRoots = cachedRoots ?: return true
for (root in cachedRoots) {
if (maybeChild.canonicalPath.startsWith(root.file.canonicalPath))
return false
}
return true
}
private fun navigateTo(directory: File) {
require(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
pendingNavigation = directory
permissionRequestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
return
}
lifecycleScope.launch {
if (isBelowCachedRoots(directory)) {
val roots = makeStorageRoots()
if (roots.count() == 1) {
navigateTo(roots.first().file)
return@launch
}
files.clear()
files.addAll(roots)
filesRoot.set(getString(R.string.tv_select_a_storage_drive))
return@launch
}
val newFiles = withContext(Dispatchers.IO) {
val newFiles = ArrayList<KeyedFile>()
try {
directory.parentFile?.let {
newFiles.add(KeyedFile(it, "../"))
}
val listing = directory.listFiles() ?: return@withContext null
listing.forEach {
if (it.extension == "conf" || it.extension == "zip" || it.isDirectory)
newFiles.add(KeyedFile(it))
}
newFiles.sortWith { a, b ->
if (a.file.isDirectory && !b.file.isDirectory) -1
else if (!a.file.isDirectory && b.file.isDirectory) 1
else a.file.compareTo(b.file)
}
} catch (e: Throwable) {
Log.e(TAG, Log.getStackTraceString(e))
}
newFiles
}
if (newFiles?.isEmpty() != false)
return@launch
files.clear()
files.addAll(newFiles)
filesRoot.set(directory.canonicalPath)
}
}
override fun onBackPressed() {
when {
isDeleting.get() -> {
isDeleting.set(false)
runOnUiThread {
binding.tunnelList.requestFocus()
}
}
filesRoot.get()?.isNotEmpty() == true -> {
files.clear()
filesRoot.set("")
runOnUiThread {
binding.tunnelList.requestFocus()
}
}
else -> super.onBackPressed()
}
}
private suspend fun updateStats() {
binding.tunnelList.forEach { viewItem ->
val listItem = DataBindingUtil.findBinding<TvTunnelListItemBinding>(viewItem)
?: return@forEach
try {
val tunnel = listItem.item!!
if (tunnel.state != Tunnel.State.UP || isDeleting.get()) {
throw Exception()
}
val statistics = tunnel.getStatisticsAsync()
val rx = statistics.totalRx()
val tx = statistics.totalTx()
listItem.tunnelTransfer.text = getString(R.string.transfer_rx_tx, QuantityFormatter.formatBytes(rx), QuantityFormatter.formatBytes(tx))
listItem.tunnelTransfer.visibility = View.VISIBLE
} catch (_: Throwable) {
listItem.tunnelTransfer.visibility = View.GONE
listItem.tunnelTransfer.text = ""
}
}
}
class KeyedFile(val file: File, private val forcedKey: String? = null) : Keyed<String> {
override val key: String
get() = forcedKey ?: if (file.isDirectory) "${file.name}/" else file.name
}
private class SlatedSpanSizeLookup(private val gridManager: GridLayoutManager) : SpanSizeLookup() {
private val originalHeight = gridManager.spanCount
private var newWidth = 0
private lateinit var sizeMap: Array<IntArray?>
private fun emptyUnderIndex(index: Int, size: Int): Int {
sizeMap[size - 1]?.let { return it[index] }
val sizes = IntArray(size)
val oh = originalHeight
val nw = newWidth
var empties = 0
for (i in 0 until size) {
val ox = (i + empties) / oh
val oy = (i + empties) % oh
var empty = 0
for (j in oy + 1 until oh) {
val ni = nw * j + ox
if (ni < size)
break
empty++
}
empties += empty
sizes[i] = empty
}
sizeMap[size - 1] = sizes
return sizes[index]
}
override fun getSpanSize(position: Int): Int {
if (newWidth == 0) {
val child = gridManager.getChildAt(0) ?: return 1
if (child.width == 0) return 1
newWidth = gridManager.width / child.width
sizeMap = Array(originalHeight * newWidth - 1) { null }
}
val total = gridManager.itemCount
if (total >= originalHeight * newWidth || total == 0)
return 1
return emptyUnderIndex(position, total) + 1
}
}
companion object {
private const val TAG = "WireGuard/TvMainActivity"
}
}
@@ -25,7 +25,8 @@ import com.wireguard.android.widget.ToggleSwitch
import com.wireguard.android.widget.ToggleSwitch.OnBeforeCheckedChangeListener
import com.wireguard.config.Attribute
import com.wireguard.config.InetNetwork
import java9.util.Optional
import java.net.InetAddress
import java.util.Optional
/**
* Static methods for use by generated code in the Android data binding library.
@@ -135,13 +136,30 @@ object BindingAdapters {
@JvmStatic
@BindingAdapter("android:text")
fun setText(view: TextView, text: Optional<*>) {
view.text = text.map { it.toString() }.orElse("")
fun setOptionalText(view: TextView, text: Optional<*>?) {
view.text = text?.map { it.toString() }?.orElse("") ?: ""
}
@JvmStatic
@BindingAdapter("android:text")
fun setText(view: TextView, networks: Iterable<InetNetwork?>?) {
fun setInetNetworkSetText(view: TextView, networks: Iterable<InetNetwork?>?) {
view.text = if (networks != null) Attribute.join(networks) else ""
}
@JvmStatic
@BindingAdapter("android:text")
fun setInetAddressSetText(view: TextView, addresses: Iterable<InetAddress?>?) {
view.text = if (addresses != null) Attribute.join(addresses.map { it?.hostAddress }) else ""
}
@JvmStatic
fun tryParseInt(s: String?): Int {
if (s == null)
return 0
return try {
Integer.parseInt(s)
} catch (_: Throwable) {
0
}
}
}
@@ -17,8 +17,10 @@ import java.lang.ref.WeakReference
/**
* A generic `RecyclerView.Adapter` backed by a `ObservableKeyedArrayList`.
*/
class ObservableKeyedRecyclerViewAdapter<K, E : Keyed<out K>> internal constructor(context: Context, private val layoutId: Int,
list: ObservableKeyedArrayList<K, E>?) : RecyclerView.Adapter<ObservableKeyedRecyclerViewAdapter.ViewHolder>() {
class ObservableKeyedRecyclerViewAdapter<K, E : Keyed<out K>> internal constructor(
context: Context, private val layoutId: Int,
list: ObservableKeyedArrayList<K, E>?
) : RecyclerView.Adapter<ObservableKeyedRecyclerViewAdapter.ViewHolder>() {
private val callback = OnListChangedCallback(this)
private val layoutInflater: LayoutInflater = LayoutInflater.from(context)
private var list: ObservableKeyedArrayList<K, E>? = null
@@ -4,7 +4,7 @@
*/
package com.wireguard.android.fragment
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.view.LayoutInflater
@@ -12,13 +12,12 @@ import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.FrameLayout
import androidx.fragment.app.Fragment
import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.zxing.integration.android.IntentIntegrator
import com.wireguard.android.R
import com.wireguard.android.activity.TunnelCreatorActivity
import com.wireguard.android.util.resolveAttribute
class AddTunnelsSheet : BottomSheetDialogFragment() {
@@ -41,7 +40,13 @@ class AddTunnelsSheet : BottomSheetDialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
if (savedInstanceState != null) dismiss()
return inflater.inflate(R.layout.add_tunnels_bottom_sheet, container, false)
val view = inflater.inflate(R.layout.add_tunnels_bottom_sheet, container, false)
if (activity?.packageManager?.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) != true) {
val qrcode = view.findViewById<View>(R.id.create_from_qrcode)
qrcode.isEnabled = false
qrcode.visibility = View.GONE
}
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -81,28 +86,23 @@ class AddTunnelsSheet : BottomSheetDialogFragment() {
behavior?.removeBottomSheetCallback(bottomSheetCallback)
}
private fun requireTargetFragment(): Fragment {
return requireNotNull(targetFragment) { "A target fragment should always be set" }
}
private fun onRequestCreateConfig() {
startActivity(Intent(activity, TunnelCreatorActivity::class.java))
setFragmentResult(REQUEST_KEY_NEW_TUNNEL, bundleOf(REQUEST_METHOD to REQUEST_CREATE))
}
private fun onRequestImportConfig() {
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
}
requireTargetFragment().startActivityForResult(intent, TunnelListFragment.REQUEST_IMPORT)
setFragmentResult(REQUEST_KEY_NEW_TUNNEL, bundleOf(REQUEST_METHOD to REQUEST_IMPORT))
}
private fun onRequestScanQRCode() {
val integrator = IntentIntegrator.forSupportFragment(requireTargetFragment()).apply {
setOrientationLocked(false)
setBeepEnabled(false)
setPrompt(getString(R.string.qr_code_hint))
}
integrator.initiateScan(listOf(IntentIntegrator.QR_CODE))
setFragmentResult(REQUEST_KEY_NEW_TUNNEL, bundleOf(REQUEST_METHOD to REQUEST_SCAN))
}
companion object {
const val REQUEST_KEY_NEW_TUNNEL = "request_new_tunnel"
const val REQUEST_METHOD = "request_method"
const val REQUEST_CREATE = "request_create"
const val REQUEST_IMPORT = "request_import"
const val REQUEST_SCAN = "request_scan"
}
}
@@ -4,65 +4,104 @@
*/
package com.wireguard.android.fragment
import android.Manifest
import android.app.Dialog
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.databinding.Observable
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import com.wireguard.android.Application
import androidx.fragment.app.setFragmentResult
import androidx.lifecycle.lifecycleScope
import com.google.android.material.tabs.TabLayout
import com.wireguard.android.BR
import com.wireguard.android.R
import com.wireguard.android.databinding.AppListDialogFragmentBinding
import com.wireguard.android.databinding.ObservableKeyedArrayList
import com.wireguard.android.model.ApplicationData
import com.wireguard.android.util.ErrorMessages
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class AppListDialogFragment : DialogFragment() {
private val appData: ObservableKeyedArrayList<String, ApplicationData> = ObservableKeyedArrayList()
private var currentlyExcludedApps = emptyList<String>()
private val appData = ObservableKeyedArrayList<String, ApplicationData>()
private var currentlySelectedApps = emptyList<String>()
private var initiallyExcluded = false
private var button: Button? = null
private var tabs: TabLayout? = null
private fun loadData() {
val activity = activity ?: return
val pm = activity.packageManager
Application.getAsyncWorker().supplyAsync<List<ApplicationData>> {
val launcherIntent = Intent(Intent.ACTION_MAIN, null)
launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER)
val resolveInfos = pm.queryIntentActivities(launcherIntent, 0)
val applicationData: MutableList<ApplicationData> = ArrayList()
resolveInfos.forEach {
val packageName = it.activityInfo.packageName
applicationData.add(ApplicationData(it.loadIcon(pm), it.loadLabel(pm).toString(), packageName, currentlyExcludedApps.contains(packageName)))
}
applicationData.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
applicationData
}.whenComplete { data, throwable ->
if (data != null) {
appData.clear()
appData.addAll(data)
} else {
val error = ErrorMessages[throwable]
val message = activity.getString(R.string.error_fetching_apps, error)
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
dismissAllowingStateLoss()
lifecycleScope.launch(Dispatchers.Default) {
try {
val applicationData: MutableList<ApplicationData> = ArrayList()
withContext(Dispatchers.IO) {
val packageInfos = pm.getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET), 0)
packageInfos.forEach {
val packageName = it.packageName
val appInfo = it.applicationInfo
val appData = ApplicationData(appInfo.loadIcon(pm), appInfo.loadLabel(pm).toString(), packageName, currentlySelectedApps.contains(packageName))
applicationData.add(appData)
appData.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
if (propertyId == BR.selected)
setButtonText()
}
})
}
}
applicationData.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
withContext(Dispatchers.Main.immediate) {
appData.clear()
appData.addAll(applicationData)
}
} catch (e: Throwable) {
withContext(Dispatchers.Main.immediate) {
val error = ErrorMessages[e]
val message = activity.getString(R.string.error_fetching_apps, error)
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
dismissAllowingStateLoss()
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val excludedApps = requireArguments().getStringArrayList(KEY_EXCLUDED_APPS)
currentlyExcludedApps = (excludedApps ?: emptyList())
currentlySelectedApps = (arguments?.getStringArrayList(KEY_SELECTED_APPS) ?: emptyList())
initiallyExcluded = arguments?.getBoolean(KEY_IS_EXCLUDED) ?: true
}
private fun setButtonText() {
val numSelected = appData.count { it.isSelected }
button?.text = if (numSelected == 0)
getString(R.string.use_all_applications)
else when (tabs?.selectedTabPosition) {
0 -> resources.getQuantityString(R.plurals.exclude_n_applications, numSelected, numSelected)
1 -> resources.getQuantityString(R.plurals.include_n_applications, numSelected, numSelected)
else -> null
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val alertDialogBuilder = AlertDialog.Builder(requireActivity())
alertDialogBuilder.setTitle(R.string.excluded_applications)
val binding = AppListDialogFragmentBinding.inflate(requireActivity().layoutInflater, null, false)
binding.executePendingBindings()
alertDialogBuilder.setView(binding.root)
alertDialogBuilder.setPositiveButton(R.string.set_exclusions) { _, _ -> setExclusionsAndDismiss() }
tabs = binding.tabs
tabs?.apply {
selectTab(binding.tabs.getTabAt(if (initiallyExcluded) 0 else 1))
addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabReselected(tab: TabLayout.Tab?) = Unit
override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
override fun onTabSelected(tab: TabLayout.Tab?) = setButtonText()
})
}
alertDialogBuilder.setPositiveButton(" ") { _, _ -> setSelectionAndDismiss() }
alertDialogBuilder.setNegativeButton(R.string.cancel) { dialog, _ -> dialog.dismiss() }
alertDialogBuilder.setNeutralButton(R.string.toggle_all) { _, _ -> }
binding.fragment = this
@@ -70,41 +109,41 @@ class AppListDialogFragment : DialogFragment() {
loadData()
val dialog = alertDialogBuilder.create()
dialog.setOnShowListener {
dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener {
val selectedItems = appData
.filter { it.isExcludedFromTunnel }
val excludeAll = selectedItems.isEmpty()
button = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
setButtonText()
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener { _ ->
val selectAll = appData.none { it.isSelected }
appData.forEach {
it.isExcludedFromTunnel = excludeAll
it.isSelected = selectAll
}
}
}
return dialog
}
private fun setExclusionsAndDismiss() {
val excludedApps: MutableList<String> = ArrayList()
private fun setSelectionAndDismiss() {
val selectedApps: MutableList<String> = ArrayList()
for (data in appData) {
if (data.isExcludedFromTunnel) {
excludedApps.add(data.packageName)
if (data.isSelected) {
selectedApps.add(data.packageName)
}
}
(targetFragment as AppExclusionListener?)!!.onExcludedAppsSelected(excludedApps)
setFragmentResult(REQUEST_SELECTION, bundleOf(
KEY_SELECTED_APPS to selectedApps.toTypedArray(),
KEY_IS_EXCLUDED to (tabs?.selectedTabPosition == 0)
))
dismiss()
}
interface AppExclusionListener {
fun onExcludedAppsSelected(excludedApps: List<String>)
}
companion object {
private const val KEY_EXCLUDED_APPS = "excludedApps"
fun <T> newInstance(excludedApps: ArrayList<String?>?, target: T): AppListDialogFragment where T : Fragment?, T : AppExclusionListener? {
const val KEY_SELECTED_APPS = "selected_apps"
const val KEY_IS_EXCLUDED = "is_excluded"
const val REQUEST_SELECTION = "request_selection"
fun newInstance(selectedApps: ArrayList<String?>?, isExcluded: Boolean): AppListDialogFragment {
val extras = Bundle()
extras.putStringArrayList(KEY_EXCLUDED_APPS, excludedApps)
extras.putStringArrayList(KEY_SELECTED_APPS, selectedApps)
extras.putBoolean(KEY_IS_EXCLUDED, isExcluded)
val fragment = AppListDialogFragment()
fragment.setTargetFragment(target, 0)
fragment.arguments = extras
return fragment
}
@@ -5,62 +5,56 @@
package com.wireguard.android.fragment
import android.content.Context
import android.content.Intent
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar
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.Backend
import com.wireguard.android.backend.GoBackend
import com.wireguard.android.backend.Tunnel
import com.wireguard.android.databinding.TunnelDetailFragmentBinding
import com.wireguard.android.databinding.TunnelListItemBinding
import com.wireguard.android.model.ObservableTunnel
import com.wireguard.android.util.ErrorMessages
import kotlinx.coroutines.launch
/**
* Base class for fragments that need to know the currently-selected tunnel. Only does anything when
* attached to a `BaseActivity`.
*/
abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
private var baseActivity: BaseActivity? = null
private var pendingTunnel: ObservableTunnel? = null
private var pendingTunnelUp: Boolean? = null
protected var selectedTunnel: ObservableTunnel?
get() = baseActivity?.selectedTunnel
protected set(tunnel) {
baseActivity?.selectedTunnel = tunnel
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE_VPN_PERMISSION) {
if (pendingTunnel != null && pendingTunnelUp != null) setTunnelStateWithPermissionsResult(pendingTunnel!!, pendingTunnelUp!!)
pendingTunnel = null
pendingTunnelUp = null
}
private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val tunnel = pendingTunnel
val checked = pendingTunnelUp
if (tunnel != null && checked != null)
setTunnelStateWithPermissionsResult(tunnel, checked)
pendingTunnel = null
pendingTunnelUp = null
}
protected var selectedTunnel: ObservableTunnel?
get() = (activity as? BaseActivity)?.selectedTunnel
protected set(tunnel) {
(activity as? BaseActivity)?.selectedTunnel = tunnel
}
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is BaseActivity) {
baseActivity = context
baseActivity?.addOnSelectedTunnelChangedListener(this)
} else {
baseActivity = null
}
(activity as? BaseActivity)?.addOnSelectedTunnelChangedListener(this)
}
override fun onDetach() {
baseActivity?.removeOnSelectedTunnelChangedListener(this)
baseActivity = null
(activity as? BaseActivity)?.removeOnSelectedTunnelChangedListener(this)
super.onDetach()
}
@@ -70,14 +64,15 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
is TunnelListItemBinding -> binding.item
else -> return
} ?: return
Application.getBackendAsync().thenAccept { backend: Backend? ->
if (backend is GoBackend) {
val intent = GoBackend.VpnService.prepare(view.context)
val activity = activity ?: return
activity.lifecycleScope.launch {
if (Application.getBackend() is GoBackend) {
val intent = GoBackend.VpnService.prepare(activity)
if (intent != null) {
pendingTunnel = tunnel
pendingTunnelUp = checked
startActivityForResult(intent, REQUEST_CODE_VPN_PERMISSION)
return@thenAccept
permissionActivityResultLauncher.launch(intent)
return@launch
}
}
setTunnelStateWithPermissionsResult(tunnel, checked)
@@ -85,24 +80,27 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
}
private fun setTunnelStateWithPermissionsResult(tunnel: ObservableTunnel, checked: Boolean) {
tunnel.setStateAsync(Tunnel.State.of(checked)).whenComplete { _, throwable ->
if (throwable == null) return@whenComplete
val error = ErrorMessages[throwable]
val messageResId = if (checked) R.string.error_up else R.string.error_down
val message = requireContext().getString(messageResId, error)
val view = view
if (view != null)
Snackbar.make(view, message, Snackbar.LENGTH_LONG)
.setAnchorView(view.findViewById<View>(R.id.create_fab))
.show()
else
Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show()
Log.e(TAG, message, throwable)
val activity = activity ?: return
activity.lifecycleScope.launch {
try {
tunnel.setStateAsync(Tunnel.State.of(checked))
} catch (e: Throwable) {
val error = ErrorMessages[e]
val messageResId = if (checked) R.string.error_up else R.string.error_down
val message = activity.getString(messageResId, error)
val view = view
if (view != null)
Snackbar.make(view, message, Snackbar.LENGTH_LONG)
.setAnchorView(view.findViewById(R.id.create_fab))
.show()
else
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
Log.e(TAG, message, e)
}
}
}
companion object {
private const val REQUEST_CODE_VPN_PERMISSION = 23491
private const val TAG = "WireGuard/BaseFragment"
}
}
@@ -5,17 +5,20 @@
package com.wireguard.android.fragment
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AlertDialog
import androidx.core.content.getSystemService
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import com.google.android.material.textfield.TextInputEditText
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 kotlinx.coroutines.launch
import java.io.ByteArrayInputStream
import java.io.IOException
import java.nio.charset.StandardCharsets
@@ -26,14 +29,15 @@ class ConfigNamingDialogFragment : DialogFragment() {
private var imm: InputMethodManager? = null
private fun createTunnelAndDismiss() {
binding?.let {
val name = it.tunnelNameText.text.toString()
Application.getTunnelManager().create(name, config).whenComplete { tunnel, throwable ->
if (tunnel != null) {
dismiss()
} else {
it.tunnelNameTextLayout.error = throwable.message
}
val binding = binding ?: return
val activity = activity ?: return
val name = binding.tunnelNameText.text.toString()
activity.lifecycleScope.launch {
try {
Application.getTunnelManager().create(name, config)
dismiss()
} catch (e: Throwable) {
binding.tunnelNameTextLayout.error = e.message
}
}
}
@@ -49,7 +53,7 @@ class ConfigNamingDialogFragment : DialogFragment() {
val configBytes = configText!!.toByteArray(StandardCharsets.UTF_8)
config = try {
Config.parse(ByteArrayInputStream(configBytes))
} catch (e: Exception) {
} catch (e: Throwable) {
when (e) {
is BadConfigException, is IOException -> throw IllegalArgumentException("Invalid config passed to ${javaClass.simpleName}", e)
else -> throw e
@@ -59,7 +63,7 @@ class ConfigNamingDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val activity = requireActivity()
imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm = activity.getSystemService()
val alertDialogBuilder = AlertDialog.Builder(activity)
alertDialogBuilder.setTitle(R.string.import_from_qr_code)
binding = ConfigNamingDialogFragmentBinding.inflate(activity.layoutInflater, null, false)
@@ -69,7 +73,18 @@ class ConfigNamingDialogFragment : DialogFragment() {
}
alertDialogBuilder.setPositiveButton(R.string.create_tunnel, null)
alertDialogBuilder.setNegativeButton(R.string.cancel) { _, _ -> dismiss() }
return alertDialogBuilder.create()
return alertDialogBuilder.create().apply {
setOnShowListener {
findViewById<TextInputEditText>(R.id.tunnel_name_text)?.apply {
setOnFocusChangeListener { v, _ ->
v.post {
imm?.showSoftInput(v, InputMethodManager.SHOW_IMPLICIT)
}
}
requestFocus()
}
}
}
}
override fun onResume() {
@@ -11,15 +11,15 @@ import android.view.MenuInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.lifecycleScope
import com.wireguard.android.R
import com.wireguard.android.backend.Tunnel
import com.wireguard.android.databinding.TunnelDetailFragmentBinding
import com.wireguard.android.databinding.TunnelDetailPeerBinding
import com.wireguard.android.model.ObservableTunnel
import com.wireguard.android.widget.EdgeToEdge.setUpRoot
import com.wireguard.android.widget.EdgeToEdge.setUpScrollingContent
import java.util.Timer
import java.util.TimerTask
import com.wireguard.android.util.QuantityFormatter
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/**
* Fragment that shows details about a specific tunnel.
@@ -27,18 +27,7 @@ import java.util.TimerTask
class TunnelDetailFragment : BaseFragment() {
private var binding: TunnelDetailFragmentBinding? = null
private var lastState = Tunnel.State.TOGGLE
private var timer: Timer? = null
private fun formatBytes(bytes: Long): String {
val context = requireContext()
return when {
bytes < 1024 -> context.getString(R.string.transfer_bytes, bytes)
bytes < 1024 * 1024 -> context.getString(R.string.transfer_kibibytes, bytes / 1024.0)
bytes < 1024 * 1024 * 1024 -> context.getString(R.string.transfer_mibibytes, bytes / (1024.0 * 1024.0))
bytes < 1024 * 1024 * 1024 * 1024L -> context.getString(R.string.transfer_gibibytes, bytes / (1024.0 * 1024.0 * 1024.0))
else -> context.getString(R.string.transfer_tibibytes, bytes / (1024.0 * 1024.0 * 1024.0) / 1024.0)
}
}
private var timerActive = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -53,11 +42,7 @@ class TunnelDetailFragment : BaseFragment() {
savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
binding = TunnelDetailFragmentBinding.inflate(inflater, container, false)
binding?.apply {
executePendingBindings()
setUpRoot(root as ViewGroup)
setUpScrollingContent(root as ViewGroup, null)
}
binding?.executePendingBindings()
return binding!!.root
}
@@ -68,28 +53,36 @@ class TunnelDetailFragment : BaseFragment() {
override fun onResume() {
super.onResume()
timer = Timer()
timer!!.scheduleAtFixedRate(object : TimerTask() {
override fun run() {
timerActive = true
lifecycleScope.launch {
while (timerActive) {
updateStats()
delay(1000)
}
}, 0, 1000)
}
}
override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?) {
binding ?: return
binding!!.tunnel = newTunnel
if (newTunnel == null) binding!!.config = null else newTunnel.configAsync.thenAccept { config -> binding!!.config = config }
val binding = binding ?: return
binding.tunnel = newTunnel
if (newTunnel == null) {
binding.config = null
} else {
lifecycleScope.launch {
try {
binding.config = newTunnel.getConfigAsync()
} catch (_: Throwable) {
binding.config = null
}
}
}
lastState = Tunnel.State.TOGGLE
updateStats()
lifecycleScope.launch { updateStats() }
}
override fun onStop() {
timerActive = false
super.onStop()
if (timer != null) {
timer!!.cancel()
timer = null
}
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
@@ -99,24 +92,17 @@ class TunnelDetailFragment : BaseFragment() {
super.onViewStateRestored(savedInstanceState)
}
private fun updateStats() {
if (binding == null || !isResumed) return
val tunnel = binding!!.tunnel ?: return
private suspend fun updateStats() {
val binding = binding ?: return
val tunnel = binding.tunnel ?: return
if (!isResumed) return
val state = tunnel.state
if (state != Tunnel.State.UP && lastState == state) return
lastState = state
tunnel.statisticsAsync.whenComplete { statistics, throwable ->
if (throwable != null) {
for (i in 0 until binding!!.peersLayout.childCount) {
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding!!.peersLayout.getChildAt(i))
?: continue
peer.transferLabel.visibility = View.GONE
peer.transferText.visibility = View.GONE
}
return@whenComplete
}
for (i in 0 until binding!!.peersLayout.childCount) {
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding!!.peersLayout.getChildAt(i))
try {
val statistics = tunnel.getStatisticsAsync()
for (i in 0 until binding.peersLayout.childCount) {
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i))
?: continue
val publicKey = peer.item!!.publicKey
val rx = statistics.peerRx(publicKey)
@@ -126,10 +112,17 @@ class TunnelDetailFragment : BaseFragment() {
peer.transferText.visibility = View.GONE
continue
}
peer.transferText.text = requireContext().getString(R.string.transfer_rx_tx, formatBytes(rx), formatBytes(tx))
peer.transferText.text = getString(R.string.transfer_rx_tx, QuantityFormatter.formatBytes(rx), QuantityFormatter.formatBytes(tx))
peer.transferLabel.visibility = View.VISIBLE
peer.transferText.visibility = View.VISIBLE
}
} catch (e: Throwable) {
for (i in 0 until binding.peersLayout.childCount) {
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i))
?: continue
peer.transferLabel.visibility = View.GONE
peer.transferText.visibility = View.GONE
}
}
}
}
@@ -18,45 +18,48 @@ import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.Toast
import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar
import com.wireguard.android.Application
import com.wireguard.android.R
import com.wireguard.android.backend.Tunnel
import com.wireguard.android.databinding.TunnelEditorFragmentBinding
import com.wireguard.android.fragment.AppListDialogFragment.AppExclusionListener
import com.wireguard.android.model.ObservableTunnel
import com.wireguard.android.util.AdminKnobs
import com.wireguard.android.util.BiometricAuthenticator
import com.wireguard.android.util.ErrorMessages
import com.wireguard.android.viewmodel.ConfigProxy
import com.wireguard.android.widget.EdgeToEdge.setUpRoot
import com.wireguard.android.widget.EdgeToEdge.setUpScrollingContent
import com.wireguard.config.Config
import kotlinx.coroutines.launch
/**
* Fragment for editing a WireGuard configuration.
*/
class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
class TunnelEditorFragment : BaseFragment() {
private var haveShownKeys = false
private var binding: TunnelEditorFragmentBinding? = null
private var tunnel: ObservableTunnel? = null
private fun onConfigLoaded(config: Config) {
binding?.config = ConfigProxy(config)
}
private fun onConfigSaved(savedTunnel: Tunnel, throwable: Throwable?) {
val message: String
val ctx = activity ?: Application.get()
if (throwable == null) {
message = getString(R.string.config_save_success, savedTunnel.name)
val message = ctx.getString(R.string.config_save_success, savedTunnel.name)
Log.d(TAG, message)
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
onFinished()
} else {
val error = ErrorMessages[throwable]
message = getString(R.string.config_save_error, savedTunnel.name, error)
val message = ctx.getString(R.string.config_save_error, savedTunnel.name, error)
Log.e(TAG, message, throwable)
binding?.let {
Snackbar.make(it.mainContainer, message, Snackbar.LENGTH_LONG).show()
}
val binding = binding
if (binding != null)
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show()
else
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
}
}
@@ -75,8 +78,6 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
binding = TunnelEditorFragmentBinding.inflate(inflater, container, false)
binding?.apply {
executePendingBindings()
setUpRoot(root as ViewGroup)
setUpScrollingContent(mainContainer, null)
privateKeyTextLayout.setEndIconOnClickListener { config?.`interface`?.generateKeyPair() }
}
return binding?.root
@@ -88,14 +89,6 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
super.onDestroyView()
}
override fun onExcludedAppsSelected(excludedApps: List<String>) {
requireNotNull(binding) { "Tried to set excluded apps while no view was loaded" }
binding!!.config!!.`interface`.excludedApplications.apply {
clear()
addAll(excludedApps)
}
}
private fun onFinished() {
// Hide the keyboard; it rarely goes away on its own.
val activity = activity ?: return
@@ -105,14 +98,11 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
inputManager?.hideSoftInputFromWindow(focusedView.windowToken,
InputMethodManager.HIDE_NOT_ALWAYS)
}
// Tell the activity to finish itself or go back to the detail view.
activity.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.
val savedTunnel = tunnel
if (savedTunnel === selectedTunnel) selectedTunnel = null
selectedTunnel = savedTunnel
}
parentFragmentManager.popBackStackImmediate()
// If we just made a new one, save it to select the details page.
if (selectedTunnel != tunnel)
selectedTunnel = tunnel
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@@ -120,7 +110,7 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
binding ?: return false
val newConfig = try {
binding!!.config!!.resolve()
} catch (e: Exception) {
} catch (e: Throwable) {
val error = ErrorMessages[e]
val tunnelName = if (tunnel == null) binding!!.name else tunnel!!.name
val message = getString(R.string.config_save_error, tunnelName, error)
@@ -128,20 +118,36 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
Snackbar.make(binding!!.mainContainer, error, Snackbar.LENGTH_LONG).show()
return false
}
when {
tunnel == null -> {
Log.d(TAG, "Attempting to create new tunnel " + binding!!.name)
val manager = Application.getTunnelManager()
manager.create(binding!!.name!!, newConfig).whenComplete(this::onTunnelCreated)
}
tunnel!!.name != binding!!.name -> {
Log.d(TAG, "Attempting to rename tunnel to " + binding!!.name)
tunnel!!.setNameAsync(binding!!.name!!).whenComplete { _, t -> onTunnelRenamed(tunnel!!, newConfig, t) }
}
else -> {
Log.d(TAG, "Attempting to save config of " + tunnel!!.name)
tunnel!!.setConfigAsync(newConfig)
.whenComplete { _, t -> onConfigSaved(tunnel!!, t) }
val activity = requireActivity()
activity.lifecycleScope.launch {
when {
tunnel == null -> {
Log.d(TAG, "Attempting to create new tunnel " + binding!!.name)
val manager = Application.getTunnelManager()
try {
onTunnelCreated(manager.create(binding!!.name!!, newConfig), null)
} catch (e: Throwable) {
onTunnelCreated(null, e)
}
}
tunnel!!.name != binding!!.name -> {
Log.d(TAG, "Attempting to rename tunnel to " + binding!!.name)
try {
tunnel!!.setNameAsync(binding!!.name!!)
onTunnelRenamed(tunnel!!, newConfig, null)
} catch (e: Throwable) {
onTunnelRenamed(tunnel!!, newConfig, e)
}
}
else -> {
Log.d(TAG, "Attempting to save config of " + tunnel!!.name)
try {
tunnel!!.setConfigAsync(newConfig)
onConfigSaved(tunnel!!, null)
} catch (e: Throwable) {
onConfigSaved(tunnel!!, e)
}
}
}
}
return true
@@ -150,11 +156,35 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
}
@Suppress("UNUSED_PARAMETER")
fun onRequestSetExcludedApplications(view: View?) {
fun onRequestSetExcludedIncludedApplications(view: View?) {
if (binding != null) {
val excludedApps = ArrayList(binding!!.config!!.`interface`.excludedApplications)
val fragment = AppListDialogFragment.newInstance(excludedApps, this)
fragment.show(parentFragmentManager, null)
var isExcluded = true
var selectedApps = ArrayList(binding!!.config!!.`interface`.excludedApplications)
if (selectedApps.isEmpty()) {
selectedApps = ArrayList(binding!!.config!!.`interface`.includedApplications)
if (selectedApps.isNotEmpty())
isExcluded = false
}
val fragment = AppListDialogFragment.newInstance(selectedApps, isExcluded)
childFragmentManager.setFragmentResultListener(AppListDialogFragment.REQUEST_SELECTION, viewLifecycleOwner) { _, bundle ->
requireNotNull(binding) { "Tried to set excluded/included apps while no view was loaded" }
val newSelections = requireNotNull(bundle.getStringArray(AppListDialogFragment.KEY_SELECTED_APPS))
val excluded = requireNotNull(bundle.getBoolean(AppListDialogFragment.KEY_IS_EXCLUDED))
if (excluded) {
binding!!.config!!.`interface`.includedApplications.clear()
binding!!.config!!.`interface`.excludedApplications.apply {
clear()
addAll(newSelections)
}
} else {
binding!!.config!!.`interface`.excludedApplications.clear()
binding!!.config!!.`interface`.includedApplications.apply {
clear()
addAll(newSelections)
}
}
}
fragment.show(childFragmentManager, null)
}
}
@@ -171,46 +201,60 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
binding!!.config = ConfigProxy()
if (tunnel != null) {
binding!!.name = tunnel!!.name
tunnel!!.configAsync.thenAccept(this::onConfigLoaded)
lifecycleScope.launch {
try {
onConfigLoaded(tunnel!!.getConfigAsync())
} catch (_: Throwable) {
}
}
} else {
binding!!.name = ""
}
}
private fun onTunnelCreated(newTunnel: ObservableTunnel, throwable: Throwable?) {
val message: String
private fun onTunnelCreated(newTunnel: ObservableTunnel?, throwable: Throwable?) {
val ctx = activity ?: Application.get()
if (throwable == null) {
tunnel = newTunnel
message = getString(R.string.tunnel_create_success, tunnel!!.name)
val message = ctx.getString(R.string.tunnel_create_success, tunnel!!.name)
Log.d(TAG, message)
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
onFinished()
} else {
val error = ErrorMessages[throwable]
message = getString(R.string.tunnel_create_error, error)
val message = ctx.getString(R.string.tunnel_create_error, error)
Log.e(TAG, message, throwable)
binding?.let {
Snackbar.make(it.mainContainer, message, Snackbar.LENGTH_LONG).show()
}
val binding = binding
if (binding != null)
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show()
else
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
}
}
private fun onTunnelRenamed(renamedTunnel: ObservableTunnel, newConfig: Config,
throwable: Throwable?) {
val message: String
private suspend fun onTunnelRenamed(renamedTunnel: ObservableTunnel, newConfig: Config,
throwable: Throwable?) {
val ctx = activity ?: Application.get()
if (throwable == null) {
message = getString(R.string.tunnel_rename_success, renamedTunnel.name)
val message = ctx.getString(R.string.tunnel_rename_success, renamedTunnel.name)
Log.d(TAG, message)
// Now save the rest of configuration changes.
Log.d(TAG, "Attempting to save config of renamed tunnel " + tunnel!!.name)
renamedTunnel.setConfigAsync(newConfig).whenComplete { _, t -> onConfigSaved(renamedTunnel, t) }
try {
renamedTunnel.setConfigAsync(newConfig)
onConfigSaved(renamedTunnel, null)
} catch (e: Throwable) {
onConfigSaved(renamedTunnel, e)
}
} else {
val error = ErrorMessages[throwable]
message = getString(R.string.tunnel_rename_error, error)
val message = ctx.getString(R.string.tunnel_rename_error, error)
Log.e(TAG, message, throwable)
binding?.let {
Snackbar.make(it.mainContainer, message, Snackbar.LENGTH_LONG).show()
}
val binding = binding
if (binding != null)
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show()
else
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
}
}
@@ -235,7 +279,9 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
fun onKeyFocusChange(view: View, isFocused: Boolean) {
if (!isFocused || showingAuthenticator) return
val edit = view as? EditText ?: return
if (edit.inputType == InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) return
if (!haveShownKeys && edit.text.isNotEmpty()) {
if (AdminKnobs.disableConfigExport) return
showingAuthenticator = true
BiometricAuthenticator.authenticate(R.string.biometric_prompt_private_key_title, this) {
showingAuthenticator = false
@@ -4,46 +4,40 @@
*/
package com.wireguard.android.fragment
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.res.Resources
import android.net.Uri
import android.os.Bundle
import android.provider.OpenableColumns
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 android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar
import com.google.zxing.integration.android.IntentIntegrator
import com.wireguard.android.Application
import com.wireguard.android.R
import com.wireguard.android.activity.TunnelCreatorActivity
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler
import com.wireguard.android.databinding.TunnelListFragmentBinding
import com.wireguard.android.databinding.TunnelListItemBinding
import com.wireguard.android.fragment.ConfigNamingDialogFragment.Companion.newInstance
import com.wireguard.android.model.ObservableTunnel
import com.wireguard.android.util.ErrorMessages
import com.wireguard.android.widget.EdgeToEdge.setUpFAB
import com.wireguard.android.widget.EdgeToEdge.setUpRoot
import com.wireguard.android.widget.EdgeToEdge.setUpScrollingContent
import com.wireguard.android.util.TunnelImporter
import com.wireguard.android.widget.MultiselectableRelativeLayout
import com.wireguard.config.Config
import java9.util.concurrent.CompletableFuture
import java.io.BufferedReader
import java.io.ByteArrayInputStream
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
import java.util.ArrayList
import java.util.HashSet
import java.util.Locale
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
/**
* Fragment containing a list of known WireGuard tunnels. It allows creating and deleting tunnels.
@@ -52,122 +46,25 @@ class TunnelListFragment : BaseFragment() {
private val actionModeListener = ActionModeListener()
private var actionMode: ActionMode? = null
private var binding: TunnelListFragmentBinding? = null
private fun importTunnel(configText: String) {
try {
// Ensure the config text is parseable before proceeding…
Config.parse(ByteArrayInputStream(configText.toByteArray(StandardCharsets.UTF_8)))
// Config text is valid, now create the tunnel…
newInstance(configText).show(parentFragmentManager, null)
} catch (e: Exception) {
onTunnelImportFinished(emptyList(), listOf<Throwable>(e))
private val tunnelFileImportResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { data ->
if (data == null) return@registerForActivityResult
val activity = activity ?: return@registerForActivityResult
val contentResolver = activity.contentResolver ?: return@registerForActivityResult
activity.lifecycleScope.launch {
TunnelImporter.importTunnel(contentResolver, data) { showSnackbar(it) }
}
}
private fun importTunnel(uri: Uri?) {
val activity = activity
if (activity == null || uri == null) {
return
}
val contentResolver = activity.contentResolver
val futureTunnels = ArrayList<CompletableFuture<ObservableTunnel>>()
val throwables = ArrayList<Throwable>()
Application.getAsyncWorker().supplyAsync {
val columns = arrayOf(OpenableColumns.DISPLAY_NAME)
var name = ""
contentResolver.query(uri, columns, null, null, null)?.use { cursor ->
if (cursor.moveToFirst() && !cursor.isNull(0)) {
name = cursor.getString(0)
}
}
if (name.isEmpty()) {
name = Uri.decode(uri.lastPathSegment)
}
var idx = name.lastIndexOf('/')
if (idx >= 0) {
require(idx < name.length - 1) { resources.getString(R.string.illegal_filename_error, name) }
name = name.substring(idx + 1)
}
val isZip = name.toLowerCase(Locale.ROOT).endsWith(".zip")
if (name.toLowerCase(Locale.ROOT).endsWith(".conf")) {
name = name.substring(0, name.length - ".conf".length)
} else {
require(isZip) { resources.getString(R.string.bad_extension_error) }
}
if (isZip) {
ZipInputStream(contentResolver.openInputStream(uri)).use { zip ->
val reader = BufferedReader(InputStreamReader(zip, StandardCharsets.UTF_8))
var entry: ZipEntry?
while (true) {
entry = zip.nextEntry ?: break
name = entry.name
idx = name.lastIndexOf('/')
if (idx >= 0) {
if (idx >= name.length - 1) {
continue
}
name = name.substring(name.lastIndexOf('/') + 1)
}
if (name.toLowerCase(Locale.ROOT).endsWith(".conf")) {
name = name.substring(0, name.length - ".conf".length)
} else {
continue
}
try {
Config.parse(reader)
} catch (e: Exception) {
throwables.add(e)
null
}?.let {
futureTunnels.add(Application.getTunnelManager().create(name, it).toCompletableFuture())
}
}
}
} else {
futureTunnels.add(
Application.getTunnelManager().create(
name,
Config.parse(contentResolver.openInputStream(uri)!!)
).toCompletableFuture()
)
}
if (futureTunnels.isEmpty()) {
if (throwables.size == 1) {
throw throwables[0]
} else {
require(throwables.isNotEmpty()) { resources.getString(R.string.no_configs_error) }
}
}
CompletableFuture.allOf(*futureTunnels.toTypedArray())
}.whenComplete { future, exception ->
if (exception != null) {
onTunnelImportFinished(emptyList(), listOf(exception))
} else {
future.whenComplete { _, _ ->
val tunnels = mutableListOf<ObservableTunnel>()
for (futureTunnel in futureTunnels) {
val tunnel: ObservableTunnel? = try {
futureTunnel.getNow(null)
} catch (e: Exception) {
throwables.add(e)
null
}
if (tunnel != null) {
tunnels.add(tunnel)
}
}
onTunnelImportFinished(tunnels, throwables)
}
}
}
private val qrImportResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val qrCode = IntentIntegrator.parseActivityResult(result.resultCode, result.data)?.contents
?: return@registerForActivityResult
val activity = activity ?: return@registerForActivityResult
val fragManager = parentFragmentManager
activity.lifecycleScope.launch { TunnelImporter.importTunnel(fragManager, qrCode) { showSnackbar(it) } }
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (savedInstanceState != null) {
val checkedItems = savedInstanceState.getIntegerArrayList(CHECKED_ITEMS)
if (checkedItems != null) {
@@ -176,38 +73,33 @@ class TunnelListFragment : BaseFragment() {
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQUEST_IMPORT -> {
if (resultCode == Activity.RESULT_OK && data != null) importTunnel(data.data)
return
}
IntentIntegrator.REQUEST_CODE -> {
val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
if (result != null && result.contents != null) {
importTunnel(result.contents)
}
return
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
@SuppressLint("ClickableViewAccessibility")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
binding = TunnelListFragmentBinding.inflate(inflater, container, false)
val bottomSheet = AddTunnelsSheet()
binding?.apply {
createFab.setOnClickListener {
val bottomSheet = AddTunnelsSheet()
bottomSheet.setTargetFragment(fragment, REQUEST_TARGET_FRAGMENT)
bottomSheet.show(parentFragmentManager, "BOTTOM_SHEET")
childFragmentManager.setFragmentResultListener(AddTunnelsSheet.REQUEST_KEY_NEW_TUNNEL, viewLifecycleOwner) { _, bundle ->
when (bundle.getString(AddTunnelsSheet.REQUEST_METHOD)) {
AddTunnelsSheet.REQUEST_CREATE -> {
startActivity(Intent(requireActivity(), TunnelCreatorActivity::class.java))
}
AddTunnelsSheet.REQUEST_IMPORT -> {
tunnelFileImportResultLauncher.launch("*/*")
}
AddTunnelsSheet.REQUEST_SCAN -> {
qrImportResultLauncher.launch(IntentIntegrator(requireActivity())
.setOrientationLocked(false)
.setBeepEnabled(false)
.setPrompt(getString(R.string.qr_code_hint))
.createScanIntent())
}
}
}
bottomSheet.show(childFragmentManager, "BOTTOM_SHEET")
}
executePendingBindings()
setUpRoot(root as ViewGroup)
setUpFAB(createFab)
setUpScrollingContent(tunnelList, createFab)
}
return binding!!.root
}
@@ -224,53 +116,34 @@ class TunnelListFragment : BaseFragment() {
override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?) {
binding ?: return
Application.getTunnelManager().tunnels.thenAccept { tunnels ->
if (newTunnel != null) viewForTunnel(newTunnel, tunnels).setSingleSelected(true)
if (oldTunnel != null) viewForTunnel(oldTunnel, tunnels).setSingleSelected(false)
lifecycleScope.launch {
val tunnels = Application.getTunnelManager().getTunnels()
if (newTunnel != null) viewForTunnel(newTunnel, tunnels)?.setSingleSelected(true)
if (oldTunnel != null) viewForTunnel(oldTunnel, tunnels)?.setSingleSelected(false)
}
}
private fun onTunnelDeletionFinished(count: Int, throwable: Throwable?) {
val message: String
val ctx = activity ?: Application.get()
if (throwable == null) {
message = resources.getQuantityString(R.plurals.delete_success, count, count)
message = ctx.resources.getQuantityString(R.plurals.delete_success, count, count)
} else {
val error = ErrorMessages[throwable]
message = resources.getQuantityString(R.plurals.delete_error, count, count, error)
message = ctx.resources.getQuantityString(R.plurals.delete_error, count, count, error)
Log.e(TAG, message, throwable)
}
showSnackbar(message)
}
private fun onTunnelImportFinished(tunnels: List<ObservableTunnel>, throwables: Collection<Throwable>) {
var message = ""
for (throwable in throwables) {
val error = ErrorMessages[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[0].name)
else if (tunnels.isEmpty() && throwables.size == 1)
else if (throwables.isEmpty())
message = resources.getQuantityString(R.plurals.import_total_success,
tunnels.size, tunnels.size)
else if (!throwables.isEmpty())
message = resources.getQuantityString(R.plurals.import_partial_success,
tunnels.size + throwables.size,
tunnels.size, tunnels.size + throwables.size)
showSnackbar(message)
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
binding ?: return
binding!!.fragment = this
Application.getTunnelManager().tunnels.thenAccept { tunnels -> binding!!.tunnels = tunnels }
val parent = this
lifecycleScope.launch { binding!!.tunnels = Application.getTunnelManager().getTunnels() }
binding!!.rowConfigurationHandler = object : RowConfigurationHandler<TunnelListItemBinding, ObservableTunnel> {
override fun onConfigureRow(binding: TunnelListItemBinding, item: ObservableTunnel, position: Int) {
binding.fragment = parent
binding.fragment = this@TunnelListFragment
binding.root.setOnClickListener {
if (actionMode == null) {
selectedTunnel = item
@@ -291,15 +164,17 @@ class TunnelListFragment : BaseFragment() {
}
private fun showSnackbar(message: CharSequence) {
binding?.let {
Snackbar.make(it.mainContainer, message, Snackbar.LENGTH_LONG)
.setAnchorView(it.createFab)
val binding = binding
if (binding != null)
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG)
.setAnchorView(binding.createFab)
.show()
}
else
Toast.makeText(activity ?: Application.get(), message, Toast.LENGTH_SHORT).show()
}
private fun viewForTunnel(tunnel: ObservableTunnel, tunnels: List<*>): MultiselectableRelativeLayout {
return binding!!.tunnelList.findViewHolderForAdapterPosition(tunnels.indexOf(tunnel))!!.itemView as MultiselectableRelativeLayout
private fun viewForTunnel(tunnel: ObservableTunnel, tunnels: List<*>): MultiselectableRelativeLayout? {
return binding?.tunnelList?.findViewHolderForAdapterPosition(tunnels.indexOf(tunnel))?.itemView as? MultiselectableRelativeLayout
}
private inner class ActionModeListener : ActionMode.Callback {
@@ -313,21 +188,31 @@ class TunnelListFragment : BaseFragment() {
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_action_delete -> {
val activity = activity ?: return true
val copyCheckedItems = HashSet(checkedItems)
Application.getTunnelManager().tunnels.thenAccept { tunnels ->
val tunnelsToDelete = ArrayList<ObservableTunnel>()
for (position in copyCheckedItems) tunnelsToDelete.add(tunnels[position])
val futures = tunnelsToDelete.map { it.delete().toCompletableFuture() }.toTypedArray()
CompletableFuture.allOf(*futures)
.thenApply { futures.size }
.whenComplete(this@TunnelListFragment::onTunnelDeletionFinished)
binding?.createFab?.apply {
visibility = View.VISIBLE
scaleX = 1f
scaleY = 1f
}
activity.lifecycleScope.launch {
try {
val tunnels = Application.getTunnelManager().getTunnels()
val tunnelsToDelete = ArrayList<ObservableTunnel>()
for (position in copyCheckedItems) tunnelsToDelete.add(tunnels[position])
val futures = tunnelsToDelete.map { async(SupervisorJob()) { it.deleteAsync() } }
onTunnelDeletionFinished(futures.awaitAll().size, null)
} catch (e: Throwable) {
onTunnelDeletionFinished(0, e)
}
}
checkedItems.clear()
mode.finish()
true
}
R.id.menu_action_select_all -> {
Application.getTunnelManager().tunnels.thenAccept { tunnels ->
lifecycleScope.launch {
val tunnels = Application.getTunnelManager().getTunnels()
for (i in 0 until tunnels.size) {
setItemChecked(i, true)
}
@@ -343,14 +228,16 @@ class TunnelListFragment : BaseFragment() {
if (activity != null) {
resources = activity!!.resources
}
animateFab(binding?.createFab, false)
mode.menuInflater.inflate(R.menu.tunnel_list_action_mode, menu)
binding!!.tunnelList.adapter!!.notifyDataSetChanged()
binding?.tunnelList?.adapter?.notifyDataSetChanged()
return true
}
override fun onDestroyActionMode(mode: ActionMode) {
actionMode = null
resources = null
animateFab(binding?.createFab, true)
checkedItems.clear()
binding!!.tunnelList.adapter!!.notifyDataSetChanged()
}
@@ -391,11 +278,29 @@ class TunnelListFragment : BaseFragment() {
mode.title = resources!!.getQuantityString(R.plurals.delete_title, count, count)
}
}
private fun animateFab(view: View?, show: Boolean) {
view ?: return
val animation = AnimationUtils.loadAnimation(
context, if (show) R.anim.scale_up else R.anim.scale_down
)
animation.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationRepeat(animation: Animation?) {
}
override fun onAnimationEnd(animation: Animation?) {
if (!show) view.visibility = View.GONE
}
override fun onAnimationStart(animation: Animation?) {
if (show) view.visibility = View.VISIBLE
}
})
view.startAnimation(animation)
}
}
companion object {
const val REQUEST_IMPORT = 1
private const val REQUEST_TARGET_FRAGMENT = 2
private const val CHECKED_ITEMS = "CHECKED_ITEMS"
private const val TAG = "WireGuard/TunnelListFragment"
}
@@ -10,13 +10,13 @@ import androidx.databinding.Bindable
import com.wireguard.android.BR
import com.wireguard.android.databinding.Keyed
class ApplicationData(val icon: Drawable, val name: String, val packageName: String, isExcludedFromTunnel: Boolean) : BaseObservable(), Keyed<String> {
class ApplicationData(val icon: Drawable, val name: String, val packageName: String, isSelected: Boolean) : BaseObservable(), Keyed<String> {
override val key = name
@get:Bindable
var isExcludedFromTunnel = isExcludedFromTunnel
var isSelected = isSelected
set(value) {
field = value
notifyPropertyChanged(BR.excludedFromTunnel)
notifyPropertyChanged(BR.selected)
}
}
@@ -4,16 +4,18 @@
*/
package com.wireguard.android.model
import android.util.Log
import androidx.databinding.BaseObservable
import androidx.databinding.Bindable
import com.wireguard.android.BR
import com.wireguard.android.backend.Statistics
import com.wireguard.android.backend.Tunnel
import com.wireguard.android.databinding.Keyed
import com.wireguard.android.util.ExceptionLoggers
import com.wireguard.android.util.applicationScope
import com.wireguard.config.Config
import java9.util.concurrent.CompletableFuture
import java9.util.concurrent.CompletionStage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* Encapsulates the volatile and nonvolatile state of a WireGuard tunnel.
@@ -24,15 +26,18 @@ class ObservableTunnel internal constructor(
config: Config?,
state: Tunnel.State
) : BaseObservable(), Keyed<String>, Tunnel {
override val key = name
override val key
get() = name
@Bindable
override fun getName() = name
fun setNameAsync(name: String): CompletionStage<String> = if (name != this.name)
manager.setTunnelName(this, name)
else
CompletableFuture.completedFuture(this.name)
suspend fun setNameAsync(name: String): String = withContext(Dispatchers.Main.immediate) {
if (name != this@ObservableTunnel.name)
manager.setTunnelName(this@ObservableTunnel, name)
else
this@ObservableTunnel.name
}
fun onNameChanged(name: String): String {
this.name = name
@@ -56,31 +61,42 @@ class ObservableTunnel internal constructor(
return state
}
fun setStateAsync(state: Tunnel.State): CompletionStage<Tunnel.State> = if (state != this.state)
manager.setTunnelState(this, state)
else
CompletableFuture.completedFuture(this.state)
suspend fun setStateAsync(state: Tunnel.State): Tunnel.State = withContext(Dispatchers.Main.immediate) {
if (state != this@ObservableTunnel.state)
manager.setTunnelState(this@ObservableTunnel, state)
else
this@ObservableTunnel.state
}
@get:Bindable
var config = config
get() {
if (field == null)
manager.getTunnelConfig(this).whenComplete(ExceptionLoggers.E)
// Opportunistically fetch this if we don't have a cached one, and rely on data bindings to update it eventually
applicationScope.launch {
try {
manager.getTunnelConfig(this@ObservableTunnel)
} catch (e: Throwable) {
Log.e(TAG, Log.getStackTraceString(e))
}
}
return field
}
private set
val configAsync: CompletionStage<Config>
get() = if (config == null)
manager.getTunnelConfig(this)
else
CompletableFuture.completedFuture(config)
suspend fun getConfigAsync(): Config = withContext(Dispatchers.Main.immediate) {
config ?: manager.getTunnelConfig(this@ObservableTunnel)
}
fun setConfigAsync(config: Config): CompletionStage<Config> = if (config != this.config)
manager.setTunnelConfig(this, config)
else
CompletableFuture.completedFuture(this.config)
suspend fun setConfigAsync(config: Config): Config = withContext(Dispatchers.Main.immediate) {
this@ObservableTunnel.config.let {
if (config != it)
manager.setTunnelConfig(this@ObservableTunnel, config)
else
it
}
}
fun onConfigChanged(config: Config?): Config? {
this.config = config
@@ -92,17 +108,27 @@ class ObservableTunnel internal constructor(
@get:Bindable
var statistics: Statistics? = null
get() {
if (field == null || field!!.isStale)
manager.getTunnelStatistics(this).whenComplete(ExceptionLoggers.E)
if (field == null || field?.isStale != false)
// Opportunistically fetch this if we don't have a cached one, and rely on data bindings to update it eventually
applicationScope.launch {
try {
manager.getTunnelStatistics(this@ObservableTunnel)
} catch (e: Throwable) {
Log.e(TAG, Log.getStackTraceString(e))
}
}
return field
}
private set
val statisticsAsync: CompletionStage<Statistics>
get() = if (statistics == null || statistics!!.isStale)
manager.getTunnelStatistics(this)
else
CompletableFuture.completedFuture(statistics)
suspend fun getStatisticsAsync(): Statistics = withContext(Dispatchers.Main.immediate) {
statistics.let {
if (it == null || it.isStale)
manager.getTunnelStatistics(this@ObservableTunnel)
else
it
}
}
fun onStatisticsChanged(statistics: Statistics?): Statistics? {
this.statistics = statistics
@@ -111,5 +137,10 @@ class ObservableTunnel internal constructor(
}
fun delete(): CompletionStage<Void> = manager.delete(this)
suspend fun deleteAsync() = manager.delete(this)
companion object {
private const val TAG = "WireGuard/ObservableTunnel"
}
}
@@ -4,17 +4,16 @@
*/
package com.wireguard.android.model
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.databinding.BaseObservable
import androidx.databinding.Bindable
import com.wireguard.android.Application.Companion.get
import com.wireguard.android.Application.Companion.getAsyncWorker
import com.wireguard.android.Application.Companion.getBackend
import com.wireguard.android.Application.Companion.getSharedPreferences
import com.wireguard.android.Application.Companion.getTunnelManager
import com.wireguard.android.BR
import com.wireguard.android.R
@@ -22,145 +21,148 @@ import com.wireguard.android.backend.Statistics
import com.wireguard.android.backend.Tunnel
import com.wireguard.android.configStore.ConfigStore
import com.wireguard.android.databinding.ObservableSortedKeyedArrayList
import com.wireguard.android.util.ExceptionLoggers
import com.wireguard.android.util.ErrorMessages
import com.wireguard.android.util.UserKnobs
import com.wireguard.android.util.applicationScope
import com.wireguard.config.Config
import java9.util.concurrent.CompletableFuture
import java9.util.concurrent.CompletionStage
import java.util.ArrayList
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* Maintains and mediates changes to the set of available WireGuard tunnels,
*/
class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
val tunnels = CompletableFuture<ObservableSortedKeyedArrayList<String, ObservableTunnel>>()
private val tunnels = CompletableDeferred<ObservableSortedKeyedArrayList<String, ObservableTunnel>>()
private val context: Context = get()
private val delayedLoadRestoreTunnels = ArrayList<CompletableFuture<Void>>()
private val tunnelMap: ObservableSortedKeyedArrayList<String, ObservableTunnel> = ObservableSortedKeyedArrayList(TunnelComparator)
private var haveLoaded = false
private fun addToList(name: String, config: Config?, state: Tunnel.State): ObservableTunnel? {
private fun addToList(name: String, config: Config?, state: Tunnel.State): ObservableTunnel {
val tunnel = ObservableTunnel(this, name, config, state)
tunnelMap.add(tunnel)
return tunnel
}
fun create(name: String, config: Config?): CompletionStage<ObservableTunnel> {
suspend fun getTunnels(): ObservableSortedKeyedArrayList<String, ObservableTunnel> = tunnels.await()
suspend fun create(name: String, config: Config?): ObservableTunnel = withContext(Dispatchers.Main.immediate) {
if (Tunnel.isNameInvalid(name))
return CompletableFuture.failedFuture(IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)))
throw IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name))
if (tunnelMap.containsKey(name))
return CompletableFuture.failedFuture(IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name)))
return getAsyncWorker().supplyAsync { configStore.create(name, config!!) }.thenApply { addToList(name, it, Tunnel.State.DOWN) }
throw IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name))
addToList(name, withContext(Dispatchers.IO) { configStore.create(name, config!!) }, Tunnel.State.DOWN)
}
fun delete(tunnel: ObservableTunnel): CompletionStage<Void> {
suspend fun delete(tunnel: ObservableTunnel) = withContext(Dispatchers.Main.immediate) {
val originalState = tunnel.state
val wasLastUsed = tunnel == lastUsedTunnel
// Make sure nothing touches the tunnel.
if (wasLastUsed)
lastUsedTunnel = null
tunnelMap.remove(tunnel)
return getAsyncWorker().runAsync {
try {
if (originalState == Tunnel.State.UP)
getBackend().setState(tunnel, Tunnel.State.DOWN, null)
withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.DOWN, null) }
try {
configStore.delete(tunnel.name)
} catch (e: Exception) {
withContext(Dispatchers.IO) { configStore.delete(tunnel.name) }
} catch (e: Throwable) {
if (originalState == Tunnel.State.UP)
getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config)
withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config) }
throw e
}
}.whenComplete { _, e ->
if (e == null)
return@whenComplete
} catch (e: Throwable) {
// Failure, put the tunnel back.
tunnelMap.add(tunnel)
if (wasLastUsed)
lastUsedTunnel = tunnel
throw e
}
}
@get:Bindable
@SuppressLint("ApplySharedPref")
var lastUsedTunnel: ObservableTunnel? = null
private set(value) {
if (value == field) return
field = value
notifyPropertyChanged(BR.lastUsedTunnel)
if (value != null)
getSharedPreferences().edit().putString(KEY_LAST_USED_TUNNEL, value.name).commit()
else
getSharedPreferences().edit().remove(KEY_LAST_USED_TUNNEL).commit()
applicationScope.launch { UserKnobs.setLastUsedTunnel(value?.name) }
}
fun getTunnelConfig(tunnel: ObservableTunnel): CompletionStage<Config> = getAsyncWorker()
.supplyAsync { configStore.load(tunnel.name) }.thenApply(tunnel::onConfigChanged)
suspend fun getTunnelConfig(tunnel: ObservableTunnel): Config = withContext(Dispatchers.Main.immediate) {
tunnel.onConfigChanged(withContext(Dispatchers.IO) { configStore.load(tunnel.name) })!!
}
fun onCreate() {
getAsyncWorker().supplyAsync { configStore.enumerate() }
.thenAcceptBoth(getAsyncWorker().supplyAsync { getBackend().runningTunnelNames }, this::onTunnelsLoaded)
.whenComplete(ExceptionLoggers.E)
applicationScope.launch {
try {
onTunnelsLoaded(withContext(Dispatchers.IO) { configStore.enumerate() }, withContext(Dispatchers.IO) { getBackend().runningTunnelNames })
} catch (e: Throwable) {
Log.e(TAG, Log.getStackTraceString(e))
}
}
}
private fun onTunnelsLoaded(present: Iterable<String>, running: Collection<String>) {
for (name in present)
addToList(name, null, if (running.contains(name)) Tunnel.State.UP else Tunnel.State.DOWN)
val lastUsedName = getSharedPreferences().getString(KEY_LAST_USED_TUNNEL, null)
if (lastUsedName != null)
lastUsedTunnel = tunnelMap[lastUsedName]
var toComplete: Array<CompletableFuture<Void>>
synchronized(delayedLoadRestoreTunnels) {
applicationScope.launch {
val lastUsedName = UserKnobs.lastUsedTunnel.first()
if (lastUsedName != null)
lastUsedTunnel = tunnelMap[lastUsedName]
haveLoaded = true
toComplete = delayedLoadRestoreTunnels.toTypedArray()
delayedLoadRestoreTunnels.clear()
restoreState(true)
tunnels.complete(tunnelMap)
}
restoreState(true).whenComplete { v: Void?, t: Throwable? ->
for (f in toComplete) {
if (t == null)
f.complete(v)
else
f.completeExceptionally(t)
}
private fun refreshTunnelStates() {
applicationScope.launch {
try {
val running = withContext(Dispatchers.IO) { getBackend().runningTunnelNames }
for (tunnel in tunnelMap)
tunnel.onStateChanged(if (running.contains(tunnel.name)) Tunnel.State.UP else Tunnel.State.DOWN)
} catch (e: Throwable) {
Log.e(TAG, Log.getStackTraceString(e))
}
}
tunnels.complete(tunnelMap)
}
fun refreshTunnelStates() {
getAsyncWorker().supplyAsync { getBackend().runningTunnelNames }
.thenAccept { running: Set<String> -> for (tunnel in tunnelMap) tunnel.onStateChanged(if (running.contains(tunnel.name)) Tunnel.State.UP else Tunnel.State.DOWN) }
.whenComplete(ExceptionLoggers.E)
}
fun restoreState(force: Boolean): CompletionStage<Void> {
if (!force && !getSharedPreferences().getBoolean(KEY_RESTORE_ON_BOOT, false))
return CompletableFuture.completedFuture(null)
synchronized(delayedLoadRestoreTunnels) {
if (!haveLoaded) {
val f = CompletableFuture<Void>()
delayedLoadRestoreTunnels.add(f)
return f
suspend fun restoreState(force: Boolean) {
if (!haveLoaded || (!force && !UserKnobs.restoreOnBoot.first()))
return
val previouslyRunning = UserKnobs.runningTunnels.first()
if (previouslyRunning.isEmpty()) return
withContext(Dispatchers.IO) {
try {
tunnelMap.filter { previouslyRunning.contains(it.name) }.map { async(Dispatchers.IO + SupervisorJob()) { setTunnelState(it, Tunnel.State.UP) } }.awaitAll()
} catch (e: Throwable) {
Log.e(TAG, Log.getStackTraceString(e))
}
}
val previouslyRunning = getSharedPreferences().getStringSet(KEY_RUNNING_TUNNELS, null)
?: return CompletableFuture.completedFuture(null)
return CompletableFuture.allOf(*tunnelMap.filter { previouslyRunning.contains(it.name) }.map { setTunnelState(it, Tunnel.State.UP).toCompletableFuture() }.toTypedArray())
}
@SuppressLint("ApplySharedPref")
fun saveState() {
getSharedPreferences().edit().putStringSet(KEY_RUNNING_TUNNELS, tunnelMap.filter { it.state == Tunnel.State.UP }.map { it.name }.toSet()).commit()
suspend fun saveState() {
UserKnobs.setRunningTunnels(tunnelMap.filter { it.state == Tunnel.State.UP }.map { it.name }.toSet())
}
fun setTunnelConfig(tunnel: ObservableTunnel, config: Config): CompletionStage<Config> = getAsyncWorker().supplyAsync {
getBackend().setState(tunnel, tunnel.state, config)
configStore.save(tunnel.name, config)
}.thenApply { tunnel.onConfigChanged(it) }
suspend fun setTunnelConfig(tunnel: ObservableTunnel, config: Config): Config = withContext(Dispatchers.Main.immediate) {
tunnel.onConfigChanged(withContext(Dispatchers.IO) {
getBackend().setState(tunnel, tunnel.state, config)
configStore.save(tunnel.name, config)
})!!
}
fun setTunnelName(tunnel: ObservableTunnel, name: String): CompletionStage<String> {
suspend fun setTunnelName(tunnel: ObservableTunnel, name: String): String = withContext(Dispatchers.Main.immediate) {
if (Tunnel.isNameInvalid(name))
return CompletableFuture.failedFuture(IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)))
throw IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name))
if (tunnelMap.containsKey(name)) {
return CompletableFuture.failedFuture(IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name)))
throw IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name))
}
val originalState = tunnel.state
val wasLastUsed = tunnel == lastUsedTunnel
@@ -168,69 +170,85 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
if (wasLastUsed)
lastUsedTunnel = null
tunnelMap.remove(tunnel)
return getAsyncWorker().supplyAsync {
var throwable: Throwable? = null
var newName: String? = null
try {
if (originalState == Tunnel.State.UP)
getBackend().setState(tunnel, Tunnel.State.DOWN, null)
configStore.rename(tunnel.name, name)
val newName = tunnel.onNameChanged(name)
withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.DOWN, null) }
withContext(Dispatchers.IO) { configStore.rename(tunnel.name, name) }
newName = tunnel.onNameChanged(name)
if (originalState == Tunnel.State.UP)
getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config)
newName
}.whenComplete { _, e ->
withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config) }
} catch (e: Throwable) {
throwable = 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.
tunnelMap.add(tunnel)
if (wasLastUsed)
lastUsedTunnel = tunnel
getTunnelState(tunnel)
}
// Add the tunnel back to the manager, under whatever name it thinks it has.
tunnelMap.add(tunnel)
if (wasLastUsed)
lastUsedTunnel = tunnel
if (throwable != null)
throw throwable
newName!!
}
fun setTunnelState(tunnel: ObservableTunnel, state: Tunnel.State): CompletionStage<Tunnel.State> = tunnel.configAsync
.thenCompose { getAsyncWorker().supplyAsync { getBackend().setState(tunnel, state, it) } }
.whenComplete { newState, e ->
// Ensure onStateChanged is always called (failure or not), and with the correct state.
tunnel.onStateChanged(if (e == null) newState else tunnel.state)
if (e == null && newState == Tunnel.State.UP)
lastUsedTunnel = tunnel
saveState()
}
suspend fun setTunnelState(tunnel: ObservableTunnel, state: Tunnel.State): Tunnel.State = withContext(Dispatchers.Main.immediate) {
var newState = tunnel.state
var throwable: Throwable? = null
try {
newState = withContext(Dispatchers.IO) { getBackend().setState(tunnel, state, tunnel.getConfigAsync()) }
if (newState == Tunnel.State.UP)
lastUsedTunnel = tunnel
} catch (e: Throwable) {
throwable = e
}
tunnel.onStateChanged(newState)
saveState()
if (throwable != null)
throw throwable
newState
}
class IntentReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
val manager = getTunnelManager()
if (intent == null) return
val action = intent.action ?: return
if ("com.wireguard.android.action.REFRESH_TUNNEL_STATES" == action) {
manager.refreshTunnelStates()
return
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || !getSharedPreferences().getBoolean("allow_remote_control_intents", false))
return
val state: Tunnel.State
state = when (action) {
"com.wireguard.android.action.SET_TUNNEL_UP" -> Tunnel.State.UP
"com.wireguard.android.action.SET_TUNNEL_DOWN" -> Tunnel.State.DOWN
else -> return
}
val tunnelName = intent.getStringExtra("tunnel") ?: return
manager.tunnels.thenAccept {
val tunnel = it[tunnelName] ?: return@thenAccept
manager.setTunnelState(tunnel, state)
applicationScope.launch {
val manager = getTunnelManager()
if (intent == null) return@launch
val action = intent.action ?: return@launch
if ("com.wireguard.android.action.REFRESH_TUNNEL_STATES" == action) {
manager.refreshTunnelStates()
return@launch
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || !UserKnobs.allowRemoteControlIntents.first())
return@launch
val state: Tunnel.State
state = when (action) {
"com.wireguard.android.action.SET_TUNNEL_UP" -> Tunnel.State.UP
"com.wireguard.android.action.SET_TUNNEL_DOWN" -> Tunnel.State.DOWN
else -> return@launch
}
val tunnelName = intent.getStringExtra("tunnel") ?: return@launch
val tunnels = manager.getTunnels()
val tunnel = tunnels[tunnelName] ?: return@launch
try {
manager.setTunnelState(tunnel, state)
} catch (e: Throwable) {
Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_LONG).show()
}
}
}
}
fun getTunnelState(tunnel: ObservableTunnel): CompletionStage<Tunnel.State> = getAsyncWorker()
.supplyAsync { getBackend().getState(tunnel) }.thenApply(tunnel::onStateChanged)
suspend fun getTunnelState(tunnel: ObservableTunnel): Tunnel.State = withContext(Dispatchers.Main.immediate) {
tunnel.onStateChanged(withContext(Dispatchers.IO) { getBackend().getState(tunnel) })
}
fun getTunnelStatistics(tunnel: ObservableTunnel): CompletionStage<Statistics> = getAsyncWorker()
.supplyAsync { getBackend().getStatistics(tunnel) }.thenApply(tunnel::onStatisticsChanged)
suspend fun getTunnelStatistics(tunnel: ObservableTunnel): Statistics = withContext(Dispatchers.Main.immediate) {
tunnel.onStatisticsChanged(withContext(Dispatchers.IO) { getBackend().getStatistics(tunnel) })!!
}
companion object {
private const val KEY_LAST_USED_TUNNEL = "last_used_tunnel"
private const val KEY_RESTORE_ON_BOOT = "restore_on_boot"
private const val KEY_RUNNING_TUNNELS = "enabled_configs"
private const val TAG = "WireGuard/TunnelManager"
}
}
@@ -4,46 +4,65 @@
*/
package com.wireguard.android.preference
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.util.AttributeSet
import android.util.Log
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import com.wireguard.android.Application
import com.wireguard.android.R
import com.wireguard.android.activity.SettingsActivity
import com.wireguard.android.backend.Tunnel
import com.wireguard.android.backend.WgQuickBackend
import java9.util.concurrent.CompletableFuture
import com.wireguard.android.util.UserKnobs
import com.wireguard.android.util.activity
import com.wireguard.android.util.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.system.exitProcess
class KernelModuleDisablerPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
private var state = if (Application.getBackend() is WgQuickBackend) State.ENABLED else State.DISABLED
private var state = State.UNKNOWN
override fun getSummary() = context.getString(state.summaryResourceId)
override fun getTitle() = context.getString(state.titleResourceId)
@SuppressLint("ApplySharedPref")
override fun onClick() {
if (state == State.DISABLED) {
setState(State.ENABLING)
Application.getSharedPreferences().edit().putBoolean("disable_kernel_module", false).commit()
} else if (state == State.ENABLED) {
setState(State.DISABLING)
Application.getSharedPreferences().edit().putBoolean("disable_kernel_module", true).commit()
init {
isVisible = false
lifecycleScope.launch {
setState(if (Application.getBackend() is WgQuickBackend) State.ENABLED else State.DISABLED)
}
Application.getAsyncWorker().runAsync {
Application.getTunnelManager().tunnels.thenApply { observableTunnels ->
val downings = observableTunnels.map { it.setStateAsync(Tunnel.State.DOWN).toCompletableFuture() }.toTypedArray()
CompletableFuture.allOf(*downings).thenRun {
}
override fun getSummary() = if (state == State.UNKNOWN) "" else context.getString(state.summaryResourceId)
override fun getTitle() = if (state == State.UNKNOWN) "" else context.getString(state.titleResourceId)
override fun onClick() {
activity.lifecycleScope.launch {
if (state == State.DISABLED) {
setState(State.ENABLING)
UserKnobs.setDisableKernelModule(false)
} else if (state == State.ENABLED) {
setState(State.DISABLING)
UserKnobs.setDisableKernelModule(true)
}
val observableTunnels = Application.getTunnelManager().getTunnels()
val downings = observableTunnels.map { async(SupervisorJob()) { it.setStateAsync(Tunnel.State.DOWN) } }
try {
downings.awaitAll()
withContext(Dispatchers.IO) {
val restartIntent = Intent(context, SettingsActivity::class.java)
restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
Application.get().startActivity(restartIntent)
exitProcess(0)
}
}.join()
} catch (e: Throwable) {
Log.e(TAG, Log.getStackTraceString(e))
}
}
}
@@ -51,13 +70,19 @@ class KernelModuleDisablerPreference(context: Context, attrs: AttributeSet?) : P
if (this.state == state) return
this.state = state
if (isEnabled != state.shouldEnableView) isEnabled = state.shouldEnableView
if (isVisible != state.visible) isVisible = state.visible
notifyChanged()
}
private enum class State(val titleResourceId: Int, val summaryResourceId: Int, val shouldEnableView: Boolean) {
ENABLED(R.string.module_disabler_enabled_title, R.string.module_disabler_enabled_summary, true),
DISABLED(R.string.module_disabler_disabled_title, R.string.module_disabler_disabled_summary, true),
ENABLING(R.string.module_disabler_disabled_title, R.string.success_application_will_restart, false),
DISABLING(R.string.module_disabler_enabled_title, R.string.success_application_will_restart, false);
private enum class State(val titleResourceId: Int, val summaryResourceId: Int, val shouldEnableView: Boolean, val visible: Boolean) {
UNKNOWN(0, 0, false, false),
ENABLED(R.string.module_disabler_enabled_title, R.string.module_disabler_enabled_summary, true, true),
DISABLED(R.string.module_disabler_disabled_title, R.string.module_disabler_disabled_summary, true, true),
ENABLING(R.string.module_disabler_disabled_title, R.string.success_application_will_restart, false, true),
DISABLING(R.string.module_disabler_enabled_title, R.string.success_application_will_restart, false, true);
}
companion object {
private const val TAG = "WireGuard/KernelModuleDisablerPreference"
}
}
@@ -4,7 +4,6 @@
*/
package com.wireguard.android.preference
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.system.OsConstants
@@ -15,40 +14,42 @@ import com.wireguard.android.Application
import com.wireguard.android.R
import com.wireguard.android.activity.SettingsActivity
import com.wireguard.android.util.ErrorMessages
import com.wireguard.android.util.UserKnobs
import com.wireguard.android.util.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.system.exitProcess
class ModuleDownloaderPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
private var state = State.INITIAL
override fun getSummary() = context.getString(state.messageResourceId)
override fun getTitle() = context.getString(R.string.module_installer_title)
override fun onClick() {
setState(State.WORKING)
Application.getAsyncWorker().supplyAsync(Application.getModuleLoader()::download).whenComplete(this::onDownloadResult)
}
@SuppressLint("ApplySharedPref")
private fun onDownloadResult(result: Int, throwable: Throwable?) {
when {
throwable != null -> {
setState(State.FAILURE)
Toast.makeText(context, ErrorMessages[throwable], Toast.LENGTH_LONG).show()
}
result == OsConstants.ENOENT -> setState(State.NOTFOUND)
result == OsConstants.EXIT_SUCCESS -> {
setState(State.SUCCESS)
Application.getSharedPreferences().edit().remove("disable_kernel_module").commit()
Application.getAsyncWorker().runAsync {
val restartIntent = Intent(context, SettingsActivity::class.java)
restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
Application.get().startActivity(restartIntent)
exitProcess(0)
lifecycleScope.launch {
try {
when (withContext(Dispatchers.IO) { Application.getModuleLoader().download() }) {
OsConstants.ENOENT -> setState(State.NOTFOUND)
OsConstants.EXIT_SUCCESS -> {
setState(State.SUCCESS)
UserKnobs.setDisableKernelModule(null)
withContext(Dispatchers.IO) {
val restartIntent = Intent(context, SettingsActivity::class.java)
restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
Application.get().startActivity(restartIntent)
exitProcess(0)
}
}
else -> setState(State.FAILURE)
}
} catch (e: Throwable) {
setState(State.FAILURE)
Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_LONG).show()
}
else -> setState(State.FAILURE)
}
}
@@ -0,0 +1,132 @@
/*
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference
import androidx.datastore.DataStore
import androidx.datastore.preferences.Preferences
import androidx.datastore.preferences.edit
import androidx.datastore.preferences.preferencesKey
import androidx.datastore.preferences.preferencesSetKey
import androidx.datastore.preferences.remove
import androidx.preference.PreferenceDataStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
class PreferencesPreferenceDataStore(private val coroutineScope: CoroutineScope, private val dataStore: DataStore<Preferences>) : PreferenceDataStore() {
override fun putString(key: String?, value: String?) {
if (key == null) return
val pk = preferencesKey<String>(key)
coroutineScope.launch {
dataStore.edit {
if (value == null) it.remove(pk)
else it[pk] = value
}
}
}
override fun putStringSet(key: String?, values: Set<String?>?) {
if (key == null) return
val pk = preferencesSetKey<String>(key)
val filteredValues = values?.filterNotNull()?.toSet()
coroutineScope.launch {
dataStore.edit {
if (filteredValues == null || filteredValues.isEmpty()) it.remove(pk)
else it[pk] = filteredValues
}
}
}
override fun putInt(key: String?, value: Int) {
if (key == null) return
val pk = preferencesKey<Int>(key)
coroutineScope.launch {
dataStore.edit {
it[pk] = value
}
}
}
override fun putLong(key: String?, value: Long) {
if (key == null) return
val pk = preferencesKey<Long>(key)
coroutineScope.launch {
dataStore.edit {
it[pk] = value
}
}
}
override fun putFloat(key: String?, value: Float) {
if (key == null) return
val pk = preferencesKey<Float>(key)
coroutineScope.launch {
dataStore.edit {
it[pk] = value
}
}
}
override fun putBoolean(key: String?, value: Boolean) {
if (key == null) return
val pk = preferencesKey<Boolean>(key)
coroutineScope.launch {
dataStore.edit {
it[pk] = value
}
}
}
override fun getString(key: String?, defValue: String?): String? {
if (key == null) return defValue
val pk = preferencesKey<String>(key)
return runBlocking {
dataStore.data.map { it[pk] ?: defValue }.first()
}
}
override fun getStringSet(key: String?, defValues: Set<String?>?): Set<String?>? {
if (key == null) return defValues
val pk = preferencesSetKey<String>(key)
return runBlocking {
dataStore.data.map { it[pk] ?: defValues }.first()
}
}
override fun getInt(key: String?, defValue: Int): Int {
if (key == null) return defValue
val pk = preferencesKey<Int>(key)
return runBlocking {
dataStore.data.map { it[pk] ?: defValue }.first()
}
}
override fun getLong(key: String?, defValue: Long): Long {
if (key == null) return defValue
val pk = preferencesKey<Long>(key)
return runBlocking {
dataStore.data.map { it[pk] ?: defValue }.first()
}
}
override fun getFloat(key: String?, defValue: Float): Float {
if (key == null) return defValue
val pk = preferencesKey<Float>(key)
return runBlocking {
dataStore.data.map { it[pk] ?: defValue }.first()
}
}
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
if (key == null) return defValue
val pk = preferencesKey<Boolean>(key)
return runBlocking {
dataStore.data.map { it[pk] ?: defValue }.first()
}
}
}
@@ -10,6 +10,10 @@ import androidx.preference.Preference
import com.wireguard.android.Application
import com.wireguard.android.R
import com.wireguard.android.util.ToolsInstaller
import com.wireguard.android.util.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* Preference implementing a button that asynchronously runs `ToolsInstaller` and displays the
@@ -17,37 +21,41 @@ import com.wireguard.android.util.ToolsInstaller
*/
class ToolsInstallerPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
private var state = State.INITIAL
override fun getSummary() = context.getString(state.messageResourceId)
override fun getTitle() = context.getString(R.string.tools_installer_title)
override fun onAttached() {
super.onAttached()
Application.getAsyncWorker().supplyAsync(Application.getToolsInstaller()::areInstalled).whenComplete(this::onCheckResult)
}
private fun onCheckResult(state: Int, throwable: Throwable?) {
when {
throwable != null || state == ToolsInstaller.ERROR -> setState(State.INITIAL)
state and ToolsInstaller.YES == ToolsInstaller.YES -> setState(State.ALREADY)
state and (ToolsInstaller.MAGISK or ToolsInstaller.NO) == ToolsInstaller.MAGISK or ToolsInstaller.NO -> setState(State.INITIAL_MAGISK)
state and (ToolsInstaller.SYSTEM or ToolsInstaller.NO) == ToolsInstaller.SYSTEM or ToolsInstaller.NO -> setState(State.INITIAL_SYSTEM)
else -> setState(State.INITIAL)
lifecycleScope.launch {
try {
val state = withContext(Dispatchers.IO) { Application.getToolsInstaller().areInstalled() }
when {
state == ToolsInstaller.ERROR -> setState(State.INITIAL)
state and ToolsInstaller.YES == ToolsInstaller.YES -> setState(State.ALREADY)
state and (ToolsInstaller.MAGISK or ToolsInstaller.NO) == ToolsInstaller.MAGISK or ToolsInstaller.NO -> setState(State.INITIAL_MAGISK)
state and (ToolsInstaller.SYSTEM or ToolsInstaller.NO) == ToolsInstaller.SYSTEM or ToolsInstaller.NO -> setState(State.INITIAL_SYSTEM)
else -> setState(State.INITIAL)
}
} catch (_: Throwable) {
setState(State.INITIAL)
}
}
}
override fun onClick() {
setState(State.WORKING)
Application.getAsyncWorker().supplyAsync { Application.getToolsInstaller().install() }.whenComplete { result: Int, throwable: Throwable? -> onInstallResult(result, throwable) }
}
private fun onInstallResult(result: Int, throwable: Throwable?) {
when {
throwable != null -> setState(State.FAILURE)
result and (ToolsInstaller.YES or ToolsInstaller.MAGISK) == ToolsInstaller.YES or ToolsInstaller.MAGISK -> setState(State.SUCCESS_MAGISK)
result and (ToolsInstaller.YES or ToolsInstaller.SYSTEM) == ToolsInstaller.YES or ToolsInstaller.SYSTEM -> setState(State.SUCCESS_SYSTEM)
else -> setState(State.FAILURE)
lifecycleScope.launch {
try {
val result = withContext(Dispatchers.IO) { Application.getToolsInstaller().install() }
when {
result and (ToolsInstaller.YES or ToolsInstaller.MAGISK) == ToolsInstaller.YES or ToolsInstaller.MAGISK -> setState(State.SUCCESS_MAGISK)
result and (ToolsInstaller.YES or ToolsInstaller.SYSTEM) == ToolsInstaller.YES or ToolsInstaller.SYSTEM -> setState(State.SUCCESS_SYSTEM)
else -> setState(State.FAILURE)
}
} catch (_: Throwable) {
setState(State.FAILURE)
}
}
}
@@ -16,6 +16,10 @@ import com.wireguard.android.R
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.GoBackend
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.Locale
class VersionPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
@@ -43,15 +47,16 @@ class VersionPreference(context: Context, attrs: AttributeSet?) : Preference(con
}
init {
Application.getBackendAsync().thenAccept { backend ->
lifecycleScope.launch {
val backend = Application.getBackend()
versionSummary = getContext().getString(R.string.version_summary_checking, getBackendPrettyName(context, backend).toLowerCase(Locale.ENGLISH))
Application.getAsyncWorker().supplyAsync(backend::getVersion).whenComplete { version, exception ->
versionSummary = if (exception == null)
getContext().getString(R.string.version_summary, getBackendPrettyName(context, backend), version)
else
getContext().getString(R.string.version_summary_unknown, getBackendPrettyName(context, backend).toLowerCase(Locale.ENGLISH))
notifyChanged()
notifyChanged()
versionSummary = try {
getContext().getString(R.string.version_summary, getBackendPrettyName(context, backend), withContext(Dispatchers.IO) { backend.version })
} catch (_: Throwable) {
getContext().getString(R.string.version_summary_unknown, getBackendPrettyName(context, backend).toLowerCase(Locale.ENGLISH))
}
notifyChanged()
}
}
}
@@ -4,21 +4,25 @@
*/
package com.wireguard.android.preference
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.util.AttributeSet
import android.util.Log
import androidx.preference.Preference
import com.google.android.material.snackbar.Snackbar
import com.wireguard.android.Application
import com.wireguard.android.R
import com.wireguard.android.model.ObservableTunnel
import com.wireguard.android.util.AdminKnobs
import com.wireguard.android.util.BiometricAuthenticator
import com.wireguard.android.util.DownloadsFileSaver
import com.wireguard.android.util.ErrorMessages
import com.wireguard.android.util.FragmentUtils
import java9.util.concurrent.CompletableFuture
import com.wireguard.android.util.activity
import com.wireguard.android.util.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.nio.charset.StandardCharsets
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
@@ -28,52 +32,48 @@ import java.util.zip.ZipOutputStream
*/
class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
private var exportedFilePath: String? = null
private val downloadsFileSaver = DownloadsFileSaver(activity)
private fun exportZip() {
Application.getTunnelManager().tunnels.thenAccept(this::exportZip)
}
private fun exportZip(tunnels: List<ObservableTunnel>) {
val futureConfigs = tunnels.map { it.configAsync.toCompletableFuture() }.toTypedArray()
if (futureConfigs.isEmpty()) {
exportZipComplete(null, IllegalArgumentException(
context.getString(R.string.no_tunnels_error)))
return
}
CompletableFuture.allOf(*futureConfigs)
.whenComplete { _, exception ->
Application.getAsyncWorker().supplyAsync {
if (exception != null) throw exception
val outputFile = DownloadsFileSaver.save(context, "wireguard-export.zip", "application/zip", true)
try {
ZipOutputStream(outputFile.outputStream).use { zip ->
for (i in futureConfigs.indices) {
zip.putNextEntry(ZipEntry(tunnels[i].name + ".conf"))
zip.write(futureConfigs[i].getNow(null)!!.toWgQuickString().toByteArray(StandardCharsets.UTF_8))
}
zip.closeEntry()
}
} catch (e: Exception) {
outputFile.delete()
throw e
lifecycleScope.launch {
val tunnels = Application.getTunnelManager().getTunnels()
try {
exportedFilePath = withContext(Dispatchers.IO) {
val configs = tunnels.map { async(SupervisorJob()) { it.getConfigAsync() } }.awaitAll()
if (configs.isEmpty()) {
throw IllegalArgumentException(context.getString(R.string.no_tunnels_error))
}
val outputFile = downloadsFileSaver.save("wireguard-export.zip", "application/zip", true)
if (outputFile == null) {
withContext(Dispatchers.Main.immediate) {
isEnabled = true
}
outputFile.fileName
}.whenComplete(this::exportZipComplete)
return@withContext null
}
try {
ZipOutputStream(outputFile.outputStream).use { zip ->
for (i in configs.indices) {
zip.putNextEntry(ZipEntry(tunnels[i].name + ".conf"))
zip.write(configs[i].toWgQuickString().toByteArray(StandardCharsets.UTF_8))
}
zip.closeEntry()
}
} catch (e: Throwable) {
outputFile.delete()
throw e
}
outputFile.fileName
}
}
private fun exportZipComplete(filePath: String?, throwable: Throwable?) {
if (throwable != null) {
val error = ErrorMessages[throwable]
val message = context.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()
isEnabled = true
} else {
exportedFilePath = filePath
notifyChanged()
notifyChanged()
} catch (e: Throwable) {
val error = ErrorMessages[e]
val message = context.getString(R.string.zip_export_error, error)
Log.e(TAG, message, e)
Snackbar.make(
activity.findViewById(android.R.id.content),
message, Snackbar.LENGTH_LONG).show()
isEnabled = true
}
}
}
@@ -82,22 +82,18 @@ class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference
override fun getTitle() = context.getString(R.string.zip_export_title)
override fun onClick() {
val prefActivity = FragmentUtils.getPrefActivity(this)
val fragment = prefActivity.supportFragmentManager.fragments.first()
if (AdminKnobs.disableConfigExport) return
val fragment = activity.supportFragmentManager.fragments.first()
BiometricAuthenticator.authenticate(R.string.biometric_prompt_zip_exporter_title, fragment) {
when (it) {
// When we have successful authentication, or when there is no biometric hardware available.
is BiometricAuthenticator.Result.Success, is BiometricAuthenticator.Result.HardwareUnavailableOrDisabled -> {
prefActivity.ensurePermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, grantResults ->
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
isEnabled = false
exportZip()
}
}
isEnabled = false
exportZip()
}
is BiometricAuthenticator.Result.Failure -> {
Snackbar.make(
prefActivity.findViewById(android.R.id.content),
activity.findViewById(android.R.id.content),
it.message,
Snackbar.LENGTH_SHORT
).show()
@@ -0,0 +1,17 @@
/*
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util
import android.content.RestrictionsManager
import androidx.core.content.getSystemService
import com.wireguard.android.Application
object AdminKnobs {
private val restrictions: RestrictionsManager? = Application.get().getSystemService()
val disableConfigExport: Boolean
get() = restrictions?.applicationRestrictions?.getBoolean("disable_config_export", false)
?: false
}
@@ -1,43 +0,0 @@
/*
* Copyright © 2017-2020 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util
import android.os.Handler
import java9.util.concurrent.CompletableFuture
import java9.util.concurrent.CompletionStage
import java.util.concurrent.Executor
/**
* Helper class for running asynchronous tasks and ensuring they are completed on the main thread.
*/
class AsyncWorker(private val executor: Executor, private val handler: Handler) {
fun runAsync(run: () -> Unit): CompletionStage<Void> {
val future = CompletableFuture<Void>()
executor.execute {
try {
run()
handler.post { future.complete(null) }
} catch (t: Throwable) {
handler.post { future.completeExceptionally(t) }
}
}
return future
}
fun <T> supplyAsync(get: () -> T?): CompletionStage<T> {
val future = CompletableFuture<T>()
executor.execute {
try {
val result = get()
handler.post { future.complete(result) }
} catch (t: Throwable) {
handler.post { future.completeExceptionally(t) }
}
}
return future
}
}
@@ -5,24 +5,23 @@
package com.wireguard.android.util
import android.annotation.SuppressLint
import android.app.KeyguardManager
import android.content.Context
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.annotation.StringRes
import androidx.biometric.BiometricConstants
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators
import androidx.biometric.BiometricPrompt
import androidx.core.content.getSystemService
import androidx.fragment.app.Fragment
import com.wireguard.android.R
object BiometricAuthenticator {
private const val TAG = "WireGuard/BiometricAuthenticator"
private val handler = Handler()
// Not all devices support strong biometric auth so we're allowing both device credentials as
// well as weak biometrics.
private const val allowedAuthenticators = Authenticators.DEVICE_CREDENTIAL or Authenticators.BIOMETRIC_WEAK
sealed class Result {
data class Success(val cryptoObject: BiometricPrompt.CryptoObject?) : Result()
@@ -31,20 +30,6 @@ object BiometricAuthenticator {
object Cancelled : Result()
}
@SuppressLint("PrivateApi")
private fun isPinEnabled(context: Context): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
return context.getSystemService<KeyguardManager>()!!.isDeviceSecure
return try {
val lockUtilsClass = Class.forName("com.android.internal.widget.LockPatternUtils")
val lockUtils = lockUtilsClass.getConstructor(Context::class.java).newInstance(context)
val method = lockUtilsClass.getMethod("isLockScreenDisabled")
!(method.invoke(lockUtils) as Boolean)
} catch (e: Exception) {
false
}
}
fun authenticate(
@StringRes dialogTitleRes: Int,
fragment: Fragment,
@@ -55,12 +40,12 @@ object BiometricAuthenticator {
super.onAuthenticationError(errorCode, errString)
Log.d(TAG, "BiometricAuthentication error: errorCode=$errorCode, msg=$errString")
callback(when (errorCode) {
BiometricConstants.ERROR_CANCELED, BiometricConstants.ERROR_USER_CANCELED,
BiometricConstants.ERROR_NEGATIVE_BUTTON -> {
BiometricPrompt.ERROR_CANCELED, BiometricPrompt.ERROR_USER_CANCELED,
BiometricPrompt.ERROR_NEGATIVE_BUTTON -> {
Result.Cancelled
}
BiometricConstants.ERROR_HW_NOT_PRESENT, BiometricConstants.ERROR_HW_UNAVAILABLE,
BiometricConstants.ERROR_NO_BIOMETRICS, BiometricConstants.ERROR_NO_DEVICE_CREDENTIAL -> {
BiometricPrompt.ERROR_HW_NOT_PRESENT, BiometricPrompt.ERROR_HW_UNAVAILABLE,
BiometricPrompt.ERROR_NO_BIOMETRICS, BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> {
Result.HardwareUnavailableOrDisabled
}
else -> Result.Failure(errorCode, fragment.getString(R.string.biometric_auth_error_reason, errString))
@@ -77,12 +62,12 @@ object BiometricAuthenticator {
callback(Result.Success(result.cryptoObject))
}
}
val biometricPrompt = BiometricPrompt(fragment, { handler.post(it) }, authCallback)
val biometricPrompt = BiometricPrompt(fragment, { Handler(Looper.getMainLooper()).post(it) }, authCallback)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(fragment.getString(dialogTitleRes))
.setDeviceCredentialAllowed(true)
.setAllowedAuthenticators(allowedAuthenticators)
.build()
if (BiometricManager.from(fragment.requireContext()).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS || isPinEnabled(fragment.requireContext())) {
if (BiometricManager.from(fragment.requireContext()).canAuthenticate(allowedAuthenticators) == BiometricManager.BIOMETRIC_SUCCESS) {
biometricPrompt.authenticate(promptInfo)
} else {
callback(Result.HardwareUnavailableOrDisabled)
@@ -4,65 +4,97 @@
*/
package com.wireguard.android.util
import android.Manifest
import android.content.ContentValues
import android.content.Context
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.provider.MediaStore.MediaColumns
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import com.wireguard.android.R
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
object DownloadsFileSaver {
@Throws(Exception::class)
fun save(context: Context, name: String, mimeType: String?, overwriteExisting: Boolean) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentResolver = context.contentResolver
if (overwriteExisting)
contentResolver.delete(MediaStore.Downloads.EXTERNAL_CONTENT_URI, String.format("%s = ?", MediaColumns.DISPLAY_NAME), arrayOf(name))
val contentValues = ContentValues()
contentValues.put(MediaColumns.DISPLAY_NAME, name)
contentValues.put(MediaColumns.MIME_TYPE, mimeType)
val contentUri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
?: throw IOException(context.getString(R.string.create_downloads_file_error))
val contentStream = contentResolver.openOutputStream(contentUri)
?: throw IOException(context.getString(R.string.create_downloads_file_error))
@Suppress("DEPRECATION") var cursor = contentResolver.query(contentUri, arrayOf(MediaColumns.DATA), null, null, null)
var path: String? = null
if (cursor != null) {
try {
if (cursor.moveToFirst())
path = cursor.getString(0)
} finally {
cursor.close()
}
class DownloadsFileSaver(private val context: ComponentActivity) {
private lateinit var activityResult: ActivityResultLauncher<String>
private lateinit var futureGrant: CompletableDeferred<Boolean>
init {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
futureGrant = CompletableDeferred()
activityResult = context.registerForActivityResult(ActivityResultContracts.RequestPermission()) { ret -> futureGrant.complete(ret) }
}
if (path == null) {
path = "Download/"
cursor = contentResolver.query(contentUri, arrayOf(MediaColumns.DISPLAY_NAME), null, null, null)
}
suspend fun save(name: String, mimeType: String?, overwriteExisting: Boolean) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
withContext(Dispatchers.IO) {
val contentResolver = context.contentResolver
if (overwriteExisting)
contentResolver.delete(MediaStore.Downloads.EXTERNAL_CONTENT_URI, String.format("%s = ?", MediaColumns.DISPLAY_NAME), arrayOf(name))
val contentValues = ContentValues()
contentValues.put(MediaColumns.DISPLAY_NAME, name)
contentValues.put(MediaColumns.MIME_TYPE, mimeType)
val contentUri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
?: throw IOException(context.getString(R.string.create_downloads_file_error))
val contentStream = contentResolver.openOutputStream(contentUri)
?: throw IOException(context.getString(R.string.create_downloads_file_error))
@Suppress("DEPRECATION") var cursor = contentResolver.query(contentUri, arrayOf(MediaColumns.DATA), null, null, null)
var path: String? = null
if (cursor != null) {
try {
if (cursor.moveToFirst())
path += cursor.getString(0)
path = cursor.getString(0)
} finally {
cursor.close()
}
}
if (path == null) {
path = "Download/"
cursor = contentResolver.query(contentUri, arrayOf(MediaColumns.DISPLAY_NAME), null, null, null)
if (cursor != null) {
try {
if (cursor.moveToFirst())
path += cursor.getString(0)
} finally {
cursor.close()
}
}
}
DownloadsFile(context, contentStream, path, contentUri)
}
DownloadsFile(context, contentStream, path, contentUri)
} else {
@Suppress("DEPRECATION") val path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(path, name)
if (!path.isDirectory && !path.mkdirs())
throw IOException(context.getString(R.string.create_output_dir_error))
DownloadsFile(context, FileOutputStream(file), file.absolutePath, null)
withContext(Dispatchers.Main.immediate) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
activityResult.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
val granted = futureGrant.await()
if (!granted) {
futureGrant = CompletableDeferred()
return@withContext null
}
}
@Suppress("DEPRECATION") val path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
withContext(Dispatchers.IO) {
val file = File(path, name)
if (!path.isDirectory && !path.mkdirs())
throw IOException(context.getString(R.string.create_output_dir_error))
DownloadsFile(context, FileOutputStream(file), file.absolutePath, null)
}
}
}
class DownloadsFile(private val context: Context, val outputStream: OutputStream, val fileName: String, private val uri: Uri?) {
fun delete() {
suspend fun delete() = withContext(Dispatchers.IO) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
context.contentResolver.delete(uri!!, null, null)
else
@@ -6,7 +6,7 @@ package com.wireguard.android.util
import android.content.res.Resources
import android.os.RemoteException
import com.wireguard.android.Application.Companion.get
import com.wireguard.android.Application
import com.wireguard.android.R
import com.wireguard.android.backend.BackendException
import com.wireguard.android.util.RootShell.RootShellException
@@ -63,7 +63,7 @@ object ErrorMessages {
)
operator fun get(throwable: Throwable?): String {
val resources = get().resources
val resources = Application.get().resources
if (throwable == null) return resources.getString(R.string.unknown_error)
val rootCause = rootCause(throwable)
return when {
@@ -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.util.Log
import java9.util.function.BiConsumer
/**
* Helpers for logging exceptions from asynchronous tasks. These can be passed to
* `CompletionStage.whenComplete()` at the end of an asynchronous future chain.
*/
enum class ExceptionLoggers(private val priority: Int) : BiConsumer<Any?, Throwable?> {
D(Log.DEBUG), E(Log.ERROR);
override fun accept(result: Any?, 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")
}
companion object {
private const val TAG = "WireGuard/ExceptionLoggers"
}
}
@@ -8,9 +8,24 @@ package com.wireguard.android.util
import android.content.Context
import android.util.TypedValue
import androidx.annotation.AttrRes
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import com.wireguard.android.Application
import com.wireguard.android.activity.SettingsActivity
import kotlinx.coroutines.CoroutineScope
fun Context.resolveAttribute(@AttrRes attrRes: Int): Int {
val typedValue = TypedValue()
theme.resolveAttribute(attrRes, typedValue, true)
return typedValue.data
}
val Any.applicationScope: CoroutineScope
get() = Application.getCoroutineScope()
val Preference.activity: SettingsActivity
get() = context as? SettingsActivity
?: throw IllegalStateException("Failed to resolve SettingsActivity")
val Preference.lifecycleScope: CoroutineScope
get() = activity.lifecycleScope
@@ -1,21 +0,0 @@
/*
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util
import android.view.ContextThemeWrapper
import androidx.preference.Preference
import com.wireguard.android.activity.SettingsActivity
object FragmentUtils {
fun getPrefActivity(preference: Preference): SettingsActivity {
val context = preference.context
if (context is ContextThemeWrapper) {
if (context is SettingsActivity) {
return context
}
}
throw IllegalStateException("Failed to resolve SettingsActivity")
}
}
@@ -0,0 +1,22 @@
/*
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util
import com.wireguard.android.Application
import com.wireguard.android.R
object QuantityFormatter {
fun formatBytes(bytes: Long): String {
val context = Application.get().applicationContext
return when {
bytes < 1024 -> context.getString(R.string.transfer_bytes, bytes)
bytes < 1024 * 1024 -> context.getString(R.string.transfer_kibibytes, bytes / 1024.0)
bytes < 1024 * 1024 * 1024 -> context.getString(R.string.transfer_mibibytes, bytes / (1024.0 * 1024.0))
bytes < 1024 * 1024 * 1024 * 1024L -> context.getString(R.string.transfer_gibibytes, bytes / (1024.0 * 1024.0 * 1024.0))
else -> context.getString(R.string.transfer_tibibytes, bytes / (1024.0 * 1024.0 * 1024.0) / 1024.0)
}
}
}
@@ -0,0 +1,150 @@
/*
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util
import android.content.ContentResolver
import android.net.Uri
import android.provider.OpenableColumns
import android.util.Log
import androidx.fragment.app.FragmentManager
import com.wireguard.android.Application
import com.wireguard.android.R
import com.wireguard.android.fragment.ConfigNamingDialogFragment
import com.wireguard.android.model.ObservableTunnel
import com.wireguard.config.Config
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.ByteArrayInputStream
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
import java.util.ArrayList
import java.util.Locale
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
object TunnelImporter {
suspend fun importTunnel(contentResolver: ContentResolver, uri: Uri, messageCallback: (CharSequence) -> Unit) = withContext(Dispatchers.IO) {
val context = Application.get().applicationContext
val futureTunnels = ArrayList<Deferred<ObservableTunnel>>()
val throwables = ArrayList<Throwable>()
try {
val columns = arrayOf(OpenableColumns.DISPLAY_NAME)
var name = ""
contentResolver.query(uri, columns, null, null, null)?.use { cursor ->
if (cursor.moveToFirst() && !cursor.isNull(0)) {
name = cursor.getString(0)
}
}
if (name.isEmpty()) {
name = Uri.decode(uri.lastPathSegment)
}
var idx = name.lastIndexOf('/')
if (idx >= 0) {
require(idx < name.length - 1) { context.getString(R.string.illegal_filename_error, name) }
name = name.substring(idx + 1)
}
val isZip = name.toLowerCase(Locale.ROOT).endsWith(".zip")
if (name.toLowerCase(Locale.ROOT).endsWith(".conf")) {
name = name.substring(0, name.length - ".conf".length)
} else {
require(isZip) { context.getString(R.string.bad_extension_error) }
}
if (isZip) {
ZipInputStream(contentResolver.openInputStream(uri)).use { zip ->
val reader = BufferedReader(InputStreamReader(zip, StandardCharsets.UTF_8))
var entry: ZipEntry?
while (true) {
entry = zip.nextEntry ?: break
name = entry.name
idx = name.lastIndexOf('/')
if (idx >= 0) {
if (idx >= name.length - 1) {
continue
}
name = name.substring(name.lastIndexOf('/') + 1)
}
if (name.toLowerCase(Locale.ROOT).endsWith(".conf")) {
name = name.substring(0, name.length - ".conf".length)
} else {
continue
}
try {
Config.parse(reader)
} catch (e: Throwable) {
throwables.add(e)
null
}?.let {
val nameCopy = name
futureTunnels.add(async(SupervisorJob()) { Application.getTunnelManager().create(nameCopy, it) })
}
}
}
} else {
futureTunnels.add(async(SupervisorJob()) { Application.getTunnelManager().create(name, Config.parse(contentResolver.openInputStream(uri)!!)) })
}
if (futureTunnels.isEmpty()) {
if (throwables.size == 1) {
throw throwables[0]
} else {
require(throwables.isNotEmpty()) { context.getString(R.string.no_configs_error) }
}
}
val tunnels = futureTunnels.mapNotNull {
try {
it.await()
} catch (e: Throwable) {
throwables.add(e)
null
}
}
withContext(Dispatchers.Main.immediate) { onTunnelImportFinished(tunnels, throwables, messageCallback) }
} catch (e: Throwable) {
withContext(Dispatchers.Main.immediate) { onTunnelImportFinished(emptyList(), listOf(e), messageCallback) }
}
}
fun importTunnel(parentFragmentManager: FragmentManager, configText: String, messageCallback: (CharSequence) -> Unit) {
try {
// Ensure the config text is parseable before proceeding…
Config.parse(ByteArrayInputStream(configText.toByteArray(StandardCharsets.UTF_8)))
// Config text is valid, now create the tunnel…
ConfigNamingDialogFragment.newInstance(configText).show(parentFragmentManager, null)
} catch (e: Throwable) {
onTunnelImportFinished(emptyList(), listOf<Throwable>(e), messageCallback)
}
}
private fun onTunnelImportFinished(tunnels: List<ObservableTunnel>, throwables: Collection<Throwable>, messageCallback: (CharSequence) -> Unit) {
val context = Application.get().applicationContext
var message = ""
for (throwable in throwables) {
val error = ErrorMessages[throwable]
message = context.getString(R.string.import_error, error)
Log.e(TAG, message, throwable)
}
if (tunnels.size == 1 && throwables.isEmpty())
message = context.getString(R.string.import_success, tunnels[0].name)
else if (tunnels.isEmpty() && throwables.size == 1)
else if (throwables.isEmpty())
message = context.resources.getQuantityString(R.plurals.import_total_success,
tunnels.size, tunnels.size)
else if (!throwables.isEmpty())
message = context.resources.getQuantityString(R.plurals.import_partial_success,
tunnels.size + throwables.size,
tunnels.size, tunnels.size + throwables.size)
messageCallback(message)
}
private const val TAG = "WireGuard/TunnelImporter"
}
@@ -0,0 +1,85 @@
/*
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util
import androidx.datastore.preferences.edit
import androidx.datastore.preferences.preferencesKey
import androidx.datastore.preferences.preferencesSetKey
import androidx.datastore.preferences.remove
import com.wireguard.android.Application
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
object UserKnobs {
private val DISABLE_KERNEL_MODULE = preferencesKey<Boolean>("disable_kernel_module")
val disableKernelModule: Flow<Boolean>
get() = Application.getPreferencesDataStore().data.map {
it[DISABLE_KERNEL_MODULE] ?: false
}
suspend fun setDisableKernelModule(disable: Boolean?) {
Application.getPreferencesDataStore().edit {
if (disable == null)
it.remove(DISABLE_KERNEL_MODULE)
else
it[DISABLE_KERNEL_MODULE] = disable
}
}
private val MULTIPLE_TUNNELS = preferencesKey<Boolean>("multiple_tunnels")
val multipleTunnels: Flow<Boolean>
get() = Application.getPreferencesDataStore().data.map {
it[MULTIPLE_TUNNELS] ?: false
}
private val DARK_THEME = preferencesKey<Boolean>("dark_theme")
val darkTheme: Flow<Boolean>
get() = Application.getPreferencesDataStore().data.map {
it[DARK_THEME] ?: false
}
private val ALLOW_REMOTE_CONTROL_INTENTS = preferencesKey<Boolean>("allow_remote_control_intents")
val allowRemoteControlIntents: Flow<Boolean>
get() = Application.getPreferencesDataStore().data.map {
it[ALLOW_REMOTE_CONTROL_INTENTS] ?: false
}
private val RESTORE_ON_BOOT = preferencesKey<Boolean>("restore_on_boot")
val restoreOnBoot: Flow<Boolean>
get() = Application.getPreferencesDataStore().data.map {
it[RESTORE_ON_BOOT] ?: false
}
private val LAST_USED_TUNNEL = preferencesKey<String>("last_used_tunnel")
val lastUsedTunnel: Flow<String?>
get() = Application.getPreferencesDataStore().data.map {
it[LAST_USED_TUNNEL]
}
suspend fun setLastUsedTunnel(lastUsedTunnel: String?) {
Application.getPreferencesDataStore().edit {
if (lastUsedTunnel == null)
it.remove(LAST_USED_TUNNEL)
else
it[LAST_USED_TUNNEL] = lastUsedTunnel
}
}
private val RUNNING_TUNNELS = preferencesSetKey<String>("enabled_configs")
val runningTunnels: Flow<Set<String>>
get() = Application.getPreferencesDataStore().data.map {
it[RUNNING_TUNNELS] ?: emptySet()
}
suspend fun setRunningTunnels(runningTunnels: Set<String>) {
Application.getPreferencesDataStore().edit {
if (runningTunnels.isEmpty())
it.remove(RUNNING_TUNNELS)
else
it[RUNNING_TUNNELS] = runningTunnels
}
}
}
@@ -22,6 +22,9 @@ class InterfaceProxy : BaseObservable, Parcelable {
@get:Bindable
val excludedApplications: ObservableList<String> = ObservableArrayList()
@get:Bindable
val includedApplications: ObservableList<String> = ObservableArrayList()
@get:Bindable
var addresses: String = ""
set(value) {
@@ -70,6 +73,7 @@ class InterfaceProxy : BaseObservable, Parcelable {
addresses = parcel.readString() ?: ""
dnsServers = parcel.readString() ?: ""
parcel.readStringList(excludedApplications)
parcel.readStringList(includedApplications)
listenPort = parcel.readString() ?: ""
mtu = parcel.readString() ?: ""
privateKey = parcel.readString() ?: ""
@@ -80,6 +84,7 @@ class InterfaceProxy : BaseObservable, Parcelable {
val dnsServerStrings = other.dnsServers.map { it.hostAddress }
dnsServers = Attribute.join(dnsServerStrings)
excludedApplications.addAll(other.excludedApplications)
includedApplications.addAll(other.includedApplications)
listenPort = other.listenPort.map { it.toString() }.orElse("")
mtu = other.mtu.map { it.toString() }.orElse("")
val keyPair = other.keyPair
@@ -103,6 +108,7 @@ class InterfaceProxy : BaseObservable, Parcelable {
if (addresses.isNotEmpty()) builder.parseAddresses(addresses)
if (dnsServers.isNotEmpty()) builder.parseDnsServers(dnsServers)
if (excludedApplications.isNotEmpty()) builder.excludeApplications(excludedApplications)
if (includedApplications.isNotEmpty()) builder.includeApplications(includedApplications)
if (listenPort.isNotEmpty()) builder.parseListenPort(listenPort)
if (mtu.isNotEmpty()) builder.parseMtu(mtu)
if (privateKey.isNotEmpty()) builder.parsePrivateKey(privateKey)
@@ -113,6 +119,7 @@ class InterfaceProxy : BaseObservable, Parcelable {
dest.writeString(addresses)
dest.writeString(dnsServers)
dest.writeStringList(excludedApplications)
dest.writeStringList(includedApplications)
dest.writeString(listenPort)
dest.writeString(mtu)
dest.writeString(privateKey)
@@ -1,66 +0,0 @@
/*
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget
import android.view.View
import android.view.ViewGroup
import androidx.core.view.marginBottom
import androidx.core.view.marginLeft
import androidx.core.view.marginRight
import androidx.core.view.marginTop
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
/**
* A utility for edge-to-edge display. It provides several features needed to make the app
* displayed edge-to-edge on Android Q with gestural navigation.
*/
object EdgeToEdge {
@JvmStatic
fun setUpRoot(root: ViewGroup) {
root.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
}
@JvmStatic
fun setUpScrollingContent(scrollingContent: ViewGroup, fab: ExtendedFloatingActionButton?) {
val originalPaddingLeft = scrollingContent.paddingLeft
val originalPaddingRight = scrollingContent.paddingRight
val originalPaddingBottom = scrollingContent.paddingBottom
val fabPaddingBottom = fab?.height ?: 0
val originalMarginTop = scrollingContent.marginTop
scrollingContent.setOnApplyWindowInsetsListener { _, windowInsets ->
scrollingContent.updatePadding(
left = originalPaddingLeft + windowInsets.systemWindowInsetLeft,
right = originalPaddingRight + windowInsets.systemWindowInsetRight,
bottom = originalPaddingBottom + fabPaddingBottom + windowInsets.systemWindowInsetBottom
)
scrollingContent.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = originalMarginTop + windowInsets.systemWindowInsetTop
}
windowInsets
}
}
@JvmStatic
fun setUpFAB(fab: ExtendedFloatingActionButton) {
val originalMarginLeft = fab.marginLeft
val originalMarginRight = fab.marginRight
val originalMarginBottom = fab.marginBottom
fab.setOnApplyWindowInsetsListener { _, windowInsets ->
fab.updateLayoutParams<ViewGroup.MarginLayoutParams> {
leftMargin = originalMarginLeft + windowInsets.systemWindowInsetLeft
rightMargin = originalMarginRight + windowInsets.systemWindowInsetRight
bottomMargin = originalMarginBottom + windowInsets.systemWindowInsetBottom
}
windowInsets
}
}
}
@@ -1,30 +0,0 @@
/*
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget
import android.content.Context
import android.text.Editable
import android.text.SpannableStringBuilder
import android.util.AttributeSet
import com.google.android.material.R
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
class MonkeyedTextInputEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = R.attr.editTextStyle) : TextInputEditText(context, attrs, defStyleAttr) {
@Override
override fun getText(): Editable? {
val text = super.getText()
if (!text.isNullOrEmpty())
return text
/* We want this expression in TextInputLayout.java to be true if there's a hint set:
* final boolean hasText = editText != null && !TextUtils.isEmpty(editText.getText());
* But for everyone else it should return the real value, so we check the caller.
*/
if (!hint.isNullOrEmpty() && Thread.currentThread().stackTrace[3].className == TextInputLayout::class.qualifiedName)
return SpannableStringBuilder(hint)
return text
}
}
@@ -10,14 +10,14 @@ import android.view.View
import android.widget.RelativeLayout
import com.wireguard.android.R
class MultiselectableRelativeLayout : RelativeLayout {
class MultiselectableRelativeLayout @JvmOverloads constructor(
context: Context? = null,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr, defStyleRes) {
private var multiselected = false
constructor(context: Context?) : super(context) {}
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {}
override fun onCreateDrawableState(extraSpace: Int): IntArray {
if (multiselected) {
val drawableState = super.onCreateDrawableState(extraSpace + 1)
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:duration="300"
android:fromXScale="1.0"
android:fromYScale="1.0"
android:interpolator="@android:anim/linear_interpolator"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="0"
android:toYScale="0" />
</set>
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:duration="300"
android:fromXScale="0"
android:fromYScale="0"
android:interpolator="@android:anim/linear_interpolator"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="1.0"
android:toYScale="1.0" />
</set>
@@ -0,0 +1,14 @@
<!--
~ Copyright © 2020 WireGuard LLC. All Rights Reserved.
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M19,11H7.83l4.88,-4.88c0.39,-0.39 0.39,-1.03 0,-1.42 -0.39,-0.39 -1.02,-0.39 -1.41,0l-6.59,6.59c-0.39,0.39 -0.39,1.02 0,1.41l6.59,6.59c0.39,0.39 1.02,0.39 1.41,0 0.39,-0.39 0.39,-1.02 0,-1.41L7.83,13H19c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1z" />
</vector>
+211
View File
@@ -0,0 +1,211 @@
<!--
~ Copyright © 2020 WireGuard LLC. All Rights Reserved.
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1934.5dp"
android:height="393.14dp"
android:viewportWidth="1934.5"
android:viewportHeight="393.14">
<path
android:fillColor="#88171a"
android:fillType="nonZero"
android:pathData="M0,0L1934.5,0L1934.5,393.14L0,393.14Z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:fillType="nonZero"
android:pathData="M432.69,266.47 L367.47,66.48l24.46,0l49.64,154.34 50.91,-154.34l22.28,0l50.72,153.98 49.81,-153.98L639.03,66.48L573.99,266.47L555.87,266.47L503.16,105.43 450.99,266.47Z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:fillType="nonZero"
android:pathData="m634.56,111.4l24.64,0l0,155.07l-24.64,0z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:fillType="nonZero"
android:pathData="m716.31,198.9l0,67.57L692.04,266.47L692.04,112.13l106.52,0c15.82,0 28.2,3.8 37.14,11.41 8.94,7.61 13.41,18.18 13.41,31.7 0.18,10.5 -3.6,20.69 -10.6,28.53 -7.06,8.03 -16.27,12.95 -27.63,14.77l42.93,67.93l-26.27,0l-43.84,-67.57zM716.31,176.25l82.24,0c8.33,0 14.7,-1.81 19.11,-5.43 4.41,-3.63 6.61,-8.82 6.61,-15.58 0,-6.77 -2.2,-11.93 -6.61,-15.49 -4.41,-3.56 -10.78,-5.34 -19.11,-5.34l-82.24,0z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:fillType="nonZero"
android:pathData="M871.98,266.47L871.98,111.77l145.1,0l0,22.64l-120.83,0l0,38.59l79.34,0l0,22.28l-79.34,0l0,48.91l127.53,0l0,22.28L871.97,266.47Z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:fillType="nonZero"
android:pathData="m1194.46,212.31l0,-29.35l-54.53,0l0,-23.01l79.34,0l0,60.14c-9.66,16.67 -22.25,29.44 -37.77,38.31 -15.52,8.88 -33.13,13.32 -52.81,13.32 -29.95,0 -54.68,-9.87 -74.18,-29.62 -19.51,-19.75 -29.26,-44.78 -29.26,-75.09 0,-30.43 9.78,-55.52 29.34,-75.27 19.57,-19.75 44.27,-29.62 74.09,-29.62 18.48,0 35.32,4.05 50.54,12.13 15.2,8.08 28.01,20.01 37.13,34.6L1195.55,123.72c-6.13,-11.8 -15.58,-21.56 -27.17,-28.08 -12.09,-6.83 -25.79,-10.33 -39.67,-10.15 -22.46,0 -41.06,7.7 -55.8,23.1 -14.73,15.4 -22.1,34.87 -22.1,58.42 0,23.55 7.37,42.99 22.1,58.33 14.73,15.34 33.33,23.01 55.8,23.01 14.01,0 26.48,-3.02 37.41,-9.06 10.93,-6.04 20.38,-15.03 28.35,-26.99z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:fillType="nonZero"
android:pathData="m1246.36,112.14l24.28,0l0,99.09c0,14.85 3.5,24.87 10.5,30.07 7.01,5.2 20.89,7.79 41.67,7.79 20.89,0 34.84,-2.59 41.84,-7.79 7.01,-5.19 10.51,-15.21 10.51,-30.07L1375.16,112.14l24.09,0l0,105.43c0,18.96 -6.01,32.67 -18.02,41.13 -12.02,8.45 -31.61,12.68 -58.79,12.68 -27.05,0 -46.49,-4.17 -58.33,-12.5 -11.84,-8.33 -17.75,-22.1 -17.75,-41.3z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:fillType="nonZero"
android:pathData="m1395.46,266.47 l78.62,-154.7l15.22,0l79.34,154.7l-25.9,0l-20.11,-39.31l-81.7,0l-19.93,39.31zM1451.44,206.69l60.51,0l-30.07,-59.23z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:fillType="nonZero"
android:pathData="m1595.56,198.9l0,67.57l-24.28,0L1571.29,112.13l106.52,0c15.82,0 28.2,3.8 37.14,11.41 8.94,7.61 13.41,18.18 13.41,31.7 0.18,10.5 -3.6,20.69 -10.6,28.53 -7.06,8.03 -16.27,12.95 -27.62,14.77l42.93,67.93l-26.27,0l-43.84,-67.57zM1595.56,176.25l82.24,0c8.33,0 14.7,-1.81 19.11,-5.43 4.41,-3.63 6.61,-8.82 6.61,-15.58 0,-6.77 -2.2,-11.93 -6.61,-15.49 -4.41,-3.56 -10.78,-5.34 -19.11,-5.34l-82.24,0z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:fillType="nonZero"
android:pathData="m1820.26,111.77c25.24,0 45.59,7.21 61.05,21.65 15.46,14.43 23.19,33.18 23.19,56.25 0,23.31 -7.58,41.94 -22.73,55.89 -15.16,13.95 -35.66,20.92 -61.5,20.92l-68.66,0L1751.6,111.78l68.66,0zM1820.63,134.05l-44.75,0l0,110.14l44.75,0c18.35,0 32.79,-4.92 43.29,-14.77 10.51,-9.84 15.76,-23.21 15.76,-40.12 0,-16.3 -5.43,-29.59 -16.3,-39.85 -10.87,-10.26 -25.12,-15.4 -42.75,-15.4l0,0z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m429.67,318.63l0,18.61l-6.35,0l0,-40.45l37.37,0l0,5.92l-31.02,0l0,10.08l20.04,0l0,5.83z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m455.24,337.24 l20.56,-40.45l3.98,0l20.75,40.45l-6.78,0l-5.26,-10.28l-21.36,0l-5.21,10.28zM469.87,321.61l15.82,0l-7.86,-15.49z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m505.02,332.08 l3.08,-5.3c2.81,1.87 5.87,3.34 9.09,4.35 3.04,1.01 6.22,1.53 9.43,1.57 3.06,0.14 6.09,-0.62 8.71,-2.2 2.03,-1.16 3.3,-3.32 3.32,-5.66 0.08,-2.16 -1.1,-4.17 -3.03,-5.14 -2.03,-1.12 -5.21,-1.75 -9.57,-1.87 -7.36,-0.09 -12.41,-0.95 -15.16,-2.56 -2.75,-1.61 -4.13,-4.28 -4.13,-8.01 -0.04,-3.39 1.67,-6.56 4.53,-8.38 3.01,-2.12 7.06,-3.18 12.15,-3.18 3.43,-0.01 6.83,0.49 10.11,1.47 3.33,1.01 6.52,2.48 9.45,4.36l-3.17,5.26c-2.52,-1.7 -5.25,-3.03 -8.15,-3.95 -2.69,-0.9 -5.5,-1.37 -8.34,-1.4 -2.56,-0.11 -5.1,0.44 -7.39,1.61 -1.66,0.76 -2.75,2.39 -2.8,4.21 -0.03,1.77 1.13,3.33 2.82,3.84 1.88,0.76 5.14,1.14 9.78,1.14 6.44,0 11.31,1.09 14.59,3.27 3.29,2.18 4.93,5.42 4.93,9.71 0.01,3.8 -1.86,7.37 -5,9.52 -3.33,2.53 -7.65,3.79 -12.95,3.79 -3.95,-0.01 -7.88,-0.57 -11.67,-1.68 -3.75,-1.07 -7.34,-2.67 -10.63,-4.76z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m568.06,337.24l0,-34.81l-17.05,0l0,-5.63l40.45,0l0,5.63L574.36,302.43l0,34.81z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m604.52,345.48c0.91,-1.32 1.73,-2.7 2.46,-4.12 0.68,-1.32 1.25,-2.7 1.7,-4.12l-0.38,0c-0.96,0.01 -1.86,-0.44 -2.44,-1.21 -0.67,-0.87 -1.01,-1.95 -0.97,-3.06 -0.05,-1.22 0.36,-2.41 1.16,-3.34 0.73,-0.84 1.8,-1.32 2.92,-1.3 1.3,-0.02 2.52,0.64 3.22,1.73 0.89,1.39 1.32,3.02 1.23,4.67 -0.09,2.03 -0.71,3.99 -1.8,5.71 -1.5,2.45 -3.3,4.7 -5.35,6.7z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m668.13,337.24l0,-40.45l5.25,0l16.58,23.68 16.58,-23.68l5.26,0l0,40.45l-6.3,0l0,-29.08l-15.54,22.49 -15.49,-22.49l0,29.08z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m748.83,338.52c-7.2,0 -13.12,-2.04 -17.76,-6.11 -4.42,-3.87 -6.96,-9.46 -6.96,-15.33 -0.01,-5.87 2.52,-11.47 6.94,-15.34 4.63,-4.02 10.55,-6.04 17.78,-6.04 7.3,0 13.25,2.01 17.88,6.02 4.42,3.89 6.95,9.48 6.94,15.36 -0.01,5.88 -2.54,11.47 -6.96,15.35 -4.64,4.06 -10.59,6.09 -17.85,6.09zM748.83,332.6c5.4,0 9.81,-1.46 13.22,-4.38 3.25,-2.79 5.12,-6.86 5.11,-11.15 -0.01,-4.29 -1.89,-8.35 -5.14,-11.14 -3.43,-2.94 -7.82,-4.4 -13.19,-4.4 -5.36,0 -9.75,1.46 -13.14,4.38 -3.23,2.8 -5.09,6.87 -5.09,11.15 0,4.28 1.86,8.35 5.09,11.16 3.39,2.92 7.78,4.38 13.14,4.38z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m803.92,296.79c6.6,0 11.92,1.89 15.96,5.66 4.04,3.78 6.06,8.68 6.06,14.71 0,6.09 -1.98,10.96 -5.94,14.61 -3.96,3.65 -9.32,5.47 -16.08,5.47l-17.95,0l0,-40.45zM804.02,302.61l-11.7,0l0,28.8l11.7,0c4.8,0 8.57,-1.29 11.32,-3.86 2.75,-2.57 4.13,-6.07 4.13,-10.49 0.13,-3.93 -1.42,-7.72 -4.27,-10.42 -2.84,-2.68 -6.57,-4.03 -11.18,-4.03z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m837.87,337.24l0,-40.45l37.94,0l0,5.92l-31.59,0l0,10.09l20.75,0l0,5.82l-20.75,0l0,12.79l33.34,0l0,5.82z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m894.94,319.57l0,17.67l-6.35,0L888.59,296.88l27.85,0c4.14,0 7.37,0.99 9.71,2.98 2.38,2.08 3.67,5.14 3.51,8.29 0.05,2.75 -0.94,5.41 -2.77,7.46 -1.86,2.11 -4.43,3.48 -7.22,3.86l11.22,17.76l-6.87,0l-11.46,-17.67zM894.94,313.65l21.5,0c1.78,0.12 3.55,-0.39 5,-1.42 1.18,-1.01 1.82,-2.52 1.72,-4.07 0.1,-1.55 -0.54,-3.05 -1.72,-4.05 -1.46,-1.02 -3.22,-1.52 -5,-1.4l-21.5,0z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m943.11,337.24l0,-40.45l3.32,0l25.72,28.42l0,-28.42l6.2,0l0,40.45l-3.32,0l-25.67,-28.32l0,28.32z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m999,345.48c0.91,-1.32 1.73,-2.7 2.46,-4.12 0.68,-1.32 1.25,-2.7 1.7,-4.12l-0.37,0c-0.96,0.01 -1.87,-0.44 -2.44,-1.21 -0.67,-0.87 -1.01,-1.95 -0.97,-3.06 -0.05,-1.22 0.37,-2.41 1.16,-3.34 0.72,-0.84 1.8,-1.32 2.91,-1.3 1.3,-0.02 2.52,0.64 3.22,1.73 0.89,1.39 1.32,3.02 1.23,4.67 -0.09,2.03 -0.71,3.99 -1.8,5.71 -1.5,2.45 -3.3,4.7 -5.35,6.7z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m1059.16,332.08 l3.08,-5.3c2.81,1.87 5.87,3.34 9.09,4.35 3.04,1.01 6.22,1.53 9.43,1.57 3.06,0.14 6.09,-0.62 8.71,-2.2 2.03,-1.16 3.3,-3.32 3.31,-5.66 0.09,-2.16 -1.1,-4.17 -3.03,-5.14 -2.02,-1.12 -5.21,-1.75 -9.57,-1.87 -7.35,-0.09 -12.41,-0.95 -15.15,-2.56 -2.75,-1.61 -4.12,-4.28 -4.12,-8.01 -0.04,-3.39 1.67,-6.56 4.53,-8.38 3.02,-2.12 7.06,-3.18 12.15,-3.18 3.43,-0.01 6.83,0.49 10.11,1.47 3.33,1.01 6.52,2.48 9.45,4.36l-3.17,5.26c-2.51,-1.7 -5.25,-3.03 -8.15,-3.95 -2.69,-0.9 -5.51,-1.37 -8.34,-1.4 -2.56,-0.11 -5.1,0.44 -7.39,1.61 -1.66,0.76 -2.74,2.39 -2.8,4.21 -0.03,1.77 1.13,3.33 2.82,3.84 1.88,0.76 5.14,1.14 9.78,1.14 6.44,0 11.3,1.09 14.59,3.27 3.28,2.18 4.93,5.41 4.93,9.71 0.01,3.8 -1.86,7.37 -5,9.52 -3.33,2.53 -7.65,3.79 -12.95,3.79 -3.95,-0.01 -7.88,-0.57 -11.68,-1.68 -3.75,-1.07 -7.33,-2.67 -10.63,-4.76z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m1112.56,337.24l0,-40.45l37.94,0l0,5.92L1118.91,302.71l0,10.09l20.74,0l0,5.82L1118.91,318.63l0,12.79l33.34,0l0,5.82z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m1206.06,327.34c-2.21,3.55 -5.35,6.42 -9.07,8.31 -3.96,1.96 -8.34,2.94 -12.77,2.87 -7.1,0 -12.95,-2.01 -17.55,-6.04 -4.38,-3.89 -6.89,-9.48 -6.89,-15.34 0,-5.87 2.51,-11.45 6.89,-15.35 4.59,-4.06 10.44,-6.08 17.55,-6.08 4.15,-0.06 8.25,0.79 12.03,2.49 3.47,1.55 6.47,3.97 8.71,7.04l-5.02,3.51c-1.7,-2.33 -3.97,-4.16 -6.6,-5.33 -2.87,-1.28 -5.98,-1.92 -9.12,-1.87 -4.69,-0.17 -9.27,1.41 -12.86,4.43 -3.23,2.78 -5.09,6.84 -5.09,11.11 0.01,4.27 1.87,8.32 5.11,11.1 3.58,3.02 8.16,4.59 12.83,4.43 3.3,0.07 6.57,-0.66 9.52,-2.13 2.93,-1.58 5.41,-3.86 7.24,-6.64z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m1218.66,296.89l6.34,0l0,25.91c0,3.89 0.92,6.51 2.75,7.86 1.83,1.36 5.46,2.04 10.89,2.04 5.46,0 9.11,-0.68 10.94,-2.04 1.83,-1.36 2.75,-3.98 2.75,-7.86l0,-25.91l6.3,0l0,27.56c0,4.96 -1.57,8.54 -4.71,10.75 -3.14,2.21 -8.27,3.31 -15.37,3.31 -7.07,0 -12.16,-1.09 -15.25,-3.27 -3.09,-2.18 -4.64,-5.78 -4.64,-10.8z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m1280.86,319.57l0,17.67l-6.34,0L1274.52,296.88l27.85,0c4.14,0 7.37,0.99 9.71,2.98 2.38,2.08 3.67,5.14 3.51,8.29 0.05,2.75 -0.94,5.41 -2.77,7.46 -1.86,2.11 -4.43,3.48 -7.22,3.86l11.22,17.76l-6.86,0l-11.46,-17.67zM1280.86,313.65l21.5,0c1.78,0.12 3.55,-0.39 5,-1.42 1.18,-1.01 1.82,-2.52 1.73,-4.07 0.1,-1.55 -0.55,-3.05 -1.73,-4.05 -1.46,-1.02 -3.22,-1.52 -5,-1.4l-21.5,0z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m1328.96,337.24l0,-40.45l37.94,0l0,5.92l-31.59,0l0,10.09l20.75,0l0,5.82l-20.75,0l0,12.79l33.34,0l0,5.82z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m1431.46,337.24l-3.27,0l-20.32,-40.45l6.72,0l15.2,30.27 14.97,-30.27l6.96,0z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m1468.06,319.57l0,17.67l-6.35,0L1461.71,296.79l29.41,0c4.17,0 7.42,0.99 9.73,2.96 2.37,2.09 3.66,5.15 3.48,8.31 0.1,3.13 -1.16,6.16 -3.46,8.29 -2.49,2.22 -5.76,3.37 -9.09,3.22zM1468.06,313.65l23.06,0c1.77,0.11 3.51,-0.39 4.95,-1.42 2.28,-2.21 2.31,-5.85 0.07,-8.1 -1.35,-1.03 -3.04,-1.53 -4.73,-1.42l-23.35,0z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m1515.26,337.24l0,-40.45l3.32,0l25.71,28.42l0,-28.42l6.21,0l0,40.45l-3.32,0l-25.67,-28.32l0,28.32z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m1610.06,337.24l0,-34.81l-17.05,0l0,-5.63l40.45,0l0,5.63l-17.1,0l0,34.81z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m1641.76,296.89l6.35,0l0,25.91c0,3.89 0.91,6.51 2.75,7.86 1.83,1.36 5.46,2.04 10.89,2.04 5.46,0 9.11,-0.68 10.94,-2.04 1.83,-1.36 2.75,-3.98 2.75,-7.86l0,-25.91l6.3,0l0,27.56c0,4.96 -1.57,8.54 -4.71,10.75 -3.14,2.21 -8.27,3.31 -15.37,3.31 -7.07,0 -12.16,-1.09 -15.25,-3.27 -3.09,-2.18 -4.64,-5.78 -4.64,-10.8z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m1697.66,337.24l0,-40.45l3.31,0l25.72,28.42l0,-28.42l6.21,0l0,40.45l-3.32,0l-25.67,-28.32l0,28.32z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m1748.76,337.24l0,-40.45l3.32,0l25.71,28.42L1777.79,296.79L1784,296.79l0,40.45l-3.32,0l-25.67,-28.32l0,28.32z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m1799.76,337.24l0,-40.45l37.94,0l0,5.92l-31.59,0l0,10.09l20.75,0l0,5.82l-20.75,0l0,12.79l33.34,0l0,5.82z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m1850.46,337.24l0,-40.45l6.35,0l0,34.62l30.03,0l0,5.82z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:fillType="nonZero"
android:pathData="M329.74,185.56C329.74,185.56 336.68,40 176.7,40 35.22,40 30.8,179.63 30.8,179.63 30.8,179.63 9.99,340 179.96,340 342.98,340 329.74,185.56 329.74,185.56ZM131.94,134.7c30.02,-18.36 68.36,-7.14 82.73,20.47 2.72,5.23 3.07,13.29 1.34,18.78 -5.95,18.96 -20.02,29.59 -39.31,34.1 5.69,-4.87 10.22,-10.4 11.66,-18.03 1.52,-7.31 -0.13,-14.93 -4.55,-20.95 -7.03,-9.65 -19.59,-13.49 -30.81,-9.39 -11.89,4.51 -18.39,15.35 -17.22,28.68 1.09,12.38 10.48,20.41 28.06,23.45 -2.63,1.39 -4.65,2.42 -6.63,3.52 -8.05,4.41 -15.06,10.51 -20.54,17.87 -1.79,2.41 -3.01,2.6 -5.73,0.94 -35.34,-21.61 -37.61,-75.84 0.98,-99.45zM105.49,268.23c-5.68,1.44 -11.18,3.57 -16.98,5.47 2.84,-19.15 25.27,-36.79 44.23,-34.78 -5.49,7.57 -8.7,16.56 -9.24,25.9 -6.3,1.16 -12.25,1.94 -18.01,3.4zM226.28,81.25c5.61,0.21 11.23,0.12 16.84,0.25 1.4,0.09 2.8,0.29 4.17,0.58 -1.26,1.93 -2.67,3.75 -4.23,5.43 -2.01,1.87 -4.28,3.7 -7.17,0.85 -0.69,-0.68 -2.34,-0.53 -3.55,-0.54 -5.58,-0.07 -11.17,-0.25 -16.75,-0.04 -4.84,0.16 -9.66,0.65 -14.43,1.47 -0.9,0.16 -2.23,3.13 -1.82,4.22 0.97,2.59 2.38,5.44 4.47,7.09 7.75,6.11 15.97,11.59 23.75,17.66 7.56,5.9 14.59,12.36 18.87,21.25 5.58,11.59 5.74,23.74 3.34,35.95 -4.02,20.38 -14.33,37.26 -31.03,49.53 -6.73,4.94 -15.06,7.74 -22.77,11.29 -6.78,3.13 -13.75,5.81 -20.55,8.9 -12.25,5.57 -19.13,18.87 -17.1,32.69 1.85,12.69 12.98,23.27 25.73,25.46 15.29,2.62 31.07,-7.32 34.81,-22.86 4.2,-17.48 -5.29,-33.08 -23.07,-37.81 -0.78,-0.21 -1.57,-0.41 -3.2,-0.83 4.75,-2.12 8.86,-3.64 12.66,-5.72 6.61,-3.64 13.11,-7.49 19.48,-11.56 1.88,-1.2 2.89,-1.2 4.49,0.18 12.23,10.57 19.52,23.72 21.56,39.84 3.39,26.68 -9.25,51.2 -33.07,63.76 -36.87,19.44 -81.97,-2.68 -90.11,-43.55 -6.97,-35 17.73,-66.75 47.46,-72.88 12.79,-2.63 24.48,-7.96 33.57,-17.81 5.87,-6.35 8.71,-11.81 9.68,-14.27 1.81,-4.61 2.73,-9.52 2.72,-14.47 -0.2,-4.29 -1.2,-8.49 -2.96,-12.4 -3.1,-7.07 -15,-18.33 -17.94,-20.7l-28,-21.92c-0.98,-0.81 -2.1,-0.75 -4.51,-0.59 -2.86,0.19 -10.18,0.6 -13.33,-0.23 2.55,-1.93 9.52,-4.74 12.51,-7.01 -9.08,-6.13 -19.43,-3.92 -28.94,-5.75 2.2,-4.09 13.08,-10.39 19.27,-11.09 -0.37,-3.46 -0.93,-6.89 -1.69,-10.28 -0.38,-1.39 -1.93,-2.74 -3.29,-3.54 -3.29,-1.93 -6.77,-3.52 -10.55,-5.43 3.39,-2.19 7.31,-3.4 11.33,-3.51 3.82,-0.15 7.63,0.23 11.35,1.1 6.74,1.54 12.12,0.54 17.49,-4.05 -4.22,-1.7 -8.45,-3.25 -12.54,-5.09 -4.03,-1.84 -7.96,-3.9 -11.78,-6.16 10.62,1.48 20.89,5.46 31.75,4 0.09,-0.49 0.19,-0.99 0.28,-1.48 -8.12,-1.89 -16.24,-3.78 -25.23,-5.87 15.04,-1.37 29.04,-1.6 42.3,4.85 3.73,1.82 7.63,3.32 11.21,5.4 1.74,1.02 2.92,3.01 4.35,4.56 1.13,1.23 2.05,2.88 3.44,3.63 5.3,2.82 11.13,2.93 17.08,2.79 0.05,-0.68 0.09,-1.31 0.13,-1.99 5.98,1.87 12.72,8.77 12.71,13.8 -9.69,0 -19.37,-0.04 -29.06,0.06 -1.04,0.01 -2.06,0.77 -3.09,1.17 0.98,0.57 1.94,1.6 2.94,1.64z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:fillType="nonZero"
android:pathData="m213.78,66.91c-0.84,0.53 -0.94,1.71 -0.19,2.37 0.61,1.08 1.99,1.45 3.07,0.82 0.94,-0.47 1.85,-0.97 2.98,-1.57 -0.91,-0.78 -1.64,-1.42 -2.39,-2.03 -1.32,-1.09 -2.41,-0.41 -3.47,0.41z"
android:strokeColor="#00000000" />
<path
android:fillColor="#d1d1d1"
android:fillType="nonZero"
android:pathData="m1890,97.71l-1.24,0c-7.56,0.08 -11.84,6.64 -11.84,13 0,6.16 3.92,12.32 12.2,12.32l0.56,0c8.6,-0.24 12.36,-6.16 12.36,-12.24l0,-0.72c-0.28,-6.36 -4.84,-12.36 -12.04,-12.36zM1884.48,102.87l0,14.44l1.8,0l0,-12.96l3.4,0c1.88,0 2.8,1.16 2.8,2.32 0,1.4 -1.36,3 -3.8,3 -0.37,0 -0.84,-0.08 -1.28,-0.16l5.44,7.8l2.36,0l-4.92,-6.64c2.4,-0.44 4,-2.36 4,-4.24 0,-1.76 -1.52,-3.56 -4.88,-3.56zM1881.04,103.16c2.52,-2.72 5.16,-3.72 8.24,-3.72l1.28,0c5.48,0.24 9.44,5.8 9.44,11.16 0,0.76 -0.08,1.64 -0.24,2.36 -0.84,4.92 -5.12,8.36 -10.08,8.36 -0.12,0 -0.37,0.04 -0.49,0.04 -6.28,0 -10.28,-5.68 -10.28,-11.36 0,-0.48 0.08,-1.12 0.12,-1.64 0,0 0.28,-3.12 2,-5.2z"
android:strokeColor="#00000000" />
</vector>
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_activity_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.MainActivity">
@@ -15,6 +15,8 @@
android:layout_marginLeft="@dimen/normal_margin"
android:layout_marginEnd="@dimen/normal_margin"
android:layout_marginRight="@dimen/normal_margin"
android:nextFocusDown="@id/create_from_qrcode"
android:nextFocusForward="@id/create_from_qrcode"
android:text="@string/create_from_file"
android:textAlignment="viewStart"
android:textColor="?attr/colorOnSurface"
@@ -36,6 +38,9 @@
android:layout_marginLeft="@dimen/normal_margin"
android:layout_marginEnd="@dimen/normal_margin"
android:layout_marginRight="@dimen/normal_margin"
android:nextFocusUp="@id/create_from_file"
android:nextFocusDown="@id/create_empty"
android:nextFocusForward="@id/create_empty"
android:text="@string/create_from_qr_code"
android:textAlignment="viewStart"
android:textColor="?attr/colorOnSurface"
@@ -57,6 +62,7 @@
android:layout_marginLeft="@dimen/normal_margin"
android:layout_marginEnd="@dimen/normal_margin"
android:layout_marginRight="@dimen/normal_margin"
android:nextFocusUp="@id/create_from_qrcode"
android:text="@string/create_empty"
android:textAlignment="viewStart"
android:textColor="?attr/colorOnSurface"
@@ -18,30 +18,50 @@
type="com.wireguard.android.databinding.ObservableKeyedArrayList&lt;String, ApplicationData&gt;" />
</data>
<FrameLayout
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="200dp">
android:orientation="vertical">
<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}"
tools:visibility="gone" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
style="@style/Widget.MaterialComponents.TabLayout.Colored"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/app_list"
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/exclude_from_tunnel" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/include_in_tunnel" />
</com.google.android.material.tabs.TabLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:items="@{appData}"
app:layout="@{@layout/app_list_item}"
tools:itemCount="10"
tools:listitem="@layout/app_list_item" />
</FrameLayout>
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}"
tools:visibility="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}"
tools:itemCount="10"
tools:listitem="@layout/app_list_item" />
</FrameLayout>
</LinearLayout>
</layout>
+3 -3
View File
@@ -24,7 +24,7 @@
android:layout_height="wrap_content"
android:background="@drawable/list_item_background"
android:gravity="center_vertical"
android:onClick="@{(view) -> item.setExcludedFromTunnel(!item.excludedFromTunnel)}"
android:onClick="@{(view) -> item.setSelected(!item.selected)}"
android:orientation="horizontal"
android:paddingTop="8dp"
android:paddingBottom="8dp">
@@ -51,10 +51,10 @@
tools:text="@tools:sample/full_names" />
<CheckBox
android:id="@+id/excluded_checkbox"
android:id="@+id/selected_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="@={item.excludedFromTunnel}"
android:checked="@={item.selected}"
tools:checked="true" />
</LinearLayout>
@@ -17,11 +17,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/tunnel_name_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/tunnel_name"
android:imeOptions="actionDone"
android:inputType="textNoSuggestions|textVisiblePassword"
app:filter="@{NameInputFilter.newInstance()}" />
@@ -29,5 +30,4 @@
</FrameLayout>
</layout>
@@ -16,13 +16,12 @@
tools:itemCount="20"
tools:listitem="@layout/log_viewer_entry" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/share_fab"
style="@style/Widget.MaterialComponents.ExtendedFloatingActionButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
app:icon="@drawable/ic_action_share_white" />
app:srcCompat="@drawable/ic_action_share_white" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/master_detail_wrapper"
android:id="@+id/main_activity_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
+246 -111
View File
@@ -22,131 +22,266 @@
type="com.wireguard.config.Config" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorBackground"
android:clickable="true"
android:focusable="true"
android:orientation="vertical">
android:focusable="true">
<com.google.android.material.card.MaterialCardView
android:id="@+id/tunnel_detail_card"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:cardBackgroundColor="?attr/elevationOverlayColor"
app:cardCornerRadius="0dp"
app:cardElevation="2dp"
app:contentPadding="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
<com.google.android.material.card.MaterialCardView
android:id="@+id/tunnel_detail_card"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/interface_title"
style="@style/SectionText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/interface_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.wireguard.android.widget.ToggleSwitch
android:id="@+id/tunnel_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:checked="@{tunnel.state == State.UP}"
app:layout_constraintBaseline_toBaselineOf="@+id/interface_title"
app:layout_constraintEnd_toEndOf="parent"
app:onBeforeCheckedChanged="@{fragment::setTunnelState}" />
<TextView
android:id="@+id/interface_name_label"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/interface_name_text"
android:text="@string/name"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/interface_title" />
android:layout_height="wrap_content">
<TextView
android:id="@+id/interface_name_text"
style="@style/DetailText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{tunnel.name}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/interface_name_label"
tools:text="wg0" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/interface_title"
style="@style/SectionText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/interface_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/public_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/public_key_text"
android:text="@string/public_key"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/interface_name_text" />
<com.wireguard.android.widget.ToggleSwitch
android:id="@+id/tunnel_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:nextFocusDown="@id/interface_name_text"
android:nextFocusForward="@id/interface_name_text"
app:checked="@{tunnel.state == State.UP}"
app:layout_constraintBaseline_toBaselineOf="@+id/interface_title"
app:layout_constraintEnd_toEndOf="parent"
app:onBeforeCheckedChanged="@{fragment::setTunnelState}" />
<TextView
android:id="@+id/public_key_text"
style="@style/DetailText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/public_key_description"
android:ellipsize="end"
android:maxLines="1"
android:onClick="@{ClipboardUtils::copyTextView}"
android:singleLine="true"
android:text="@{config.interface.keyPair.publicKey.toBase64}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/public_key_label"
tools:text="wOs2eguFEohqIZxlSJ1CAT9584tc6ejj9hfGFsoBVkA=" />
<TextView
android:id="@+id/interface_name_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/interface_name_text"
android:text="@string/name"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/interface_title" />
<TextView
android:id="@+id/addresses_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/addresses_text"
android:text="@string/addresses"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/public_key_text" />
<TextView
android:id="@+id/interface_name_text"
style="@style/DetailText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/name"
android:nextFocusUp="@id/tunnel_switch"
android:nextFocusDown="@id/public_key_text"
android:nextFocusForward="@id/public_key_text"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{tunnel.name}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/interface_name_label"
tools:text="wg0" />
<TextView
android:id="@+id/addresses_text"
style="@style/DetailText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/addresses"
android:text="@{config.interface.addresses}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/addresses_label"
tools:text="fc00:bbbb:bbbb:bb11::3:368b/128" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/public_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/public_key_text"
android:text="@string/public_key"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/interface_name_text" />
<LinearLayout
android:id="@+id/peers_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:divider="@null"
android:orientation="vertical"
app:items="@{config.peers}"
app:layout="@{@layout/tunnel_detail_peer}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tunnel_detail_card"
tools:ignore="UselessLeaf" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/public_key_text"
style="@style/DetailText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/public_key"
android:ellipsize="end"
android:maxLines="1"
android:nextFocusUp="@id/interface_name_text"
android:nextFocusDown="@id/addresses_text"
android:nextFocusForward="@id/addresses_text"
android:onClick="@{ClipboardUtils::copyTextView}"
android:singleLine="true"
android:text="@{config.interface.keyPair.publicKey.toBase64}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/public_key_label"
tools:text="wOs2eguFEohqIZxlSJ1CAT9584tc6ejj9hfGFsoBVkA=" />
<TextView
android:id="@+id/addresses_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/addresses_text"
android:text="@string/addresses"
android:visibility="@{config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/public_key_text" />
<TextView
android:id="@+id/addresses_text"
style="@style/DetailText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/addresses"
android:nextFocusUp="@id/public_key_text"
android:nextFocusDown="@id/dns_servers_text"
android:nextFocusForward="@id/dns_servers_text"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{config.interface.addresses}"
android:visibility="@{config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/addresses_label"
tools:text="fc00:bbbb:bbbb:bb11::3:368b/128" />
<TextView
android:id="@+id/dns_servers_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/dns_servers_text"
android:text="@string/dns_servers"
android:visibility="@{config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/addresses_text" />
<TextView
android:id="@+id/dns_servers_text"
style="@style/DetailText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/dns_servers"
android:nextFocusUp="@id/addresses_text"
android:nextFocusDown="@id/listen_port_text"
android:nextFocusForward="@id/listen_port_text"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{config.interface.dnsServers}"
android:visibility="@{config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dns_servers_label"
tools:text="8.8.8.8, 8.8.4.4" />
<TextView
android:id="@+id/listen_port_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/listen_port_text"
android:text="@string/listen_port"
android:visibility="@{!config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintEnd_toStartOf="@id/mtu_label"
app:layout_constraintHorizontal_weight="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/dns_servers_text" />
<TextView
android:id="@+id/listen_port_text"
style="@style/DetailText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:contentDescription="@string/listen_port"
android:nextFocusRight="@id/mtu_text"
android:nextFocusUp="@id/dns_servers_text"
android:nextFocusDown="@id/applications_text"
android:nextFocusForward="@id/mtu_text"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{config.interface.listenPort}"
android:visibility="@{!config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintEnd_toStartOf="@id/mtu_label"
app:layout_constraintHorizontal_weight="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/listen_port_label"
tools:text="51820" />
<TextView
android:id="@+id/mtu_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/mtu_text"
android:text="@string/mtu"
android:visibility="@{!config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="0.5"
app:layout_constraintLeft_toRightOf="@id/listen_port_label"
app:layout_constraintStart_toEndOf="@id/listen_port_label"
app:layout_constraintTop_toBottomOf="@id/dns_servers_text" />
<TextView
android:id="@+id/mtu_text"
style="@style/DetailText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:contentDescription="@string/mtu"
android:nextFocusLeft="@id/listen_port_text"
android:nextFocusUp="@id/dns_servers_text"
android:nextFocusForward="@id/applications_text"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{config.interface.mtu}"
android:visibility="@{!config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="0.5"
app:layout_constraintStart_toEndOf="@id/listen_port_label"
app:layout_constraintStart_toStartOf="@+id/mtu_label"
app:layout_constraintTop_toBottomOf="@+id/mtu_label"
tools:text="1500" />
<TextView
android:id="@+id/applications_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/applications_text"
android:text="@string/applications"
android:visibility="@{config.interface.includedApplications.isEmpty() &amp;&amp; config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/listen_port_text" />
<TextView
android:id="@+id/applications_text"
style="@style/DetailText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/applications"
android:nextFocusUp="@id/mtu_text"
android:nextFocusDown="@id/peers_layout"
android:nextFocusForward="@id/peers_layout"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{config.interface.includedApplications.isEmpty() ? @plurals/n_excluded_applications(config.interface.excludedApplications.size(), config.interface.excludedApplications.size()) : @plurals/n_included_applications(config.interface.includedApplications.size(), config.interface.includedApplications.size())}"
android:visibility="@{config.interface.includedApplications.isEmpty() &amp;&amp; config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/applications_label"
tools:text="8 excluded" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<LinearLayout
android:id="@+id/peers_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:divider="@null"
android:orientation="vertical"
app:items="@{config.peers}"
app:layout="@{@layout/tunnel_detail_peer}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tunnel_detail_card"
tools:ignore="UselessLeaf" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</layout>
+79 -8
View File
@@ -15,11 +15,7 @@
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardBackgroundColor="?attr/elevationOverlayColor"
app:cardCornerRadius="0dp"
app:cardElevation="2dp"
app:contentPadding="8dp">
android:layout_margin="8dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
@@ -49,9 +45,11 @@
style="@style/DetailText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/public_key_description"
android:contentDescription="@string/public_key"
android:ellipsize="end"
android:maxLines="1"
android:nextFocusDown="@id/pre_shared_key_text"
android:nextFocusForward="@id/pre_shared_key_text"
android:onClick="@{ClipboardUtils::copyTextView}"
android:singleLine="true"
android:text="@{item.publicKey.toBase64}"
@@ -59,6 +57,35 @@
app:layout_constraintTop_toBottomOf="@+id/public_key_label"
tools:text="wOs2eguFEohqIZxlSJ1CAT9584tc6ejj9hfGFsoBVkA=" />
<TextView
android:id="@+id/pre_shared_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/pre_shared_key_text"
android:text="@string/pre_shared_key"
android:visibility="@{!item.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/public_key_text" />
<TextView
android:id="@+id/pre_shared_key_text"
style="@style/DetailText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/pre_shared_key"
android:ellipsize="end"
android:maxLines="1"
android:nextFocusUp="@id/public_key_text"
android:nextFocusDown="@id/allowed_ips_text"
android:nextFocusForward="@id/allowed_ips_text"
android:singleLine="true"
android:text="@string/pre_shared_key_enabled"
android:visibility="@{!item.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pre_shared_key_label"
tools:text="8VyS8W8XeMcBWfKp1GuG3/fZlnUQFkqMNbrdmZtVQIM=" />
<TextView
android:id="@+id/allowed_ips_label"
android:layout_width="match_parent"
@@ -66,15 +93,22 @@
android:layout_marginTop="8dp"
android:labelFor="@+id/allowed_ips_text"
android:text="@string/allowed_ips"
android:visibility="@{item.allowedIps.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/public_key_text" />
app:layout_constraintTop_toBottomOf="@+id/pre_shared_key_text" />
<TextView
android:id="@+id/allowed_ips_text"
style="@style/DetailText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/allowed_ips"
android:nextFocusUp="@id/pre_shared_key_text"
android:nextFocusDown="@id/endpoint_text"
android:nextFocusForward="@id/endpoint_text"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{item.allowedIps}"
android:visibility="@{item.allowedIps.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/allowed_ips_label"
tools:text="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" />
@@ -86,6 +120,7 @@
android:layout_marginTop="8dp"
android:labelFor="@+id/endpoint_text"
android:text="@string/endpoint"
android:visibility="@{!item.endpoint.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/allowed_ips_text" />
@@ -94,11 +129,44 @@
style="@style/DetailText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/endpoint"
android:nextFocusUp="@id/allowed_ips_text"
android:nextFocusDown="@id/persistent_keepalive_text"
android:nextFocusForward="@id/persistent_keepalive_text"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{item.endpoint}"
android:visibility="@{!item.endpoint.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/endpoint_label"
tools:text="192.168.0.1:51820" />
<TextView
android:id="@+id/persistent_keepalive_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/persistent_keepalive_text"
android:text="@string/persistent_keepalive"
android:visibility="@{!item.persistentKeepalive.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/endpoint_text" />
<TextView
android:id="@+id/persistent_keepalive_text"
style="@style/DetailText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/persistent_keepalive"
android:nextFocusUp="@id/endpoint_text"
android:nextFocusDown="@id/transfer_text"
android:nextFocusForward="@id/transfer_text"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{@plurals/persistent_keepalive_seconds_unit(item.persistentKeepalive.orElse(0), item.persistentKeepalive.orElse(0))}"
android:visibility="@{!item.persistentKeepalive.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/persistent_keepalive_label"
tools:text="every 3 seconds" />
<TextView
android:id="@+id/transfer_label"
android:layout_width="match_parent"
@@ -109,7 +177,7 @@
android:text="@string/transfer"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/endpoint_text"
app:layout_constraintTop_toBottomOf="@+id/persistent_keepalive_text"
tools:visibility="visible" />
<TextView
@@ -118,6 +186,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/transfer_label"
android:contentDescription="@string/transfer"
android:nextFocusUp="@id/persistent_keepalive_text"
android:onClick="@{ClipboardUtils::copyTextView}"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/transfer_label"
@@ -45,11 +45,7 @@
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="16dp"
app:cardBackgroundColor="?attr/elevationOverlayColor"
app:cardCornerRadius="0dp"
app:cardElevation="2dp"
app:contentPadding="8dp">
android:layout_marginBottom="16dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
@@ -80,7 +76,10 @@
android:id="@+id/interface_name_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
android:inputType="textNoSuggestions|textVisiblePassword"
android:nextFocusDown="@id/private_key_text"
android:nextFocusForward="@id/private_key_text"
android:text="@={name}"
app:filter="@{NameInputFilter.newInstance()}" />
</com.google.android.material.textfield.TextInputLayout>
@@ -102,7 +101,11 @@
android:id="@+id/private_key_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
android:inputType="textNoSuggestions|textPassword"
android:nextFocusUp="@id/interface_name_text"
android:nextFocusDown="@id/public_key_text"
android:nextFocusForward="@id/public_key_text"
android:onClick="@{fragment::onKeyClick}"
android:text="@={config.interface.privateKey}"
app:filter="@{KeyInputFilter.newInstance()}"
@@ -115,11 +118,12 @@
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:hint="@string/public_key"
app:expandedHintEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/private_key_text_layout">
<com.wireguard.android.widget.MonkeyedTextInputEditText
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/public_key_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -127,6 +131,10 @@
android:ellipsize="end"
android:focusable="false"
android:hint="@string/hint_generated"
android:imeOptions="actionNext"
android:nextFocusUp="@id/private_key_text"
android:nextFocusDown="@id/addresses_label_text"
android:nextFocusForward="@id/addresses_label_text"
android:onClick="@{ClipboardUtils::copyTextView}"
android:singleLine="true"
android:text="@{config.interface.publicKey}" />
@@ -148,7 +156,11 @@
android:id="@+id/addresses_label_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
android:inputType="textNoSuggestions|textVisiblePassword"
android:nextFocusUp="@id/public_key_text"
android:nextFocusDown="@id/dns_servers_text"
android:nextFocusForward="@id/listen_port_text"
android:text="@={config.interface.addresses}" />
</com.google.android.material.textfield.TextInputLayout>
@@ -158,16 +170,22 @@
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:hint="@string/listen_port"
app:expandedHintEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="0.3"
app:layout_constraintStart_toEndOf="@id/addresses_label_layout"
app:layout_constraintTop_toBottomOf="@id/public_key_label_layout">
<com.wireguard.android.widget.MonkeyedTextInputEditText
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/listen_port_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_random"
android:imeOptions="actionNext"
android:inputType="number"
android:nextFocusUp="@id/public_key_text"
android:nextFocusDown="@id/mtu_text"
android:nextFocusForward="@id/dns_servers_text"
android:text="@={config.interface.listenPort}"
android:textAlignment="center" />
</com.google.android.material.textfield.TextInputLayout>
@@ -189,7 +207,11 @@
android:id="@+id/dns_servers_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
android:inputType="textNoSuggestions|textVisiblePassword"
android:nextFocusUp="@id/addresses_label_text"
android:nextFocusDown="@id/set_excluded_applications"
android:nextFocusForward="@id/mtu_text"
android:text="@={config.interface.dnsServers}" />
</com.google.android.material.textfield.TextInputLayout>
@@ -199,17 +221,22 @@
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:hint="@string/mtu"
app:expandedHintEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="0.3"
app:layout_constraintStart_toEndOf="@id/dns_servers_label_layout"
app:layout_constraintTop_toBottomOf="@id/addresses_label_layout">
<com.wireguard.android.widget.MonkeyedTextInputEditText
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/mtu_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_automatic"
android:imeOptions="actionDone"
android:inputType="number"
android:nextFocusUp="@id/listen_port_text"
android:nextFocusDown="@id/set_excluded_applications"
android:nextFocusForward="@id/set_excluded_applications"
android:text="@={config.interface.mtu}"
android:textAlignment="center" />
</com.google.android.material.textfield.TextInputLayout>
@@ -220,18 +247,23 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:onClick="@{fragment::onRequestSetExcludedApplications}"
android:text="@{@plurals/set_excluded_applications(config.interface.excludedApplications.size, config.interface.excludedApplications.size)}"
android:nextFocusUp="@id/dns_servers_text"
android:nextFocusDown="@id/peers_layout"
android:nextFocusForward="@id/peers_layout"
android:onClick="@{fragment::onRequestSetExcludedIncludedApplications}"
android:text="@{config.interface.includedApplications.size > 0 ? @plurals/set_included_applications(config.interface.includedApplications.size, config.interface.includedApplications.size) : config.interface.excludedApplications.size > 0 ? @plurals/set_excluded_applications(config.interface.excludedApplications.size, config.interface.excludedApplications.size) : @string/all_applications}"
android:textColor="?attr/colorSecondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/mtu_label_layout"
app:rippleColor="?attr/colorSecondary" />
app:rippleColor="?attr/colorSecondary"
tools:text="4 excluded applications" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<LinearLayout
android:id="@+id/peers_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="@null"
@@ -242,6 +274,7 @@
tools:ignore="UselessLeaf" />
<com.google.android.material.button.MaterialButton
android:id="@+id/add_peer_button"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+34 -9
View File
@@ -8,6 +8,8 @@
<import type="com.wireguard.android.widget.KeyInputFilter" />
<import type="com.wireguard.android.databinding.BindingAdapters" />
<variable
name="collection"
type="androidx.databinding.ObservableList&lt;com.wireguard.android.viewmodel.PeerProxy&gt;" />
@@ -27,11 +29,7 @@
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp"
app:cardBackgroundColor="?attr/elevationOverlayColor"
app:cardCornerRadius="0dp"
app:cardElevation="2dp"
app:contentPadding="8dp">
android:layout_marginBottom="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
@@ -53,6 +51,8 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@null"
android:nextFocusDown="@id/public_key_text"
android:nextFocusForward="@id/public_key_text"
android:onClick="@{() -> item.unbind()}"
android:padding="8dp"
android:src="@drawable/ic_action_delete"
@@ -75,7 +75,11 @@
android:id="@+id/public_key_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
android:inputType="textNoSuggestions|textVisiblePassword"
android:nextFocusUp="@id/delete"
android:nextFocusDown="@id/pre_shared_key_text"
android:nextFocusForward="@id/pre_shared_key_text"
android:text="@={item.publicKey}"
app:filter="@{KeyInputFilter.newInstance()}" />
</com.google.android.material.textfield.TextInputLayout>
@@ -86,16 +90,21 @@
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:hint="@string/pre_shared_key"
app:expandedHintEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/public_key_label_layout">
<com.wireguard.android.widget.MonkeyedTextInputEditText
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/pre_shared_key_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_optional"
android:imeOptions="actionNext"
android:inputType="textNoSuggestions|textPassword"
android:nextFocusUp="@id/public_key_text"
android:nextFocusDown="@id/persistent_keepalive_text"
android:nextFocusForward="@id/persistent_keepalive_text"
android:onClick="@{fragment::onKeyClick}"
android:text="@={item.preSharedKey}"
app:filter="@{KeyInputFilter.newInstance()}"
@@ -108,16 +117,22 @@
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:hint="@string/persistent_keepalive"
app:expandedHintEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/pre_shared_key_label_layout">
app:layout_constraintTop_toBottomOf="@id/pre_shared_key_label_layout"
app:suffixText="@{@plurals/persistent_keepalive_seconds_suffix(BindingAdapters.tryParseInt(item.persistentKeepalive))}">
<com.wireguard.android.widget.MonkeyedTextInputEditText
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/persistent_keepalive_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_optional_discouraged"
android:imeOptions="actionNext"
android:inputType="number"
android:nextFocusUp="@id/persistent_keepalive_text"
android:nextFocusDown="@id/endpoint_text"
android:nextFocusForward="@id/endpoint_text"
android:text="@={item.persistentKeepalive}" />
</com.google.android.material.textfield.TextInputLayout>
@@ -135,7 +150,11 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:imeOptions="actionNext"
android:inputType="textNoSuggestions|textVisiblePassword"
android:nextFocusUp="@id/persistent_keepalive_text"
android:nextFocusDown="@id/allowed_ips_text"
android:nextFocusForward="@id/allowed_ips_text"
android:text="@={item.endpoint}" />
</com.google.android.material.textfield.TextInputLayout>
@@ -154,17 +173,23 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:imeOptions="actionDone"
android:inputType="textNoSuggestions|textVisiblePassword"
android:nextFocusUp="@id/endpoint_text"
android:nextFocusDown="@id/selected_checkbox"
android:nextFocusForward="@id/selected_checkbox"
android:text="@={item.allowedIps}" />
</com.google.android.material.textfield.TextInputLayout>
<CheckBox
android:id="@+id/excluded_checkbox"
android:id="@+id/selected_checkbox"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginStart="4dp"
android:layout_marginTop="0dp"
android:checked="@={item.excludingPrivateIps}"
android:nextFocusDown="@id/add_peer_button"
android:nextFocusForward="@id/add_peer_button"
android:text="@string/exclude_private_ips"
android:visibility="@{item.ableToExcludePrivateIps ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toEndOf="parent"
@@ -31,8 +31,9 @@
android:id="@+id/tunnel_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:choiceMode="multipleChoiceModal"
android:clipToPadding="false"
android:nextFocusDown="@id/create_fab"
android:nextFocusForward="@id/create_fab"
android:paddingBottom="@{@dimen/design_fab_size_normal * 1.1f}"
android:visibility="@{tunnels.size() > 0 ? android.view.View.VISIBLE : android.view.View.GONE}"
app:configurationHandler="@{rowConfigurationHandler}"
@@ -66,14 +67,14 @@
android:textSize="20sp" />
</LinearLayout>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/create_fab"
style="@style/Widget.MaterialComponents.ExtendedFloatingActionButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
app:icon="@drawable/ic_action_add_white" />
android:nextFocusUp="@id/tunnel_list"
app:srcCompat="@drawable/ic_action_add_white" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
+156
View File
@@ -0,0 +1,156 @@
<?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="android.view.View" />
<import type="com.wireguard.android.model.ObservableTunnel" />
<import type="com.wireguard.android.activity.TvMainActivity.KeyedFile" />
<variable
name="isDeleting"
type="androidx.databinding.ObservableBoolean" />
<variable
name="files"
type="com.wireguard.android.databinding.ObservableKeyedArrayList&lt;String, KeyedFile&gt;" />
<variable
name="filesRoot"
type="androidx.databinding.ObservableField&lt;String&gt;" />
<variable
name="tunnels"
type="com.wireguard.android.databinding.ObservableKeyedArrayList&lt;String, ObservableTunnel&gt;" />
<variable
name="tunnelRowConfigurationHandler"
type="com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler" />
<variable
name="filesRowConfigurationHandler"
type="com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.card.MaterialCardView
android:id="@+id/banner_logo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginStart="5dp"
android:layout_marginTop="5dp"
app:cardElevation="2dp"
app:contentPadding="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:layout_width="320dp"
android:layout_height="67dp"
android:contentDescription="@string/app_name"
android:scaleType="fitXY"
app:srcCompat="@drawable/tv_logo_banner" />
</com.google.android.material.card.MaterialCardView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/tunnel_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="16dp"
android:orientation="horizontal"
android:visibility="@{(tunnels.isEmpty || !filesRoot.isEmpty) ? View.GONE : View.VISIBLE}"
app:configurationHandler="@{tunnelRowConfigurationHandler}"
app:items="@{tunnels}"
app:layout="@{@layout/tv_tunnel_list_item}"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintBottom_toTopOf="@id/delete_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/banner_logo"
app:spanCount="3"
tools:itemCount="10"
tools:listitem="@layout/tv_tunnel_list_item" />
<TextView
android:id="@+id/files_root_label"
style="@style/TextAppearance.MaterialComponents.Headline5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginStart="8dp"
android:text="@{filesRoot}"
android:visibility="@{filesRoot.isEmpty ? View.GONE : View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/banner_logo"
tools:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/files_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="16dp"
android:orientation="horizontal"
android:visibility="@{filesRoot.isEmpty ? View.GONE : View.VISIBLE}"
app:configurationHandler="@{filesRowConfigurationHandler}"
app:items="@{files}"
app:layout="@{@layout/tv_file_list_item}"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintBottom_toTopOf="@id/import_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/files_root_label"
app:spanCount="5"
tools:itemCount="10"
tools:listitem="@layout/tv_file_list_item"
tools:visibility="gone" />
<TextView
style="@style/TextAppearance.MaterialComponents.Headline4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/tv_add_tunnel_get_started"
android:visibility="@{(filesRoot.isEmpty &amp;&amp; tunnels.isEmpty) ? View.VISIBLE : View.GONE}"
app:layout_constraintBottom_toTopOf="@id/delete_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/banner_logo"
tools:visibility="gone" />
<com.google.android.material.button.MaterialButton
android:id="@+id/import_button"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:minWidth="0dp"
android:visibility="@{isDeleting ? View.GONE : View.VISIBLE}"
app:icon="@{filesRoot.isEmpty ? @drawable/ic_action_add_white : @drawable/ic_arrow_back}"
app:iconPadding="0dp"
app:iconTint="?attr/colorOnPrimary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/delete_button"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:minWidth="0dp"
android:visibility="@{((tunnels.isEmpty &amp;&amp; !isDeleting) || !filesRoot.isEmpty) ? View.GONE : View.VISIBLE}"
app:icon="@{isDeleting ? @drawable/ic_arrow_back : @drawable/ic_action_delete}"
app:iconPadding="0dp"
app:iconTint="?attr/colorOnPrimary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
@@ -0,0 +1,46 @@
<?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.activity.TvMainActivity.KeyedFile" />
<variable
name="key"
type="String" />
<variable
name="item"
type="KeyedFile" />
</data>
<com.google.android.material.card.MaterialCardView
android:layout_width="320dp"
android:layout_height="50dp"
android:layout_margin="8dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="0dp"
android:backgroundTint="@color/tv_card_background"
android:checkable="true"
android:focusable="true"
app:contentPadding="8dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.textview.MaterialTextView
style="@style/TextAppearance.MaterialComponents.Headline5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{key}"
android:textColor="?attr/colorOnPrimary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</layout>

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