Compare commits

...

135 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
110 changed files with 7175 additions and 1489 deletions
+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" />
+5 -1
View File
@@ -22,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"
}
```
+56 -27
View File
@@ -1,33 +1,24 @@
allprojects {
repositories {
google()
jcenter()
}
}
buildscript {
ext {
agpVersion = '4.0.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'
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.3.0'
coroutinesVersion = '1.3.7'
eddsaVersion = '0.3.0'
fragmentVersion = '1.2.5'
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.72'
materialComponentsVersion = '1.2.0-alpha06'
mavenPluginVersion = '2.1'
junitVersion = '4.13.1'
kotlinVersion = '1.4.21'
lifecycleRuntimeKtxVersion = '2.3.0-beta01'
materialComponentsVersion = '1.3.0-beta01'
preferenceVersion = '1.1.1'
streamsupportVersion = '1.7.2'
threetenabpVersion = '1.2.4'
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,15 +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.5.1"
gradleVersion = "6.7.1"
distributionType = Wrapper.DistributionType.ALL
distributionSha256Sum = "143a28f54f1ae93ef4f72d862dbc3c438050d81bb45b4601eb7076e998362920"
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 -2
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=143a28f54f1ae93ef4f72d862dbc3c438050d81bb45b4601eb7076e998362920
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-all.zip
distributionSha256Sum=22449f5231796abd892c98b2a07c9ceebe4688d192cd2d6763f8e3bf8acbedeb
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+1 -1
View File
@@ -130,7 +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 {})"' \;
+3 -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
}
@@ -47,16 +46,14 @@ 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,
@@ -34,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";
@@ -44,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;
}
@@ -65,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) {
@@ -75,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();
@@ -124,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);
@@ -167,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);
@@ -255,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();
}
@@ -293,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,7 +143,7 @@ public final class ToolsInstaller {
extract();
final StringBuilder script = new StringBuilder("set -ex; ");
script.append("trap 'rm -rf /data/adb/moduleswireguard' INT TERM EXIT; ");
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; ");
@@ -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#][^#]*)");
@@ -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
@@ -223,9 +221,7 @@ 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())
@@ -339,11 +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(Lists.of(Attribute.split(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
+3 -3
View File
@@ -20,11 +20,11 @@ export GOARCH := $(NDK_GO_ARCH_MAP_$(ANDROID_ARCH_NAME))
export GOOS := android
export CGO_ENABLED := 1
GO_VERSION := 1.14.4
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 := 3fa7ed8dc44fdd50c0bfe72676250cceca527d59950aef20af906a670cf88de2
GO_HASH_linux-amd64 := aed845e4185a0b2a3c3d5e1d0a35491702c55889192bb9c30e67a3de6849c067
GO_HASH_darwin-amd64 := 9bd39600d9fa1fa4a5ccce8761d249f7421cffe671376f791293c4138f3d7c62
GO_HASH_linux-amd64 := b49fda1ca29a1946d6bb2a5a6982cf07ccd2aba849289508ee0f9918f6bb4552
default: $(DESTDIR)/libwg-go.so
+2 -2
View File
@@ -46,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)
@@ -91,6 +90,7 @@ func wgTurnOn(ifnameRef string, tunFd int32, settings string) int32 {
logger.Error.Println(setError)
return -1
}
device.DisableSomeRoamingForBrokenMobileSemantics()
var uapi net.Listener
@@ -172,7 +172,7 @@ func wgGetSocketV6(tunnelHandle int32) int32 {
if bind == nil {
return -1
}
fd, err := bind.PeekLookAtSocketFd4()
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.14
go 1.15
require (
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 // indirect
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 // indirect
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980
golang.zx2c4.com/wireguard v0.0.20200321-0.20200622004228-b84f1d4db25e
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 -14
View File
@@ -1,20 +1,24 @@
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/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-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
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-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/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.20200321-0.20200622004228-b84f1d4db25e h1:f8BS3yEMeIGx/zzJfihxDRedx6lT7EiJlfih4j6LY98=
golang.zx2c4.com/wireguard v0.0.20200321-0.20200622004228-b84f1d4db25e/go.mod h1:GJvYs5O24/ASlwPiRklVnjMx2xQzrOic0DuU6GvYJL4=
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
+19 -20
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
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 {
@@ -66,6 +57,7 @@ android {
dependencies {
implementation project(":tunnel")
implementation "androidx.activity:activity-ktx:$activityVersion"
implementation "androidx.annotation:annotation:$annotationsVersion"
implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion"
@@ -73,17 +65,24 @@ dependencies {
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
}
}
+22 -3
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"
@@ -113,7 +124,15 @@
android:value="false" />
</service>
<meta-data android:name="android.content.APP_RESTRICTIONS"
<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) {
}
}
}
@@ -10,10 +10,9 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.ActionBar
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
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
@@ -59,16 +58,6 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener
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.main_activity_container).setOnApplyWindowInsetsListener { _, insets ->
supportFragmentManager.addOnBackStackChangedListener {
supportFragmentManager.fragments.forEach {
ViewCompat.dispatchApplyWindowInsets(it.requireView(), WindowInsetsCompat.toWindowInsetsCompat(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,55 +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())
}
}
}
@@ -65,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) {
@@ -98,8 +67,8 @@ class SettingsActivity : ThemeChangeAwareActivity() {
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 {
@@ -115,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,8 +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.
@@ -158,7 +158,7 @@ object BindingAdapters {
return 0
return try {
Integer.parseInt(s)
} catch (_: Exception) {
} catch (_: Throwable) {
0
}
}
@@ -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.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.requireTargetFragment
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?) {
@@ -82,23 +87,22 @@ class AddTunnelsSheet : BottomSheetDialogFragment() {
}
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,24 +4,27 @@
*/
package com.wireguard.android.fragment
import android.Manifest
import android.app.Dialog
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 androidx.fragment.app.setFragmentResult
import androidx.lifecycle.lifecycleScope
import com.google.android.material.tabs.TabLayout
import com.wireguard.android.Application
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 com.wireguard.android.util.requireTargetFragment
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class AppListDialogFragment : DialogFragment() {
private val appData = ObservableKeyedArrayList<String, ApplicationData>()
@@ -33,40 +36,42 @@ class AppListDialogFragment : DialogFragment() {
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
val appData = ApplicationData(it.loadIcon(pm), it.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()
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 })
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()
}
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)
require(requireTargetFragment() is AppSelectionListener) { "${requireTargetFragment()} must implement AppSelectionListener" }
currentlySelectedApps = (arguments?.getStringArrayList(KEY_SELECTED_APPS) ?: emptyList())
initiallyExcluded = arguments?.getBoolean(KEY_IS_EXCLUDED) ?: true
}
@@ -123,23 +128,22 @@ class AppListDialogFragment : DialogFragment() {
selectedApps.add(data.packageName)
}
}
(requireTargetFragment() as AppSelectionListener).onSelectedAppsSelected(selectedApps, tabs?.selectedTabPosition == 0)
setFragmentResult(REQUEST_SELECTION, bundleOf(
KEY_SELECTED_APPS to selectedApps.toTypedArray(),
KEY_IS_EXCLUDED to (tabs?.selectedTabPosition == 0)
))
dismiss()
}
interface AppSelectionListener {
fun onSelectedAppsSelected(selectedApps: List<String>, isExcluded: Boolean)
}
companion object {
private const val KEY_SELECTED_APPS = "selected_apps"
private const val KEY_IS_EXCLUDED = "is_excluded"
fun <T> newInstance(selectedApps: ArrayList<String?>?, isExcluded: Boolean, target: T): AppListDialogFragment where T : Fragment?, T : AppSelectionListener? {
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_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,46 +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.AppSelectionListener
import com.wireguard.android.model.ObservableTunnel
import com.wireguard.android.util.BiometricAuthenticator
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(), AppSelectionListener {
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()
}
}
@@ -76,8 +78,6 @@ class TunnelEditorFragment : BaseFragment(), AppSelectionListener {
binding = TunnelEditorFragmentBinding.inflate(inflater, container, false)
binding?.apply {
executePendingBindings()
setUpRoot(root as ViewGroup)
setUpScrollingContent(mainContainer, null)
privateKeyTextLayout.setEndIconOnClickListener { config?.`interface`?.generateKeyPair() }
}
return binding?.root
@@ -89,23 +89,6 @@ class TunnelEditorFragment : BaseFragment(), AppSelectionListener {
super.onDestroyView()
}
override fun onSelectedAppsSelected(selectedApps: List<String>, isExcluded: Boolean) {
requireNotNull(binding) { "Tried to set excluded/included apps while no view was loaded" }
if (isExcluded) {
binding!!.config!!.`interface`.includedApplications.clear()
binding!!.config!!.`interface`.excludedApplications.apply {
clear()
addAll(selectedApps)
}
} else {
binding!!.config!!.`interface`.excludedApplications.clear()
binding!!.config!!.`interface`.includedApplications.apply {
clear()
addAll(selectedApps)
}
}
}
private fun onFinished() {
// Hide the keyboard; it rarely goes away on its own.
val activity = activity ?: return
@@ -115,14 +98,11 @@ class TunnelEditorFragment : BaseFragment(), AppSelectionListener {
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 {
@@ -130,7 +110,7 @@ class TunnelEditorFragment : BaseFragment(), AppSelectionListener {
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)
@@ -138,20 +118,36 @@ class TunnelEditorFragment : BaseFragment(), AppSelectionListener {
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
@@ -169,8 +165,26 @@ class TunnelEditorFragment : BaseFragment(), AppSelectionListener {
if (selectedApps.isNotEmpty())
isExcluded = false
}
val fragment = AppListDialogFragment.newInstance(selectedApps, isExcluded, this)
fragment.show(parentFragmentManager, null)
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)
}
}
@@ -187,46 +201,60 @@ class TunnelEditorFragment : BaseFragment(), AppSelectionListener {
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()
}
}
@@ -4,13 +4,9 @@
*/
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
@@ -19,33 +15,29 @@ 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.
@@ -54,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) {
@@ -178,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
}
@@ -226,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
@@ -293,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 {
@@ -315,26 +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)
binding?.createFab?.apply {
visibility = View.VISIBLE
scaleX = 1f
scaleY = 1f
}
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)
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)
}
@@ -423,8 +301,6 @@ class TunnelListFragment : BaseFragment() {
}
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"
}
@@ -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.
@@ -30,10 +32,12 @@ class ObservableTunnel internal constructor(
@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
@@ -57,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
@@ -94,16 +109,26 @@ class ObservableTunnel internal constructor(
var statistics: Statistics? = null
get() {
if (field == null || field?.isStale != false)
manager.getTunnelStatistics(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.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 != false)
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
@@ -112,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,17 +4,26 @@
*/
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) {
@@ -22,8 +31,8 @@ class KernelModuleDisablerPreference(context: Context, attrs: AttributeSet?) : P
init {
isVisible = false
Application.getBackendAsync().thenAccept { backend ->
setState(if (backend is WgQuickBackend) State.ENABLED else State.DISABLED)
lifecycleScope.launch {
setState(if (Application.getBackend() is WgQuickBackend) State.ENABLED else State.DISABLED)
}
}
@@ -31,26 +40,29 @@ class KernelModuleDisablerPreference(context: Context, attrs: AttributeSet?) : P
override fun getTitle() = if (state == State.UNKNOWN) "" else 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()
}
Application.getAsyncWorker().runAsync {
Application.getTunnelManager().tunnels.thenApply { observableTunnels ->
val downings = observableTunnels.map { it.setStateAsync(Tunnel.State.DOWN).toCompletableFuture() }.toTypedArray()
CompletableFuture.allOf(*downings).thenRun {
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))
}
}
}
@@ -69,4 +81,8 @@ class KernelModuleDisablerPreference(context: Context, attrs: AttributeSet?) : P
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,22 +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.AdminKnobs
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
@@ -29,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
}
}
}
@@ -84,22 +83,17 @@ class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference
override fun onClick() {
if (AdminKnobs.disableConfigExport) return
val prefActivity = FragmentUtils.getPrefActivity(this)
val fragment = prefActivity.supportFragmentManager.fragments.first()
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()
@@ -12,5 +12,6 @@ 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
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,7 +8,11 @@ package com.wireguard.android.util
import android.content.Context
import android.util.TypedValue
import androidx.annotation.AttrRes
import androidx.fragment.app.Fragment
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()
@@ -16,6 +20,12 @@ fun Context.resolveAttribute(@AttrRes attrRes: Int): Int {
return typedValue.data
}
fun Fragment.requireTargetFragment(): Fragment {
return requireNotNull(targetFragment) { "A target fragment should always be set for $this" }
}
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
}
}
}
@@ -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.FloatingActionButton
/**
* 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: FloatingActionButton?) {
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: FloatingActionButton) {
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
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:duration="300"
android:fromXScale="1.0"
+1 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:duration="300"
android:fromXScale="0"
@@ -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>
@@ -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"
@@ -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>
@@ -62,6 +62,8 @@
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"
@@ -83,6 +85,9 @@
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"
@@ -107,6 +112,9 @@
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}"
@@ -131,6 +139,9 @@
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}"
@@ -155,6 +166,9 @@
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}"
@@ -169,7 +183,7 @@
android:layout_marginTop="8dp"
android:labelFor="@+id/listen_port_text"
android:text="@string/listen_port"
android:visibility="@{config.interface.listenPort.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
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"
@@ -181,9 +195,13 @@
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.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
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"
@@ -197,7 +215,7 @@
android:layout_marginTop="8dp"
android:labelFor="@+id/mtu_text"
android:text="@string/mtu"
android:visibility="@{config.interface.mtu.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
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"
@@ -210,9 +228,12 @@
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.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
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"
@@ -237,6 +258,9 @@
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}"
+21 -6
View File
@@ -48,6 +48,8 @@
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}"
@@ -62,7 +64,7 @@
android:layout_marginTop="8dp"
android:labelFor="@+id/pre_shared_key_text"
android:text="@string/pre_shared_key"
android:visibility="@{item.preSharedKey.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
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" />
@@ -74,9 +76,12 @@
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.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
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=" />
@@ -98,6 +103,9 @@
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}"
@@ -112,7 +120,7 @@
android:layout_marginTop="8dp"
android:labelFor="@+id/endpoint_text"
android:text="@string/endpoint"
android:visibility="@{item.endpoint.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
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" />
@@ -122,9 +130,12 @@
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.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
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" />
@@ -136,7 +147,7 @@
android:layout_marginTop="8dp"
android:labelFor="@+id/persistent_keepalive_text"
android:text="@string/persistent_keepalive"
android:visibility="@{item.persistentKeepalive.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
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" />
@@ -146,9 +157,12 @@
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.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
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" />
@@ -173,6 +187,7 @@
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"
@@ -76,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>
@@ -98,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()}"
@@ -111,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"
@@ -123,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}" />
@@ -144,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>
@@ -154,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>
@@ -185,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>
@@ -195,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>
@@ -216,6 +247,9 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="4dp"
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"
@@ -223,11 +257,13 @@
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"
@@ -238,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"
+31 -5
View File
@@ -51,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"
@@ -73,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>
@@ -84,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()}"
@@ -106,18 +117,23 @@
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:hint="@string/persistent_keepalive"
app:suffixText="@{@plurals/persistent_keepalive_seconds_suffix(BindingAdapters.tryParseInt(item.persistentKeepalive))}"
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:text="@={item.persistentKeepalive}"/>
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>
<com.google.android.material.textfield.TextInputLayout
@@ -134,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>
@@ -153,7 +173,11 @@
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>
@@ -164,6 +188,8 @@
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}"
@@ -72,6 +73,7 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
android:nextFocusUp="@id/tunnel_list"
app:srcCompat="@drawable/ic_action_add_white" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
+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>
@@ -0,0 +1,81 @@
<?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.backend.Tunnel.State" />
<variable
name="isDeleting"
type="androidx.databinding.ObservableBoolean" />
<variable
name="isFocused"
type="androidx.databinding.ObservableBoolean" />
<variable
name="key"
type="String" />
<variable
name="item"
type="com.wireguard.android.model.ObservableTunnel" />
</data>
<com.google.android.material.card.MaterialCardView
android:layout_width="225dp"
android:layout_height="110dp"
android:layout_margin="8dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="0dp"
android:backgroundTint="@{(item.state == State.UP &amp;&amp; !isDeleting) ? @color/secondary_dark_color : (isDeleting &amp;&amp; isFocused) ? @color/tv_card_delete_background : @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
android:id="@+id/tunnel_name"
style="@style/TextAppearance.MaterialComponents.Headline5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{item.name}"
android:textColor="?attr/colorOnPrimary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@sample/interface_names.json/names/names/name" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tunnel_transfer"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{isDeleting ? View.GONE : View.VISIBLE}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="rx: 200 MB, tx: 100 MB" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tunnel_delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/tv_delete"
android:visibility="@{(isDeleting &amp;&amp; isFocused) ? View.VISIBLE : View.GONE}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</layout>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="delete_success">
<item quantity="zero">تم حذف نفق %d بنجاح</item>
<item quantity="one">تم حذف %d نفق بنجاح</item>
<item quantity="two">تم حذف %d نفق بنجاح</item>
<item quantity="few">تم حذف %d نفق بنجاح</item>
<item quantity="many">تم حذف %d نفق بنجاح</item>
<item quantity="other">تم حذف %d انفاق بنجاح</item>
</plurals>
<plurals name="delete_title">
<item quantity="zero">%d نفق محدد</item>
<item quantity="one">%d نفق محدد</item>
<item quantity="two">%d نفق محدد</item>
<item quantity="few">%d عنصر محدد</item>
<item quantity="many">%d عنصر محدد</item>
<item quantity="other">%d عنصر محدد</item>
</plurals>
</resources>
+98
View File
@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="delete_error">
<item quantity="one">No s\'ha pogut esborrar %d túnel: %s</item>
<item quantity="other">No s\'han pogut esborrar %d túnels: %s</item>
</plurals>
<plurals name="delete_title">
<item quantity="one">%d túnel seleccionat</item>
<item quantity="other">%d túnels seleccionats</item>
</plurals>
<plurals name="n_excluded_applications">
<item quantity="one">%d exclòs</item>
<item quantity="other">%d exclosos</item>
</plurals>
<plurals name="n_included_applications">
<item quantity="one">%d inclòs</item>
<item quantity="other">%d inclosos</item>
</plurals>
<string name="all_applications">Totes les aplicacions</string>
<string name="exclude_from_tunnel">Exclou</string>
<string name="include_in_tunnel">Inclou només</string>
<plurals name="include_n_applications">
<item quantity="one">Inclou %d aplicació</item>
<item quantity="other">Inclou %d aplicacions</item>
</plurals>
<plurals name="exclude_n_applications">
<item quantity="one">Exclou %d aplicació</item>
<item quantity="other">Exclou %d aplicacions</item>
</plurals>
<plurals name="persistent_keepalive_seconds_unit">
<item quantity="one">cada segon</item>
<item quantity="other">cada %d segons</item>
</plurals>
<plurals name="persistent_keepalive_seconds_suffix">
<item quantity="one">segon</item>
<item quantity="other">segons</item>
</plurals>
<string name="add_peer">Afegir parell</string>
<string name="addresses">Adreces</string>
<string name="applications">Aplicacions</string>
<string name="allowed_ips">IPs permeses</string>
<string name="bad_config_explanation_positive_number">: Ha de ser positiu</string>
<string name="bad_config_reason_invalid_key">Clau no vàlida</string>
<string name="bad_config_reason_invalid_number">Número no vàlid</string>
<string name="bad_config_reason_invalid_value">Valor no vàlid</string>
<string name="bad_config_reason_syntax_error">Error de sintaxi</string>
<string name="bad_config_reason_unknown_attribute">Atribut desconegut</string>
<string name="bad_config_reason_unknown_section">Secció desconeguda</string>
<string name="cancel">Cancel·la</string>
<string name="create_activity_title">Crear túnel WireGuard</string>
<string name="create_empty">Crea des de zero</string>
<string name="create_from_file">Importa des de fitxer o arxiu</string>
<string name="create_from_qr_code">Escaneja codi QR</string>
<string name="create_tunnel">Crear túnel</string>
<string name="dark_theme_title">Utilitza tema fosc</string>
<string name="delete">Elimina</string>
<string name="dns_servers">Servidors DNS</string>
<string name="edit">Edita</string>
<string name="exclude_private_ips">Exclou IPs privades</string>
<string name="generate_new_private_key">Genera nova clau privada</string>
<string name="generic_error">Error “%s” desconegut</string>
<string name="hint_generated">(generat)</string>
<string name="hint_optional">(opcional)</string>
<string name="hint_optional_discouraged">(opcional, no recomanat)</string>
<string name="hint_random">(aleatori)</string>
<string name="import_from_qr_code">Importa túnel desde codi QR</string>
<string name="import_success">Importat “%s”</string>
<string name="interface_title">Interfície</string>
<string name="key_length_error">Longitud de clau incorrecta</string>
<string name="log_export_success">Guardat a \"%s\"</string>
<string name="log_export_title">Exporta el registre</string>
<string name="log_saver_activity_label">Guarda registre</string>
<string name="log_viewer_pref_title">Mostra el registre d\'aplicació</string>
<string name="log_viewer_title">Registre</string>
<string name="multiple_tunnels_title">Permet múltiples túnels simultanis</string>
<string name="name">Nom</string>
<string name="no_tunnels_error">No existeixen túnels</string>
<string name="parse_error_inet_address">Adreça IP</string>
<string name="parse_error_inet_network">Xarxa IP</string>
<string name="parse_error_integer">número</string>
<string name="peer">Parell</string>
<string name="pre_shared_key_enabled">activat</string>
<string name="private_key">Clau privada</string>
<string name="public_key">Clau pública</string>
<string name="restore_on_boot_title">Restableix a l\'inici</string>
<string name="save">Guarda</string>
<string name="select_all">Selecciona-ho tot</string>
<string name="settings">Configuració</string>
<string name="tunnel_error_already_exists">El túnel \"%s\" ja existeix</string>
<string name="tunnel_error_invalid_name">Nom no vàlid</string>
<string name="tunnel_list_placeholder">Afegiu un túnel usant el botó blau</string>
<string name="tunnel_name">Nom del túnel</string>
<string name="unknown_error">Error desconegut</string>
<string name="version_summary_unknown">Versió de %s desconeguda</string>
<string name="zip_export_success">Guardat a \"%s\"</string>
<string name="biometric_auth_error">Error d\'autenticació</string>
<string name="biometric_auth_error_reason">Error d\'autenticació: %s</string>
</resources>
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="delete_error">
<item quantity="one">Kan ikke slette %d tunnel: %s</item>
<item quantity="other">Ikke i stand til at slette %d tunneler: %s</item>
</plurals>
<plurals name="delete_success">
<item quantity="one">Slettede %d tunnel</item>
<item quantity="other">%d tunneller blev slettet</item>
</plurals>
</resources>
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="delete_error">
<item quantity="one">%d Tunnel konnte nicht gelöscht werden: %s</item>
<item quantity="one">%d Tunnel konnten nicht gelöscht werden: %s</item>
<item quantity="other">%d Tunnel konnten nicht gelöscht werden: %s</item>
</plurals>
<plurals name="delete_success">
+62
View File
@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="delete_error">
<item quantity="one">Δεν είναι δυνατή η διαγραφή του %d tunnel: %s</item>
<item quantity="other">Δεν είναι δυνατή η διαγραφή %d tunnels: %s</item>
</plurals>
<plurals name="delete_success">
<item quantity="one">Το %d tunnel διαγράφηκε με επιτυχία</item>
<item quantity="other">Διαγράφηκαν επιτυχώς τα %d tunnels</item>
</plurals>
<plurals name="delete_title">
<item quantity="one">%d επιλεγμένο tunnel</item>
<item quantity="other">%d επιλεγμένα tunnels</item>
</plurals>
<plurals name="import_partial_success">
<item quantity="one">Έγινε εισαγωγή %1$d από %2$d tunnels</item>
<item quantity="other">Έγινε εισαγωγή %1$d από %2$d tunnels</item>
</plurals>
<plurals name="import_total_success">
<item quantity="one">Εισαγωγή tunnel %d</item>
<item quantity="other">Εισαγωγή tunnels %d</item>
</plurals>
<plurals name="set_excluded_applications">
<item quantity="one">%d Εξαιρούμενη εφαρμογή</item>
<item quantity="other">%d Εξαιρούμενες εφαρμογές</item>
</plurals>
<plurals name="set_included_applications">
<item quantity="one">%d Συμπεριλαμβανόμενη εφαρμογή</item>
<item quantity="other">%d Συμπεριλαμβανόμενες εφαρμογές</item>
</plurals>
<plurals name="n_excluded_applications">
<item quantity="one">εξαιρέθηκε %d</item>
<item quantity="other">εξαιρέθηκε %d</item>
</plurals>
<plurals name="n_included_applications">
<item quantity="one">περιλαμβάνεται %d</item>
<item quantity="other">περιλαμβάνεται %d</item>
</plurals>
<string name="all_applications">Όλες οι εφαρμογές</string>
<string name="exclude_from_tunnel">Εξαίρεση</string>
<string name="include_in_tunnel">Συμπεριλάβετε μόνο</string>
<plurals name="include_n_applications">
<item quantity="one">Συμπερίληψη %d app</item>
<item quantity="other">Συμπερίληψη %d apps</item>
</plurals>
<plurals name="exclude_n_applications">
<item quantity="one">Εξαίρεση %d app</item>
<item quantity="other">Εξαίρεση %d apps</item>
</plurals>
<plurals name="persistent_keepalive_seconds_unit">
<item quantity="one">κάθε δευτερόλεπτο</item>
<item quantity="other">κάθε %d δευτερόλεπτα</item>
</plurals>
<plurals name="persistent_keepalive_seconds_suffix">
<item quantity="one">δευτερόλεπτο</item>
<item quantity="other">δευτερόλεπτα</item>
</plurals>
<string name="use_all_applications">Χρησιμοποίησε όλες τις εφαρμογές</string>
<string name="add_peer">Προσθήκη peer</string>
<string name="addresses">Διευθύνσεις</string>
<string name="applications">Εφαρμογές</string>
</resources>
+180
View File
@@ -0,0 +1,180 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="n_excluded_applications">
<item quantity="one">%d excluido</item>
<item quantity="other">%d excluidos</item>
</plurals>
<string name="all_applications">Todas las Aplicaciones</string>
<string name="exclude_from_tunnel">Excluir</string>
<string name="include_in_tunnel">Sólo inclusión</string>
<string name="use_all_applications">Usar todas las aplicaciones</string>
<string name="add_peer">Añadir par</string>
<string name="addresses">Direcciones</string>
<string name="applications">Aplicaciones</string>
<string name="allow_remote_control_intents_summary_off">Las aplicaciones externas no pueden cambiar túneles (recomendado)</string>
<string name="allow_remote_control_intents_summary_on">Las aplicaciones externas pueden cambiar túneles (avanzado)</string>
<string name="allow_remote_control_intents_title">Permitir aplicaciones de mando remoto</string>
<string name="allowed_ips">IPs permitidas</string>
<string name="bad_config_context">%1$s de %2$s</string>
<string name="bad_config_context_top_level">%s</string>
<string name="bad_config_error">%1$s en %2$s</string>
<string name="bad_config_explanation_pka">: Debe ser positivo y no más de 65535</string>
<string name="bad_config_explanation_positive_number">: Debe ser positivo</string>
<string name="bad_config_explanation_udp_port">: Debe ser un número válido de puerto UDP</string>
<string name="bad_config_reason_invalid_key">Clave inválida</string>
<string name="bad_config_reason_invalid_number">Número inválido</string>
<string name="bad_config_reason_invalid_value">Valor inválido</string>
<string name="bad_config_reason_missing_attribute">Falta atributo</string>
<string name="bad_config_reason_missing_section">Sección faltante</string>
<string name="bad_config_reason_syntax_error">Error de sintaxis</string>
<string name="bad_config_reason_unknown_attribute">Atributo desconocido</string>
<string name="bad_config_reason_unknown_section">Sección desconocida</string>
<string name="bad_config_reason_value_out_of_range">Valor fuera de rango</string>
<string name="bad_extension_error">El archivo debe ser .conf o .zip</string>
<string name="cancel">Cancelar</string>
<string name="config_delete_error">No se puede eliminar el archivo de configuración %s</string>
<string name="config_exists_error">La configuración para “%s” ya existe</string>
<string name="config_file_exists_error">El archivo de configuración “%s” ya existe</string>
<string name="config_not_found_error">Archivo de configuración “%s” no encontrado</string>
<string name="config_rename_error">No se puede renombrar el archivo de configuración “%s”</string>
<string name="config_save_error">No se puede guardar la configuración para “%1$s”: %2$s</string>
<string name="config_save_success">Configuración guardada correctamente para “%s”</string>
<string name="create_activity_title">Crear túnel WireGuard</string>
<string name="create_bin_dir_error">No se puede crear el directorio binario local</string>
<string name="create_downloads_file_error">No se puede crear archivo en el directorio de descargas</string>
<string name="create_empty">Crear de cero</string>
<string name="create_from_file">Importar desde archivo</string>
<string name="create_from_qr_code">Escanear desde código QR</string>
<string name="create_output_dir_error">No se puede crear el directorio de salida</string>
<string name="create_temp_dir_error">No se puede crear la carpeta temporal</string>
<string name="create_tunnel">Crear túnel</string>
<string name="dark_theme_summary_off">Actualmente usando tema claro (día)</string>
<string name="dark_theme_summary_on">Actualmente usando tema oscuro (noche)</string>
<string name="dark_theme_title">Usar tema oscuro</string>
<string name="delete">Eliminar</string>
<string name="dns_servers">Servidores DNS</string>
<string name="edit">Editar</string>
<string name="endpoint">Punto final</string>
<string name="error_down">Error al bajar el túnel: %s</string>
<string name="error_fetching_apps">Error al obtener la lista de aplicaciones: %s</string>
<string name="error_root">Por favor, obtén acceso root y vuelve a intentarlo</string>
<string name="error_up">Error al abrir el túnel: %s</string>
<string name="exclude_private_ips">Excluir direcciones privadas</string>
<string name="generate_new_private_key">Generar nueva clave privada</string>
<string name="generic_error">Error desconocido “%s”</string>
<string name="hint_automatic">(auto)</string>
<string name="hint_generated">(generado)</string>
<string name="hint_optional">(opcional)</string>
<string name="hint_optional_discouraged">(opcional, no recomendado)</string>
<string name="hint_random">(aleatorio)</string>
<string name="illegal_filename_error">Nombre de archivo no válido “%s”</string>
<string name="import_error">No se puede importar túnel: %s</string>
<string name="import_from_qr_code">Importar túnel desde código QR</string>
<string name="import_success">Importado “%s”</string>
<string name="interface_title">Interfaz</string>
<string name="key_contents_error">Caracteres incorrectos en la clave</string>
<string name="key_length_error">Longitud de clave incorrecta</string>
<string name="key_length_explanation_base64">Las claves base64 de WireGuard deben tener 44 caracteres (32 bytes)</string>
<string name="key_length_explanation_binary">: Las claves WireGuard deben tener 32 bytes</string>
<string name="key_length_explanation_hex">: Las claves hexadecimales de Wirex deben tener 64 caracteres (32 bytes)</string>
<string name="listen_port">Puerto de escucha</string>
<string name="log_export_error">No se pudo exportar el registro: %s</string>
<string name="log_export_subject">Archivo de registro WireGuard Android</string>
<string name="log_export_success">Guardado en “%s”</string>
<string name="log_export_title">Exportar archivo de registro</string>
<string name="log_saver_activity_label">Guardar registro</string>
<string name="log_viewer_pref_summary">Los registros pueden ayudar con la depuración</string>
<string name="log_viewer_pref_title">Ver registro de aplicación</string>
<string name="log_viewer_title">Registro</string>
<string name="logcat_error">No se puede ejecutar logcat: </string>
<string name="module_disabler_disabled_summary">El módulo experimental del kernel puede mejorar el rendimiento</string>
<string name="module_disabler_disabled_title">Habilitar backend del módulo del kernel</string>
<string name="module_disabler_enabled_summary">El backend más lento del espacio de usuario puede mejorar la estabilidad</string>
<string name="module_disabler_enabled_title">Desactivar backend del módulo del kernel</string>
<string name="module_installer_error">Ocurrió un error. Intente de nuevo</string>
<string name="module_installer_initial">El módulo experimental del kernel puede mejorar el rendimiento</string>
<string name="module_installer_not_found">No hay módulos disponibles para tu dispositivo</string>
<string name="module_installer_title">Descargar e instalar el módulo del kernel</string>
<string name="module_installer_working">Descargando e instalando…</string>
<string name="module_version_error">No se puede determinar la versión del módulo del kernel</string>
<string name="mtu">MTU</string>
<string name="multiple_tunnels_summary_off">Activar un túnel apagará los demás</string>
<string name="multiple_tunnels_summary_on">Múltiples túneles pueden ser activados simultáneamente</string>
<string name="multiple_tunnels_title">Permitir múltiples túneles simultáneos</string>
<string name="name">Nombre</string>
<string name="no_config_error">Intentando abrir un túnel sin configuración</string>
<string name="no_configs_error">No se encontraron configuraciones</string>
<string name="no_tunnels_error">No existen túneles</string>
<string name="parse_error_generic">cadena</string>
<string name="parse_error_inet_address">Dirección IP</string>
<string name="parse_error_inet_endpoint">punto final</string>
<string name="parse_error_inet_network">Red IP</string>
<string name="parse_error_integer">número</string>
<string name="parse_error_reason">No se puede analizar %1$s “%2$s”</string>
<string name="peer">Pares</string>
<string name="permission_description">controlar túneles de WireGuard, habilitando y desactivando túneles a su antojo, lo que podría conducir mal al tráfico de Internet</string>
<string name="permission_label">controlar túneles de WireGuard</string>
<string name="persistent_keepalive">Mantenimiento persistente</string>
<string name="pre_shared_key">Clave precompartida</string>
<string name="pre_shared_key_enabled">activado</string>
<string name="private_key">Clave privada</string>
<string name="public_key">Clave pública</string>
<string name="qr_code_hint">Consejo: generar con `qrencode -t ansiutf8 &lt; tunnel.conf`.</string>
<string name="restore_on_boot_summary_off">No mostrará túneles habilitados al arrancar</string>
<string name="restore_on_boot_summary_on">Mostrará túneles habilitados al arrancar</string>
<string name="restore_on_boot_title">Restaurar al arrancar</string>
<string name="save">Guardar</string>
<string name="select_all">Seleccionar todo</string>
<string name="settings">Preferencias</string>
<string name="shell_exit_status_read_error">Shell no puede leer estado de salida</string>
<string name="shell_marker_count_error">Shell esperaba 4 marcadores, recibió %d</string>
<string name="shell_start_error">No se pudo iniciar Shell: %d</string>
<string name="success_application_will_restart">Éxito. La aplicación se reiniciará ahora…</string>
<string name="toggle_all">Cambiar Todos</string>
<string name="toggle_error">Error al cambiar el túnel WireGuard: %s</string>
<string name="tools_installer_already">wg y wg-quick ya están instalados</string>
<string name="tools_installer_failure">No se puede instalar herramientas de línea de comandos (sin root?)</string>
<string name="tools_installer_initial">Instalar herramientas opcionales para el scripting</string>
<string name="tools_installer_initial_magisk">Instalar herramientas opcionales para el scripting como módulo Magisk</string>
<string name="tools_installer_initial_system">Instalar herramientas opcionales para el scripting en la partición del sistema</string>
<string name="tools_installer_success_magisk">wg y wg-quick instalados como un módulo Magisk (requiere reinicio)</string>
<string name="tools_installer_success_system">wg y wg-quick instalados en la partición del sistema</string>
<string name="tools_installer_title">Instalar herramientas de línea de comandos</string>
<string name="tools_installer_working">Instalando wg y wg-quick</string>
<string name="tools_unavailable_error">Herramientas requeridas no disponibles</string>
<string name="transfer">Transferir</string>
<string name="transfer_bytes">%d B</string>
<string name="transfer_gibibytes">%.2f GiB</string>
<string name="transfer_kibibytes">%.2f KiB</string>
<string name="transfer_mibibytes">%.2f MiB</string>
<string name="transfer_rx_tx">rx: %1$s, tx: %2$s</string>
<string name="transfer_tibibytes">%.2f TiB</string>
<string name="tun_create_error">No se puede crear el dispositivo túnel</string>
<string name="tunnel_config_error">No se puede configurar el túnel (wg-quick devuelto %d)</string>
<string name="tunnel_create_error">No se puede crear el dispositivo túnel: %s</string>
<string name="tunnel_create_success">Túnel creado con éxito “%s”</string>
<string name="tunnel_error_already_exists">Túnel “%s” ya existe</string>
<string name="tunnel_error_invalid_name">Nombre inválido</string>
<string name="tunnel_list_placeholder">Añadir un túnel usando el botón azul</string>
<string name="tunnel_name">Nombre del túnel</string>
<string name="tunnel_on_error">No se puede activar el túnel (wgTurnOn devolvió %d)</string>
<string name="tunnel_rename_error">No se puede renombrar túnel: %s</string>
<string name="tunnel_rename_success">Túnel renombrado con éxito a “%s”</string>
<string name="type_name_go_userspace">Ir al espacio de usuario</string>
<string name="type_name_kernel_module">Módulo Kernel</string>
<string name="unknown_error">Error desconocido</string>
<string name="version_summary">%1$s backend v%2$s</string>
<string name="version_summary_checking">Comprobando versión de backend %s</string>
<string name="version_summary_unknown">Versión %s desconocida</string>
<string name="version_title">WireGuard para Android -%s</string>
<string name="vpn_not_authorized_error">Servicio VPN no autorizado por el usuario</string>
<string name="vpn_start_error">No se puede iniciar el servicio VPN Android</string>
<string name="zip_export_error">No se pueden exportar túneles: %s</string>
<string name="zip_export_success">Guardado en “%s”</string>
<string name="zip_export_summary">El archivo Zip se guardará en la carpeta de descargas</string>
<string name="zip_export_title">Exportar túneles a archivo zip</string>
<string name="biometric_prompt_zip_exporter_title">Autenticar para exportar túneles</string>
<string name="biometric_prompt_private_key_title">Autenticar para ver la clave privada</string>
<string name="biometric_auth_error">Error de autenticación</string>
<string name="biometric_auth_error_reason">Error de autenticación: %s</string>
</resources>
+212
View File
@@ -0,0 +1,212 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="delete_error">
<item quantity="one">حذف %d تونل‌ امکان‌پذیر نیست: %s</item>
<item quantity="other">حذف %d تونل‌ها امکان‌پذیر نیست: %s</item>
</plurals>
<plurals name="delete_success">
<item quantity="one">%d تونل با موقیت حذف شد</item>
<item quantity="other">%d تونل‌ها با موقیت حذف شدند</item>
</plurals>
<plurals name="delete_title">
<item quantity="one">%d تونل انتخاب شد</item>
<item quantity="other">%d تونل‌ها انتخاب شدند</item>
</plurals>
<plurals name="import_partial_success">
<item quantity="one">%1$d از %2$d تونل اضافه شد</item>
<item quantity="other">%1$d از %2$d تونل اضافه شد</item>
</plurals>
<plurals name="import_total_success">
<item quantity="one">%d تونل اضافه شد</item>
<item quantity="other">%d از تونل اضافه شد</item>
</plurals>
<plurals name="set_excluded_applications">
<item quantity="one">%d برنامه استثنا</item>
<item quantity="other">%d برنامه‌های استثنا</item>
</plurals>
<plurals name="set_included_applications">
<item quantity="one">%d برنامه مشمول</item>
<item quantity="other">%d برنامه‌های مشمول</item>
</plurals>
<plurals name="n_excluded_applications">
<item quantity="one">%d استثنا</item>
<item quantity="other">%d استثناها</item>
</plurals>
<plurals name="n_included_applications">
<item quantity="one">شامل %d</item>
<item quantity="other">شامل %d</item>
</plurals>
<string name="all_applications">همه‌ برنامه‌ها</string>
<string name="exclude_from_tunnel">جدا کردن</string>
<string name="include_in_tunnel">تنها شامل</string>
<plurals name="include_n_applications">
<item quantity="one">شامل %d برنامه</item>
<item quantity="other">شامل %d برنامه</item>
</plurals>
<plurals name="exclude_n_applications">
<item quantity="one">جداکردن %d برنامه</item>
<item quantity="other">جداکردن %d برنامه</item>
</plurals>
<plurals name="persistent_keepalive_seconds_unit">
<item quantity="one">هر ثانیه</item>
<item quantity="other">هر %d ثانیه</item>
</plurals>
<plurals name="persistent_keepalive_seconds_suffix">
<item quantity="one">ثانیه</item>
<item quantity="other">ثانیه</item>
</plurals>
<string name="use_all_applications">از همه برنامه‌ها استفاده کن</string>
<string name="add_peer">افزودن همتا</string>
<string name="addresses">نشانی‌ها</string>
<string name="applications">برنامه‌ها</string>
<string name="allow_remote_control_intents_summary_off">برنامه‌های بیرونی تونل ها را عوض نکنند
(توصیه می‌شود)</string>
<string name="allow_remote_control_intents_summary_on">برنامه‌های بیرونی تونل‌ها را عوض کنند (پیشرفته)</string>
<string name="allow_remote_control_intents_title">اجازه به برنامه‌های کنترل از راه‌دور</string>
<string name="allowed_ips">IPهای مجاز</string>
<string name="app_name">WireGuard</string>
<string name="bad_config_context">%1$s\'s %2$s</string>
<string name="bad_config_context_top_level">%s</string>
<string name="bad_config_error">%1$s در %2$s</string>
<string name="bad_config_explanation_pka">: باید مثبت و بیشتر از ۶۵۵۳۵ نباشد</string>
<string name="bad_config_explanation_positive_number">: باید مثبت باشد</string>
<string name="bad_config_explanation_udp_port">: باید یک شماره پورت UDP معتبر باشد</string>
<string name="bad_config_reason_invalid_key">کلید نامعتبر است</string>
<string name="bad_config_reason_invalid_number">شماره نامعتبر است</string>
<string name="bad_config_reason_invalid_value">مقدار نامعتبر است</string>
<string name="bad_config_reason_missing_attribute">مشخصه موجود نیست</string>
<string name="bad_config_reason_missing_section">بخش موجود نیست</string>
<string name="bad_config_reason_syntax_error">خطای نحوی</string>
<string name="bad_config_reason_unknown_attribute">مشخصهٔ نامعلوم</string>
<string name="bad_config_reason_unknown_section">بخش نامعلوم</string>
<string name="bad_config_reason_value_out_of_range">مقدار خارج از محدوده</string>
<string name="bad_extension_error">پرونده باید .conf یا .zip باشد</string>
<string name="cancel">لغو</string>
<string name="config_delete_error">نمی‌توان پرونده پیکربندی %s را حذف کرد</string>
<string name="config_exists_error">پیکربندی برای ”%s” در حال حاضر وجود دارد</string>
<string name="config_file_exists_error">فایل پیکربندی ”%s” در حال حاضر وجود دارد</string>
<string name="config_not_found_error">پرونده پیکربندی “%s” یافت نشد</string>
<string name="config_rename_error">نمی‌توان نام پرونده پیکربندی “%s” را تغییر داد</string>
<string name="config_save_error">نمی‌توان پیکربندی برای “%1$s”: %2$s را ذخیره کرد</string>
<string name="config_save_success">پیکربندی برای “%s” با موفقیت ذخیره شد</string>
<string name="create_activity_title">ساخت تونل WireGuard</string>
<string name="create_bin_dir_error">نمی‌توان دایرکتوری باینری محلی را ایجاد کرد</string>
<string name="create_downloads_file_error">نمی‌توان در مسیر بارگیری پرونده‌ای ساخت</string>
<string name="create_empty">ساختن از ابتدا</string>
<string name="create_from_file">واردکردن از طریق پرونده یا آرشیو</string>
<string name="create_from_qr_code">اسکن از کد QR</string>
<string name="create_output_dir_error">نمی‌توان دایرکتوری خروجی را ایجاد کرد</string>
<string name="create_temp_dir_error">نمی‌توان دایرکتوری موقت محلی را ساخت</string>
<string name="create_tunnel">ساختن تونل</string>
<string name="dark_theme_summary_off">اکنون از پوسته روشن(روز) استفاده می‌شود</string>
<string name="dark_theme_summary_on">اکنون از پوسته تاریک(شب) استفاده می‌شود</string>
<string name="dark_theme_title">استفاده از پوسته تاریک</string>
<string name="delete">حذف</string>
<string name="dns_servers">سرورهای DNS</string>
<string name="edit">ویرایش</string>
<string name="endpoint">نقطه پایان</string>
<string name="error_down">خطا هنگام بستن تونل: %s</string>
<string name="error_fetching_apps">خطا هنگام واکشی فهرست برنامه‌ها: %s</string>
<string name="error_root">لطفا دسترسی روت را فراهم‌کرده و دوباره تلاش کنید</string>
<string name="error_up">خطا هنگام راه‌اندازی تونل: %s</string>
<string name="exclude_private_ips">مستثنی کردن IPهای خصوصی</string>
<string name="generate_new_private_key">تولید کلید خصوصی جدید</string>
<string name="generic_error">خطای “%s” ناشناخته</string>
<string name="hint_automatic">(خودکار)</string>
<string name="hint_generated">(تولید شده)</string>
<string name="hint_optional">(دلخواه)</string>
<string name="hint_optional_discouraged">(اختیاری، پیشنهاد نمی‌شود)</string>
<string name="hint_random">(تصادفی)</string>
<string name="illegal_filename_error">نام پرونده “%s” غیرمجاز است</string>
<string name="import_error">نمی‌توان تونل را وارد کرد: %s</string>
<string name="import_from_qr_code">وارد کردن تونل از کد QR</string>
<string name="import_success">“%s” وارد شد</string>
<string name="interface_title">رابط</string>
<string name="key_contents_error">در کلید نویسه‌های بد وجود دارد</string>
<string name="key_length_error">طول کلید نادرست است</string>
<string name="key_length_explanation_base64">: کلیدهای WireGuard base64 باید دارای ۴۴ نویسه باشند ( ۳۲ بایت)</string>
<string name="key_length_explanation_binary">: کلیدهای WireGuard باید ۳۲ بایت باشند</string>
<string name="key_length_explanation_hex">: کلیدهای هگز WireGuard باید دارای ۶۴ نویسه باشند ( ۳۲ بایت)</string>
<string name="listen_port">شنود پورت</string>
<string name="log_export_error">نمی‌توان گزارش رویداد را برون‌برد: %s</string>
<string name="log_export_subject">پرونده گزارش رویداد WireGuard اندروید</string>
<string name="log_export_success">ذخیره شد در “%s”</string>
<string name="log_export_title">برون‌برد پرونده گزارش رویداد</string>
<string name="log_saver_activity_label">ذخیره گزارش رویداد</string>
<string name="log_viewer_pref_summary">گزارش رویداد شاید به اشکال زدایی کمک کند</string>
<string name="log_viewer_pref_title">نمایش گزارش رویداد برنامه</string>
<string name="log_viewer_title">گزارش رویداد</string>
<string name="logcat_error">نمی‌توان logcat را اجرا کرد: </string>
<string name="module_disabler_disabled_summary">ماژول آزمایشی‌ِ کرنل می تواند کارایی را افزایش دهد</string>
<string name="module_disabler_disabled_title">فعال‌سازی ماژول کرنل ِبک اند</string>
<string name="module_disabler_enabled_title">غیرفعال‌سازی پس‌زمینه واحد هسته</string>
<string name="module_installer_error">مشکلی پیش آمد. لطفا دوباره تلاش کنید</string>
<string name="module_installer_not_found">هیچ واحدی برای دستگاه شما در دسترس نیست</string>
<string name="module_installer_title">واحد هسته را بارگیری و نصب کن</string>
<string name="module_installer_working">در حال بارگیری و نصب…</string>
<string name="module_version_error">نمی‌توان نگارش واحد هسته را مشخص کرد</string>
<string name="mtu">MTU</string>
<string name="multiple_tunnels_summary_off">روشن کردن یک تونل ، تونل های دیگر را خاموش خواهد کرد</string>
<string name="name">نام</string>
<string name="no_config_error">تلاش برای فعالسازی تونل بدون تنظیمات</string>
<string name="no_configs_error">هیچ پیکربندی یافت نشد</string>
<string name="no_tunnels_error">هیچ تونلی وجود ندارد</string>
<string name="parse_error_generic">رشته</string>
<string name="parse_error_inet_address">نشانی IP</string>
<string name="parse_error_inet_endpoint">نقطه پایان</string>
<string name="parse_error_inet_network">شبکه IP</string>
<string name="parse_error_integer">شماره</string>
<string name="parse_error_reason">نمی‌توان %1$s “%2$s” تجزیه کرد</string>
<string name="peer">همتا</string>
<string name="permission_description">کنترل تونل های وایرگارد، فعال و غیرفعال کردن تونل ها، و حتی تغییر مسیر ترافیک اینترنت</string>
<string name="permission_label">کنترل تونل‌های WireGuard</string>
<string name="persistent_keepalive">زنده نگه‌داشتن پیوسته</string>
<string name="pre_shared_key">کلید از پیش تقسیم شده</string>
<string name="pre_shared_key_enabled">فعال شده</string>
<string name="private_key">کلید خصوصی</string>
<string name="public_key">کلید عمومی</string>
<string name="restore_on_boot_summary_off">تونل های فعال در لحظه بالا آمدن سیستم، روشن نخواهند شد</string>
<string name="restore_on_boot_summary_on">تونل های فعال در لحظه بالا آمدن سیستم، روشن خواهند شد</string>
<string name="restore_on_boot_title">بازگردانی در بوت</string>
<string name="save">ذخیره</string>
<string name="select_all">انتخاب همه</string>
<string name="settings">تنظیمات</string>
<string name="shell_start_error">آغاز پوسته شکست خورد: %d</string>
<string name="success_application_will_restart">موفقیت. برنامه اکنون دوباره راه‌اندازی خواهد شد…</string>
<string name="toggle_all">معکوس کردن همه</string>
<string name="tools_installer_title">ابزارهای خط فرمان را نصب کنید</string>
<string name="tools_installer_working">در حال نصب wg و wg-quick</string>
<string name="tools_unavailable_error">ابزارهای لازم در دسترس نیست</string>
<string name="transfer">انتقال</string>
<string name="transfer_bytes">%d B</string>
<string name="transfer_gibibytes">%.2f GiB</string>
<string name="transfer_kibibytes">%.2f KiB</string>
<string name="transfer_mibibytes">%.2f MiB</string>
<string name="transfer_rx_tx">rx: %1$s, tx: %2$s</string>
<string name="transfer_tibibytes">%.2f TiB</string>
<string name="tunnel_create_error">نمی‌توان تونل را ساخت: %s</string>
<string name="tunnel_create_success">تونل “%s” با موفقیت ساخته شد</string>
<string name="tunnel_error_already_exists">تونل “%s” از قبل وجود دارد</string>
<string name="tunnel_error_invalid_name">نام نامعتبر</string>
<string name="tunnel_list_placeholder">به‌وسیله دکمه آبی یک تونل بیفزایید</string>
<string name="tunnel_name">نام تونل</string>
<string name="tunnel_rename_error">ناتوان در تغییر نام تونل: %s</string>
<string name="tunnel_rename_success">نام تونل با موفقیت تغییر یافت به “%s”</string>
<string name="type_name_go_userspace">رفتن به فضای کاربر</string>
<string name="type_name_kernel_module">واحد هسته</string>
<string name="unknown_error">خطای نامشخص</string>
<string name="version_summary">%1$s پس‌زمینه نگارش%2$s</string>
<string name="version_summary_checking">در حال بررسی نگارش پس‌زمینه %s</string>
<string name="version_summary_unknown">نگارش %s ناشناخته</string>
<string name="version_title">WireGuard برای اندروید نگارش %s</string>
<string name="vpn_not_authorized_error">کاربر به سرویس VPN اجازه نداد</string>
<string name="vpn_start_error">نمی‌توان سرویس VPN اندروید را آغاز کرد</string>
<string name="zip_export_error">نمی‌توان تونل‌ها را برون‎برد: %s</string>
<string name="zip_export_success">ذخیره شد در “%s”</string>
<string name="zip_export_summary">پرونده زیپ در پوشه بارگیری‌ها ذخیره خواهد شد</string>
<string name="zip_export_title">برون‌بری تونل‌ها در پرونده زیپ</string>
<string name="biometric_prompt_zip_exporter_title">برای برون‌بری تونل‌ها، هویت خود را تایید کنید</string>
<string name="biometric_prompt_private_key_title">برای دیدن کلید خصوصی، هویت خود را تایید کنید</string>
<string name="biometric_auth_error">شکست در تایید هویت</string>
<string name="biometric_auth_error_reason">شکست در تایید هویت: %s</string>
</resources>
+64
View File
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="persistent_keepalive_seconds_unit">
<item quantity="one">joka sekunti</item>
<item quantity="other">%d sekunnin välein</item>
</plurals>
<plurals name="persistent_keepalive_seconds_suffix">
<item quantity="one">sekunti</item>
<item quantity="other">sekuntia</item>
</plurals>
<string name="use_all_applications">Käytä kaikkia sovelluksia</string>
<string name="add_peer">Lisää toinen osapuoli</string>
<string name="addresses">Osoitteet</string>
<string name="applications">Sovellukset</string>
<string name="allowed_ips">Sallitut IP-osoitteet</string>
<string name="app_name">WireGuard</string>
<string name="bad_config_context_top_level">%s</string>
<string name="bad_config_reason_invalid_key">Virheellinen avain</string>
<string name="bad_config_reason_invalid_number">Virheellinen luku</string>
<string name="bad_config_reason_invalid_value">Virheellinen arvo</string>
<string name="bad_config_reason_missing_attribute">Attribuutti puuttuu</string>
<string name="bad_config_reason_missing_section">Osio puuttuu</string>
<string name="bad_config_reason_syntax_error">Syntaksivirhe</string>
<string name="bad_config_reason_unknown_attribute">Tuntematon määrite</string>
<string name="bad_config_reason_unknown_section">Tuntematon osio</string>
<string name="bad_config_reason_value_out_of_range">Arvo alueen ulkopuolella</string>
<string name="bad_extension_error">Tiedoston on oltava .conf tai .zip</string>
<string name="cancel">Peruuta</string>
<string name="config_not_found_error">Asetustiedostoa “%s” ei löydy</string>
<string name="config_rename_error">Asetustiedostoa \"%s\" ei voi nimetä uudelleen</string>
<string name="config_save_success">Asetustiedosto \"%s\" tallennettu onnistuneesti</string>
<string name="create_activity_title">Luo WireGuard Tunnel</string>
<string name="exclude_private_ips">Jätä pois yksityiset IP-osoitteet</string>
<string name="generate_new_private_key">Luo uusi yksityinen avain</string>
<string name="generic_error">Tuntematon ”%s” virhe</string>
<string name="hint_automatic">(oletus)</string>
<string name="hint_generated">(generoitu)</string>
<string name="hint_optional">(valinnainen)</string>
<string name="hint_optional_discouraged">(valinnainen, ei suositeltava)</string>
<string name="hint_random">(satunnainen)</string>
<string name="illegal_filename_error">Virheellinen tiedostonimi “%s”</string>
<string name="import_error">Tunnelia \"%s\" ei voitu tuoda</string>
<string name="import_from_qr_code">Tuo tunneli QR-koodista</string>
<string name="import_success">Tuotu ”%s”</string>
<string name="key_contents_error">Virheellinen merkki avaimessa</string>
<string name="key_length_error">Virheellinen avaimen pituus</string>
<string name="key_length_explanation_base64">: WireGuardin base64-avainten pituus on oltava 44 merkkiä (32 tavua)</string>
<string name="key_length_explanation_binary">: WireGuard-avainten on oltava 32 tavua</string>
<string name="key_length_explanation_hex">: WireGuardin base64-avainten pituus on oltava 64 merkkiä (32 tavua)</string>
<string name="listen_port">Kuuntele porttia</string>
<string name="mtu">MTU</string>
<string name="no_configs_error">Asetuksia ei löydy</string>
<string name="no_tunnels_error">Tunneleita ei ole</string>
<string name="parse_error_generic">merkkijono</string>
<string name="parse_error_inet_address">IP-osoite</string>
<string name="parse_error_inet_network">IP-verkko</string>
<string name="transfer_bytes">%d B</string>
<string name="transfer_gibibytes">%.2f GiB</string>
<string name="transfer_kibibytes">%.2f KiB</string>
<string name="transfer_mibibytes">%.2f MiB</string>
<string name="transfer_rx_tx">rx: %1$s, tx: %2$s</string>
<string name="transfer_tibibytes">%.2f TiB</string>
<string name="tunnel_name">Tunnelin nimi</string>
</resources>
+2 -2
View File
@@ -5,7 +5,7 @@
<item quantity="other">Impossible de supprimer %d tunnels : %s</item>
</plurals>
<plurals name="delete_success">
<item quantity="one">Supprimé avec succès %d tunnel</item>
<item quantity="one">Suppression réussie du tunnel %d</item>
<item quantity="other">Supprimé avec succès %d tunnels</item>
</plurals>
<plurals name="delete_title">
@@ -191,7 +191,7 @@
<string name="tools_installer_title">Installer les outils de ligne de commande</string>
<string name="tools_installer_working">Installation de wg et wg-quick</string>
<string name="tools_unavailable_error">Outils requis indisponibles</string>
<string name="transfer">Transférer</string>
<string name="transfer">Données transférées</string>
<string name="transfer_bytes">%d Octets</string>
<string name="transfer_gibibytes">%.2f Go</string>
<string name="transfer_kibibytes">%.2f Ko</string>
+220
View File
@@ -0,0 +1,220 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="delete_error">
<item quantity="one">%d टनल हटाने में असमर्थ: %s</item>
<item quantity="other">%d टनलस को हटाने में असमर्थ: %s</item>
</plurals>
<plurals name="delete_success">
<item quantity="one">%d टनल को सफलतापूर्वक हटा दिया गया</item>
<item quantity="other">%d टनलस को सफलतापूर्वक हटा दिया गया</item>
</plurals>
<plurals name="delete_title">
<item quantity="one">%d टनल चयनित</item>
<item quantity="other">%d टनलस का चयन किया गया</item>
</plurals>
<plurals name="import_partial_success">
<item quantity="one">आयातित %d %d टनल</item>
<item quantity="other">आयातित %d %d टनलस</item>
</plurals>
<plurals name="import_total_success">
<item quantity="one">आयातित %d टनल</item>
<item quantity="other">आयातित %d टनलस</item>
</plurals>
<plurals name="set_excluded_applications">
<item quantity="one">%d बहिष्कृत अनुप्रयोग</item>
<item quantity="other">%d बहिष्कृत अनुप्रयोग</item>
</plurals>
<plurals name="set_included_applications">
<item quantity="one">%d ऐप्स शामिल</item>
<item quantity="other">%d ऐप्स शामिल किये गए</item>
</plurals>
<plurals name="n_excluded_applications">
<item quantity="one">%d अपवर्जित</item>
<item quantity="other">%d अपवर्जित</item>
</plurals>
<plurals name="n_included_applications">
<item quantity="one">%d शामिल</item>
<item quantity="other">%d शामिल</item>
</plurals>
<string name="all_applications">सभी एप्लीकेशन</string>
<string name="exclude_from_tunnel">वर्जित</string>
<string name="include_in_tunnel">केवल शामिल करें</string>
<plurals name="include_n_applications">
<item quantity="one">%d ऐप शामिल करें</item>
<item quantity="other">%d ऐप्स शामिल करें</item>
</plurals>
<plurals name="exclude_n_applications">
<item quantity="one">%d ऐप को बाहर करें</item>
<item quantity="other">%d ऐप्स को बाहर करें</item>
</plurals>
<plurals name="persistent_keepalive_seconds_unit">
<item quantity="one">हर सेकंड</item>
<item quantity="other">हर %d सेकंड्‌स</item>
</plurals>
<plurals name="persistent_keepalive_seconds_suffix">
<item quantity="one">सेकंड</item>
<item quantity="other">सेकंड्‌स</item>
</plurals>
<string name="use_all_applications">सभी ऐप्स का उपयोग करें</string>
<string name="add_peer">पीयर जोड़ें</string>
<string name="addresses">एड्रेससैस</string>
<string name="applications">ऍप्लिकेशन्स</string>
<string name="allow_remote_control_intents_summary_off">बाहरी ऐप्स टनल्स को चालू नहीं कर सकते (अनुशंसित)</string>
<string name="allow_remote_control_intents_summary_on">बाहरी ऐप्स टनल्स को चालू कर सकते है (एडवांस्ड)</string>
<string name="allow_remote_control_intents_title">रिमोट कंट्रोल ऐप्स की अनुमति दें</string>
<string name="allowed_ips">अनुमत आईपी</string>
<string name="app_name">WireGuard</string>
<string name="bad_config_explanation_pka">: सकारात्मक होना चाहिए और 65535 से अधिक नहीं होना चाहिए</string>
<string name="bad_config_explanation_positive_number">: सकारात्मक होना चाहिए</string>
<string name="bad_config_explanation_udp_port">: एक वैध यूडीपी पोर्ट नंबर होना चाहिए</string>
<string name="bad_config_reason_invalid_key">अमान्य चाबी</string>
<string name="bad_config_reason_invalid_number">अमान्य संख्या</string>
<string name="bad_config_reason_invalid_value">अमान्य मूल्य</string>
<string name="bad_config_reason_missing_attribute">गुम विशेषता</string>
<string name="bad_config_reason_missing_section">छूटा हुआ भाग</string>
<string name="bad_config_reason_syntax_error">वक्य रचना त्रुटि</string>
<string name="bad_config_reason_unknown_attribute">अज्ञात एट्रिब्यूट</string>
<string name="bad_config_reason_unknown_section">अज्ञात एट्रिब्यूट </string>
<string name="bad_config_reason_value_out_of_range">मूल्य सीमा से बाहर</string>
<string name="bad_extension_error">फ़ाइल .conf या .zip होनी चाहिए</string>
<string name="cancel">रद्द</string>
<string name="config_delete_error">कॉन्फ़िगरेशन फ़ाइल %s को नहीं हटा सकता</string>
<string name="config_exists_error">“%s” के लिए कॉन्फ़िगरेशन पहले से मौजूद है</string>
<string name="config_file_exists_error">कॉन्फ़िगरेशन फ़ाइल “%s” पहले से मौजूद है</string>
<string name="config_not_found_error">कॉन्फ़िगरेशन फ़ाइल “%s” नहीं मिली</string>
<string name="config_rename_error">कॉन्फ़िगरेशन फ़ाइल “%s” का नाम नहीं बदल सकता</string>
<string name="config_save_error">“%1$s” के लिए कॉन्फ़िगरेशन को नहीं बचा सकता: %2$s</string>
<string name="config_save_success">“%s” के लिए सफलतापूर्वक सहेजा गया कॉन्फ़िगरेशन</string>
<string name="create_activity_title">वायरगार्ड टनल बनाएं</string>
<string name="create_bin_dir_error">स्थानीय बाइनरी निर्देशिका नहीं बना सकते</string>
<string name="create_downloads_file_error">डाउनलोड निर्देशिका में फ़ाइल नहीं बना सकते</string>
<string name="create_empty">शुरू से बनाएँ</string>
<string name="create_from_file">फ़ाइल या संग्रह से आयात करें</string>
<string name="create_from_qr_code">QR कोड स्कैन करें</string>
<string name="create_output_dir_error">आउटपुट निर्देशिका नहीं बना सकता</string>
<string name="create_temp_dir_error">स्थानीय अस्थायी निर्देशिका नहीं बना सकते</string>
<string name="create_tunnel">टनल बनाए</string>
<string name="dark_theme_summary_off">अभी प्रकाश (दिन) थीम का उपयोग कर रहे हैं</string>
<string name="dark_theme_summary_on">अभी डार्क (रात) थीम का उपयोग कर रहे हैं</string>
<string name="dark_theme_title">डार्क थीम का इस्तेमाल करें</string>
<string name="delete">हटाएं</string>
<string name="dns_servers">डीएनएस सर्वर</string>
<string name="edit">संपादित करें</string>
<string name="endpoint">अंतिम</string>
<string name="error_down">टनल को लाने में त्रुटि: %s</string>
<string name="error_fetching_apps">ऐप्स सूची लाने में त्रुटि: %s</string>
<string name="error_root">कृपया रूट एक्सेस प्राप्त करें और पुनः प्रयास करें</string>
<string name="error_up">टनल को लाने में त्रुटि: %s</string>
<string name="exclude_private_ips">निजी आईपी को छोड़ दें</string>
<string name="generate_new_private_key">नई प्राइवेट की उत्पन्न करें</string>
<string name="generic_error">अज्ञात “%s” त्रुटि</string>
<string name="hint_automatic">(ऑटो)</string>
<string name="hint_generated">(उत्पन्न)</string>
<string name="hint_optional">(ऐच्छिक)</string>
<string name="hint_optional_discouraged">(वैकल्पिक, अनुशंसित नहीं)</string>
<string name="hint_random">(क्रमरहित)</string>
<string name="illegal_filename_error">अवैध फ़ाइल नाम “%s”</string>
<string name="import_error">टनल को आयात करने में असमर्थ: %s</string>
<string name="import_from_qr_code">क्यूआर कोड से टनल को आयात करें</string>
<string name="import_success">आयातित “%s”</string>
<string name="interface_title">इंटरफेस</string>
<string name="key_contents_error">चाबी में खराब वर्ण</string>
<string name="key_length_error">चाबी की लम्बाई गलत </string>
<string name="key_length_explanation_base64">: वायरगार्ड बेस 64 कीज़ में 44 अक्षर (32 बाइट्स) होने चाहिए</string>
<string name="key_length_explanation_binary">: वायरगार्ड कीज 32 बाइट होनी चाहिए</string>
<string name="key_length_explanation_hex">: वायरगार्ड हेक्स कीज़ 64 अक्षरों की होनी चाहिए (32 बाइट्स)</string>
<string name="listen_port">पोर्ट सूने</string>
<string name="log_export_error">लॉग निर्यात करने में असमर्थ: %s</string>
<string name="log_export_subject">WireGuard एंड्राइड लॉग फ़ाइल</string>
<string name="log_export_success">“%s” में सहेजा गया</string>
<string name="log_export_title">लॉग फ़ाइल निर्यात करें</string>
<string name="log_saver_activity_label">लॉग सहेजे</string>
<string name="log_viewer_pref_summary">लॉग डीबगिंग में सहायता कर सकते हैं</string>
<string name="log_viewer_pref_title">एप्लिकेशन लॉग देखें</string>
<string name="log_viewer_title">लॉग</string>
<string name="logcat_error">लॉगकैट चलाने में असमर्थ: </string>
<string name="module_disabler_disabled_summary">प्रयोगात्मक कर्नेल मॉड्यूल प्रदर्शन में सुधार कर सकता है</string>
<string name="module_disabler_disabled_title">कर्नेल मॉड्यूल बैकएंड सक्षम करें</string>
<string name="module_disabler_enabled_summary">धीमे यूजरस्पेस बैकएंड में स्थिरता में सुधार हो सकता है</string>
<string name="module_disabler_enabled_title">कर्नेल मॉड्यूल बैकएंड को अक्षम करें</string>
<string name="module_installer_error">कुछ गलत हो गया। कृपया पुन: प्रयास करें</string>
<string name="module_installer_initial">प्रयोगात्मक कर्नेल मॉड्यूल प्रदर्शन में सुधार कर सकता है</string>
<string name="module_installer_not_found">आपके डिवाइस के लिए कोई मॉड्यूल उपलब्ध नहीं हैं</string>
<string name="module_installer_title">कर्नेल मॉड्यूल डाउनलोड और इंस्टॉल करें</string>
<string name="module_installer_working">डाउनलोड कर रहा है और स्थापित कर रहा है…</string>
<string name="module_version_error">कर्नेल मॉड्यूल संस्करण निर्धारित करने में असमर्थ</string>
<string name="mtu">MTU</string>
<string name="multiple_tunnels_summary_off">एक टनल को चालू करने से अन्य बंद हो जाएंगे</string>
<string name="multiple_tunnels_summary_on">एक साथ कई टनलस को चालू किया जा सकता है</string>
<string name="multiple_tunnels_title">एक साथ कई टनलस को अनुमति दें</string>
<string name="name">नाम</string>
<string name="no_config_error">बिना किसी कॉन्फ़िगरेशन के एक टनल को लाने की कोशिश करना</string>
<string name="no_configs_error">कोई कॉन्फ़िगरेशन नहीं मिला</string>
<string name="no_tunnels_error">कोई टनल मौजूद नहीं है</string>
<string name="parse_error_generic">पाठ</string>
<string name="parse_error_inet_address">आईपी पता</string>
<string name="parse_error_inet_endpoint">समाप्त</string>
<string name="parse_error_inet_network">आईपी नेटवर्क</string>
<string name="parse_error_integer">संख्या</string>
<string name="parse_error_reason">%1$s “%2$s” को पार्स नहीं कर सकता</string>
<string name="peer">पीयर</string>
<string name="permission_description">वायरगार्ड टनल्स को नियंत्रित करना, टनल्स को सक्षम और अक्षम करना, संभवतः इंटरनेट ट्रैफ़िक को गलत तरीके से अक्षम करना है</string>
<string name="permission_label">वायरगार्ड टनलस को नियंत्रित करें</string>
<string name="persistent_keepalive">लगातार जिंदा रहो</string>
<string name="pre_shared_key">प्री-शेयर्ड कीस</string>
<string name="pre_shared_key_enabled">सक्षम</string>
<string name="private_key">निजी कीस</string>
<string name="public_key">सार्वजनिक कीस</string>
<string name="qr_code_hint">सुझाव: `qrencode -t ansiutf8 &lt; tunnel.conf` के साथ उत्पन्न करो</string>
<string name="restore_on_boot_summary_off">बूट पर सक्षम टनलस को नहीं लाएगा</string>
<string name="restore_on_boot_summary_on">बूट पर सक्षम टनलस को लाएगा</string>
<string name="restore_on_boot_title">बूट पर पुनर्स्थापित करें</string>
<string name="save">सहेजें</string>
<string name="select_all">सभी का चयन करे</string>
<string name="settings">सेटिंग्स</string>
<string name="shell_exit_status_read_error">शेल बाहर निकलने की स्थिति नहीं पढ़ सकता</string>
<string name="shell_marker_count_error">शेल ने 4 मार्करों की अपेक्षा की, %d प्राप्त किया</string>
<string name="shell_start_error">शेल शुरू करने में विफल: %d</string>
<string name="success_application_will_restart">सफलता। एप्लीकेशन अब पुनः आरंभ होगा...</string>
<string name="toggle_all">सबको स्विच करे</string>
<string name="toggle_error">वायरगार्ड टनल टॉगल करने में त्रुटि: %s</string>
<string name="tools_installer_already">wg और wg-quick पहले से इंस्टॉल हैं</string>
<string name="tools_installer_failure">कमांड-लाइन टूल स्थापित करने में असमर्थ (कोई रूट नहीं)</string>
<string name="tools_installer_initial">स्क्रिप्टिंग के लिए वैकल्पिक उपकरण स्थापित करें</string>
<string name="tools_installer_initial_magisk">Magisk मॉड्यूल के रूप में स्क्रिप्टिंग के लिए वैकल्पिक उपकरण स्थापित करें</string>
<string name="tools_installer_initial_system">सिस्टम विभाजन में स्क्रिप्टिंग के लिए वैकल्पिक उपकरण स्थापित करें</string>
<string name="tools_installer_success_magisk">wg और wg-quick को मैजिक मॉड्यूल के रूप में स्थापित किया गया है (रिबूट आवश्यक)</string>
<string name="tools_installer_success_system">wg और wg-quick सिस्टम विभाजन में स्थापित है</string>
<string name="tools_installer_title">कमांड लाइन उपकरण स्थापित करें</string>
<string name="tools_installer_working">Wg और wg-quick इंस्टॉल करना</string>
<string name="tools_unavailable_error">आवश्यक उपकरण अनुपलब्ध हैं</string>
<string name="transfer">स्थानांतरण</string>
<string name="tun_create_error">ट्यून डिवाइस बनाने में असमर्थ</string>
<string name="tunnel_config_error">टनल को कॉन्फ़िगर करने में असमर्थ (wg-quick लौटा %d)</string>
<string name="tunnel_create_error">टनल बनाने में असमर्थ: %s</string>
<string name="tunnel_create_success">सफलतापूर्वक बनाया गया टनल “%s”</string>
<string name="tunnel_error_already_exists">टनल “%s” पहले से मौजूद है</string>
<string name="tunnel_error_invalid_name">गलत नाम</string>
<string name="tunnel_list_placeholder">नीले बटन का उपयोग करके एक टनल को जोड़ें</string>
<string name="tunnel_name">टनल का नाम</string>
<string name="tunnel_on_error">टनल चालू करने में असमर्थ (wgTurnOn लौटा %d)</string>
<string name="tunnel_rename_error">टनल का नाम बदलने में असमर्थ: %s</string>
<string name="tunnel_rename_success">सफलतापूर्वक टनल का नाम बदलकर “%s” कर दिया गया</string>
<string name="type_name_go_userspace">userspace पे जाए </string>
<string name="type_name_kernel_module">कर्नेल मॉड्यूल</string>
<string name="unknown_error">अज्ञात त्रुटि</string>
<string name="version_summary">%1$s बैकएंड v%2$s</string>
<string name="version_summary_checking">%s बैकएंड संस्करण की जाँच कर रहा है</string>
<string name="version_summary_unknown">अज्ञात %s संस्करण</string>
<string name="version_title">WireGuard for Android v%s</string>
<string name="vpn_not_authorized_error">वीपीएन सेवा उपयोगकर्ता द्वारा अधिकृत नहीं है</string>
<string name="vpn_start_error">एंड्रॉयड वीपीएन सेवा प्रारंभ करने में असमर्थ</string>
<string name="zip_export_error">टनल का निर्यात करने में असमर्थ: %s</string>
<string name="zip_export_success">“%s” पर सहेजा गया</string>
<string name="zip_export_summary">ज़िप फ़ाइल को डाउनलोड फ़ोल्डर में सहेजा जाएगा</string>
<string name="zip_export_title">जिप फाइल के लिए टनल को एक्सपोर्ट करें</string>
<string name="biometric_prompt_zip_exporter_title">टनल्स के निर्यात के लिए प्रमाणित करें</string>
<string name="biometric_prompt_private_key_title">प्राइवेट की देखने के लिए प्रमाणित करें</string>
<string name="biometric_auth_error">प्रमाणीकरण विफलता</string>
<string name="biometric_auth_error_reason">प्रमाणीकरण विफल: %s</string>
</resources>
+1 -1
View File
@@ -105,7 +105,7 @@
<string name="hint_random">(ランダム)</string>
<string name="illegal_filename_error">不正なファイル名 “%s”</string>
<string name="import_error">トンネル設定をインポートできません: %s</string>
<string name="import_from_qr_code">QR コードからトンネル設定をインポートできません</string>
<string name="import_from_qr_code">QR コードからトンネル設定をインポートします</string>
<string name="import_success">“%s” をインポートしました</string>
<string name="interface_title">インターフェース</string>
<string name="key_contents_error">鍵に不正な文字があります</string>

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