Compare commits

..

71 Commits

Author SHA1 Message Date
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
64 changed files with 2684 additions and 425 deletions
+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"
}
```
+47 -19
View File
@@ -1,25 +1,23 @@
buildscript {
ext {
activityVersion = '1.2.0-alpha07'
agpVersion = '4.0.1'
activityVersion = '1.2.0-beta02'
agpVersion = '4.1.1'
annotationsVersion = '1.1.0'
appcompatVersion = '1.2.0'
bintrayPluginVersion = '1.8.5'
biometricVersion = '1.1.0-alpha02'
biometricVersion = '1.1.0-rc01'
collectionVersion = '1.1.0'
constraintLayoutVersion = '2.0.1'
constraintLayoutVersion = '2.0.4'
coordinatorLayoutVersion = '1.1.0'
coreKtxVersion = '1.3.1'
coroutinesVersion = '1.3.9'
datastoreVersion = '1.0.0-alpha01'
coreKtxVersion = '1.3.2'
coroutinesVersion = '1.4.2'
datastoreVersion = '1.0.0-alpha02'
desugarVersion = '1.0.10'
fragmentVersion = '1.3.0-alpha07'
fragmentVersion = '1.3.0-beta02'
jsr305Version = '3.0.2'
junitVersion = '4.13'
kotlinVersion = '1.4.10'
lifecycleRuntimeKtxVersion = '2.3.0-alpha07'
materialComponentsVersion = '22973ff10305397bfc72fed41684175fee8aeefb'
mavenPluginVersion = '2.1'
junitVersion = '4.13.1'
kotlinVersion = '1.4.21'
lifecycleRuntimeKtxVersion = '2.3.0-beta01'
materialComponentsVersion = '1.3.0-alpha04'
preferenceVersion = '1.1.1'
zxingEmbeddedVersion = '3.6.0'
@@ -28,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()
@@ -37,11 +33,43 @@ 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()
maven { url 'https://jitpack.io' }
}
}
@@ -51,9 +79,9 @@ task clean(type: Delete) {
tasks {
wrapper {
gradleVersion = "6.6.1"
gradleVersion = "6.7.1"
distributionType = Wrapper.DistributionType.ALL
distributionSha256Sum = "11657af6356b7587bfb37287b5992e94a9686d5c8a0a1b60b87b9928a2decde5"
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
+2 -2
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=11657af6356b7587bfb37287b5992e94a9686d5c8a0a1b60b87b9928a2decde5
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip
distributionSha256Sum=22449f5231796abd892c98b2a07c9ceebe4688d192cd2d6763f8e3bf8acbedeb
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+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 {})"' \;
+1
View File
@@ -46,6 +46,7 @@ android {
}
lintOptions {
disable('LongLogTag')
disable('NewApi') // Desugaring!
}
}
+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
}
}
}
@@ -214,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);
@@ -302,11 +304,6 @@ 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.
@@ -56,9 +56,10 @@ public class Statistics {
* @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;
}
/**
@@ -69,9 +70,10 @@ public class Statistics {
* @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;
}
/**
@@ -34,7 +34,8 @@ import java.util.Set;
import androidx.annotation.Nullable;
/**
* 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
@@ -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; ");
@@ -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
@@ -13,7 +13,9 @@ import java.security.MessageDigest;
import java.util.Arrays;
/**
* This implementation is based on the ed25519/ref10 implementation in NaCl.
* Implementation of Ed25519 signature verification.
*
* <p>This implementation is based on the ed25519/ref10 implementation in NaCl.</p>
*
* <p>It implements this twisted Edwards curve:
*
+1 -1
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
+4 -4
View File
@@ -3,8 +3,8 @@ module golang.zx2c4.com/wireguard/android
go 1.14
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-20201208171446-5f87f3452ae9 // indirect
golang.org/x/net v0.0.0-20201216054612-986b41b23924 // indirect
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e
golang.zx2c4.com/wireguard v0.0.20201119-0.20201215171155-74008520cdd9
)
+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-20201208171446-5f87f3452ae9 h1:sYNJzB4J8toYPQTM6pAkcmBRgw9SnQKP9oXCHfgy604=
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/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-20201214210602-f9fddec55a1e h1:AyodaIpKjppX+cBfTASF2E1US3H2JFBj920Ot3rtDjs=
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/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.20201215171155-74008520cdd9 h1:rdbSLajsO6nfT4NViHeB/6lMW4vaO1bfsTvmeDpYvVs=
golang.zx2c4.com/wireguard v0.0.20201119-0.20201215171155-74008520cdd9/go.mod h1:ITsWNpkFv78VPB7f8MiyuxeEMcHR4jfxHGCJLPP3GHs=
+1 -1
View File
@@ -69,7 +69,7 @@ dependencies {
implementation "androidx.preference:preference-ktx:$preferenceVersion"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleRuntimeKtxVersion"
implementation "androidx.datastore:datastore-preferences:$datastoreVersion"
implementation "com.github.material-components:material-components-android:$materialComponentsVersion"
implementation "com.google.android.material:material:$materialComponentsVersion"
implementation "com.journeyapps:zxing-android-embedded:$zxingEmbeddedVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
+9 -1
View File
@@ -48,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>
@@ -56,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"
@@ -19,6 +19,7 @@ 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
@@ -146,12 +147,14 @@ class LogViewerActivity : AppCompatActivity() {
}
}
private val downloadsFileSaver = DownloadsFileSaver(this)
private suspend fun saveLog() {
var exception: Throwable? = null
var outputFile: DownloadsFileSaver.DownloadsFile? = null
withContext(Dispatchers.IO) {
try {
outputFile = DownloadsFileSaver.save(this@LogViewerActivity, "wireguard-log.txt", "text/plain", true)
outputFile = downloadsFileSaver.save("wireguard-log.txt", "text/plain", true)
outputFile?.outputStream?.write(rawLogLines.toString().toByteArray(Charsets.UTF_8))
} catch (e: Throwable) {
outputFile?.delete()
@@ -177,7 +180,7 @@ class LogViewerActivity : AppCompatActivity() {
process = try {
builder.start()
} catch (e: IOException) {
e.printStackTrace()
Log.e(TAG, Log.getStackTraceString(e))
return@withContext
}
val stdout = BufferedReader(InputStreamReader(process!!.inputStream, StandardCharsets.UTF_8))
@@ -251,6 +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 TAG = "WireGuard/LogViewerActivity"
}
private inner class LogEntryAdapter : RecyclerView.Adapter<LogEntryAdapter.ViewHolder>() {
@@ -12,6 +12,7 @@ import android.view.View
import androidx.appcompat.app.ActionBar
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import androidx.fragment.app.commit
import com.wireguard.android.R
import com.wireguard.android.fragment.TunnelDetailFragment
import com.wireguard.android.fragment.TunnelEditorFragment
@@ -72,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.
@@ -104,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)
}
}
}
}
@@ -8,6 +8,7 @@ import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.MenuItem
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
@@ -28,9 +29,9 @@ class SettingsActivity : ThemeChangeAwareActivity() {
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())
}
}
}
@@ -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,20 +10,23 @@ 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
lifecycleScope.launch {
try {
@@ -42,6 +45,20 @@ class TunnelToggleActivity : AppCompatActivity() {
}
}
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 {
private const val TAG = "WireGuard/TunnelToggleActivity"
}
@@ -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"
}
}
@@ -41,7 +41,7 @@ class AddTunnelsSheet : BottomSheetDialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
if (savedInstanceState != null) dismiss()
val view = inflater.inflate(R.layout.add_tunnels_bottom_sheet, container, false)
if (activity?.packageManager?.hasSystemFeature(PackageManager.FEATURE_CAMERA) != true) {
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
@@ -4,8 +4,8 @@
*/
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
@@ -40,12 +40,11 @@ class AppListDialogFragment : DialogFragment() {
try {
val applicationData: MutableList<ApplicationData> = ArrayList()
withContext(Dispatchers.IO) {
val launcherIntent = Intent(Intent.ACTION_MAIN, null)
launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER)
val resolveInfos = pm.queryIntentActivities(launcherIntent, 0)
resolveInfos.forEach {
val packageName = it.activityInfo.packageName
val appData = ApplicationData(it.loadIcon(pm), it.loadLabel(pm).toString(), packageName, currentlySelectedApps.contains(packageName))
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) {
@@ -31,7 +31,6 @@ import kotlinx.coroutines.launch
* attached to a `BaseActivity`.
*/
abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
private var baseActivity: BaseActivity? = null
private var pendingTunnel: ObservableTunnel? = null
private var pendingTunnelUp: Boolean? = null
private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
@@ -44,24 +43,18 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
}
protected var selectedTunnel: ObservableTunnel?
get() = baseActivity?.selectedTunnel
get() = (activity as? BaseActivity)?.selectedTunnel
protected set(tunnel) {
baseActivity?.selectedTunnel = 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()
}
@@ -71,9 +64,10 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
is TunnelListItemBinding -> binding.item
else -> return
} ?: return
lifecycleScope.launch {
val activity = activity ?: return
activity.lifecycleScope.launch {
if (Application.getBackend() is GoBackend) {
val intent = GoBackend.VpnService.prepare(view.context)
val intent = GoBackend.VpnService.prepare(activity)
if (intent != null) {
pendingTunnel = tunnel
pendingTunnelUp = checked
@@ -86,20 +80,21 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
}
private fun setTunnelStateWithPermissionsResult(tunnel: ObservableTunnel, checked: Boolean) {
lifecycleScope.launch {
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 = requireContext().getString(messageResId, error)
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(requireContext(), message, Toast.LENGTH_LONG).show()
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
Log.e(TAG, message, e)
}
}
@@ -29,15 +29,15 @@ class ConfigNamingDialogFragment : DialogFragment() {
private var imm: InputMethodManager? = null
private fun createTunnelAndDismiss() {
binding?.let {
val name = it.tunnelNameText.text.toString()
lifecycleScope.launch {
try {
Application.getTunnelManager().create(name, config)
dismiss()
} catch (e: Throwable) {
it.tunnelNameTextLayout.error = e.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
}
}
}
@@ -17,9 +17,9 @@ 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.util.QuantityFormatter
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.Timer
import java.util.TimerTask
/**
* 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)
@@ -64,34 +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 lifecycleScope.launch {
try {
binding!!.config = newTunnel.getConfigAsync()
} catch (_: Throwable) {
binding!!.config = null
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?) {
@@ -101,37 +92,36 @@ 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
lifecycleScope.launch {
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)
val tx = statistics.peerTx(publicKey)
if (rx == 0L && tx == 0L) {
peer.transferLabel.visibility = View.GONE
peer.transferText.visibility = View.GONE
continue
}
peer.transferText.text = requireContext().getString(R.string.transfer_rx_tx, formatBytes(rx), 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
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)
val tx = statistics.peerTx(publicKey)
if (rx == 0L && tx == 0L) {
peer.transferLabel.visibility = View.GONE
peer.transferText.visibility = View.GONE
continue
}
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
}
}
}
@@ -39,24 +39,27 @@ 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()
}
}
@@ -115,7 +118,8 @@ class TunnelEditorFragment : BaseFragment() {
Snackbar.make(binding!!.mainContainer, error, Snackbar.LENGTH_LONG).show()
return false
}
lifecycleScope.launch {
val activity = requireActivity()
activity.lifecycleScope.launch {
when {
tunnel == null -> {
Log.d(TAG, "Attempting to create new tunnel " + binding!!.name)
@@ -209,46 +213,48 @@ class TunnelEditorFragment : BaseFragment() {
}
private fun onTunnelCreated(newTunnel: ObservableTunnel?, throwable: Throwable?) {
val message: String
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)
lifecycleScope.launch {
try {
renamedTunnel.setConfigAsync(newConfig)
onConfigSaved(renamedTunnel, null)
} catch (e: Throwable) {
onConfigSaved(renamedTunnel, e)
}
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()
}
}
@@ -6,9 +6,7 @@ package com.wireguard.android.fragment
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
@@ -17,6 +15,7 @@ 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
@@ -29,27 +28,16 @@ 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.util.TunnelImporter
import com.wireguard.android.widget.MultiselectableRelativeLayout
import com.wireguard.config.Config
import kotlinx.coroutines.Deferred
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.io.BufferedReader
import java.io.ByteArrayInputStream
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
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.
@@ -59,114 +47,20 @@ class TunnelListFragment : BaseFragment() {
private var actionMode: ActionMode? = null
private var binding: TunnelListFragmentBinding? = null
private val tunnelFileImportResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { data ->
importTunnel(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 val qrImportResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val qrCode = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
qrCode?.contents?.let { importTunnel(it) }
}
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: Throwable) {
onTunnelImportFinished(emptyList(), listOf<Throwable>(e))
}
}
private fun importTunnel(uri: Uri?) {
lifecycleScope.launch {
withContext(Dispatchers.IO) {
val activity = activity
if (activity == null || uri == null) {
return@withContext
}
val contentResolver = activity.contentResolver
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) { 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: 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()) { resources.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) }
} catch (e: Throwable) {
withContext(Dispatchers.Main.immediate) { onTunnelImportFinished(emptyList(), listOf(e)) }
}
}
}
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 onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -224,43 +118,24 @@ class TunnelListFragment : BaseFragment() {
binding ?: return
lifecycleScope.launch {
val tunnels = Application.getTunnelManager().getTunnels()
if (newTunnel != null) viewForTunnel(newTunnel, tunnels).setSingleSelected(true)
if (oldTunnel != null) viewForTunnel(oldTunnel, tunnels).setSingleSelected(false)
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
@@ -289,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 {
@@ -311,13 +188,14 @@ 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
}
lifecycleScope.launch {
activity.lifecycleScope.launch {
try {
val tunnels = Application.getTunnelManager().getTunnels()
val tunnelsToDelete = ArrayList<ObservableTunnel>()
@@ -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"
}
@@ -9,6 +9,7 @@ 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
@@ -20,6 +21,7 @@ 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.ErrorMessages
import com.wireguard.android.util.UserKnobs
import com.wireguard.android.util.applicationScope
import com.wireguard.config.Config
@@ -229,7 +231,11 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
val tunnelName = intent.getStringExtra("tunnel") ?: return@launch
val tunnels = manager.getTunnels()
val tunnel = tunnels[tunnelName] ?: return@launch
manager.setTunnelState(tunnel, state)
try {
manager.setTunnelState(tunnel, state)
} catch (e: Throwable) {
Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_LONG).show()
}
}
}
}
@@ -8,6 +8,7 @@ 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
@@ -15,6 +16,7 @@ import com.wireguard.android.activity.SettingsActivity
import com.wireguard.android.backend.Tunnel
import com.wireguard.android.backend.WgQuickBackend
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
@@ -39,7 +41,7 @@ class KernelModuleDisablerPreference(context: Context, attrs: AttributeSet?) : P
override fun getTitle() = if (state == State.UNKNOWN) "" else context.getString(state.titleResourceId)
override fun onClick() {
lifecycleScope.launch {
activity.lifecycleScope.launch {
if (state == State.DISABLED) {
setState(State.ENABLING)
UserKnobs.setDisableKernelModule(false)
@@ -32,6 +32,8 @@ 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() {
lifecycleScope.launch {
val tunnels = Application.getTunnelManager().getTunnels()
@@ -41,7 +43,7 @@ class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference
if (configs.isEmpty()) {
throw IllegalArgumentException(context.getString(R.string.no_tunnels_error))
}
val outputFile = DownloadsFileSaver.save(activity, "wireguard-export.zip", "application/zip", true)
val outputFile = downloadsFileSaver.save("wireguard-export.zip", "application/zip", true)
if (outputFile == null) {
withContext(Dispatchers.Main.immediate) {
isEnabled = true
@@ -9,7 +9,6 @@ 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
@@ -41,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))
@@ -14,6 +14,7 @@ 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
@@ -25,9 +26,18 @@ import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
object DownloadsFileSaver {
@Throws(Exception::class)
suspend fun save(context: ComponentActivity, name: String, mimeType: String?, overwriteExisting: Boolean) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
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) }
}
}
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)
@@ -66,13 +76,12 @@ object DownloadsFileSaver {
} else {
withContext(Dispatchers.Main.immediate) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
val futureGrant = CompletableDeferred<Boolean>()
val activityResult = context.registerForActivityResult(ActivityResultContracts.RequestPermission(), futureGrant::complete)
activityResult.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
val granted = futureGrant.await()
activityResult.unregister()
if (!granted)
if (!granted) {
futureGrant = CompletableDeferred()
return@withContext null
}
}
@Suppress("DEPRECATION") val path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
withContext(Dispatchers.IO) {
@@ -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 {
@@ -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,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>
@@ -31,7 +31,6 @@
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"
+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.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+70
View File
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<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="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_from_qr_code">Escaneja codi QR</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_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_title">Registre</string>
<string name="name">Nom</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="private_key">Clau privada</string>
<string name="public_key">Clau pública</string>
<string name="save">Guarda</string>
<string name="select_all">Selecciona-ho tot</string>
<string name="settings">Configuració</string>
<string name="tunnel_error_invalid_name">Nom no vàlid</string>
<string name="zip_export_success">Guardat a \"%s\"</string>
</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>
+211
View File
@@ -0,0 +1,211 @@
<?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="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>
+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>
+23
View File
@@ -0,0 +1,23 @@
<?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">Kan ikke slette %d tunneler %s</item>
</plurals>
<string name="all_applications">Alle applikasjoner</string>
<string name="exclude_from_tunnel">Ekskluder</string>
<string name="include_in_tunnel">Inkluder kun</string>
<plurals name="persistent_keepalive_seconds_unit">
<item quantity="one">hvert %d sekund</item>
<item quantity="other">hvert %d sekund</item>
</plurals>
<plurals name="persistent_keepalive_seconds_suffix">
<item quantity="one">sekund</item>
<item quantity="other">sekunder</item>
</plurals>
<string name="use_all_applications">Bruk alle apper</string>
<string name="add_peer">Legg til peer</string>
<string name="addresses">Adresser</string>
<string name="applications">Applikasjoner</string>
<string name="app_name">WireGuard</string>
</resources>
+255
View File
@@ -0,0 +1,255 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="delete_error">
<item quantity="one">Nie można usunąć %d tunelu: %s</item>
<item quantity="few">Nie można usunąć %d tuneli: %s</item>
<item quantity="many">Nie można usunąć %d tuneli: %s</item>
<item quantity="other">Nie można usunąć %d tuneli: %s</item>
</plurals>
<plurals name="delete_success">
<item quantity="one">Pomyślnie usunięto tunel %d</item>
<item quantity="few">Pomyślnie usunięto %d tunele</item>
<item quantity="many">Pomyślnie usunięto %d tuneli</item>
<item quantity="other">Pomyślnie usunięto %d tuneli</item>
</plurals>
<plurals name="delete_title">
<item quantity="one">%d wybrany tunel</item>
<item quantity="few">%d wybranych tuneli</item>
<item quantity="many">%d wybranych tuneli</item>
<item quantity="other">%d wybranych tuneli</item>
</plurals>
<plurals name="import_partial_success">
<item quantity="one">Zaimportowano %1$d z %2$d tuneli</item>
<item quantity="few">Zaimportowano %1$d z %2$d tuneli</item>
<item quantity="many">Zaimportowano %1$d z %2$d tuneli</item>
<item quantity="other">Zaimportowano %1$d z %2$d tuneli</item>
</plurals>
<plurals name="import_total_success">
<item quantity="one">Zaimportowano %d tunel</item>
<item quantity="few">Zaimportowano %d tunele</item>
<item quantity="many">Zaimportowano %d tuneli</item>
<item quantity="other">Zaimportowano %d tuneli</item>
</plurals>
<plurals name="set_excluded_applications">
<item quantity="one">%d wykluczona aplikacja</item>
<item quantity="few">%d wykluczone aplikacje</item>
<item quantity="many">%d wykluczonych aplikacji</item>
<item quantity="other">%d wykluczonych aplikacji</item>
</plurals>
<plurals name="set_included_applications">
<item quantity="one">%d dołączona aplikacja</item>
<item quantity="few">%d dołączone aplikacje</item>
<item quantity="many">%d dołączonych aplikacji</item>
<item quantity="other">%d dołączonych aplikacji</item>
</plurals>
<plurals name="n_excluded_applications">
<item quantity="one">%d wykluczona</item>
<item quantity="few">%d wykluczone</item>
<item quantity="many">%d wykluczonych</item>
<item quantity="other">%d wykluczonych</item>
</plurals>
<plurals name="n_included_applications">
<item quantity="one">%d dołączona</item>
<item quantity="few">%d dołączone</item>
<item quantity="many">%d dołączonych</item>
<item quantity="other">%d dołączonych</item>
</plurals>
<string name="all_applications">Wszystkie aplikacje</string>
<string name="exclude_from_tunnel">Wyklucz</string>
<string name="include_in_tunnel">Dołącz tylko</string>
<plurals name="include_n_applications">
<item quantity="one">Dołącz %d aplikację</item>
<item quantity="few">Dołącz %d aplikacje</item>
<item quantity="many">Dołącz %d aplikacji</item>
<item quantity="other">Dołącz %d aplikacji</item>
</plurals>
<plurals name="exclude_n_applications">
<item quantity="one">Wyklucz %d aplikację</item>
<item quantity="few">Wyklucz %d aplikacje</item>
<item quantity="many">Wyklucz %d aplikacji</item>
<item quantity="other">Wyklucz %d aplikacji</item>
</plurals>
<plurals name="persistent_keepalive_seconds_unit">
<item quantity="one">co sekundę</item>
<item quantity="few">co %d sekundy</item>
<item quantity="many">co %d sekund</item>
<item quantity="other">co %d sekund</item>
</plurals>
<plurals name="persistent_keepalive_seconds_suffix">
<item quantity="one">sekunda</item>
<item quantity="few">sekundy</item>
<item quantity="many">sekund</item>
<item quantity="other">sekundy</item>
</plurals>
<string name="use_all_applications">Używaj wszystkich aplikacji</string>
<string name="add_peer">Dodaj Peer\'a</string>
<string name="addresses">Adresy</string>
<string name="applications">Aplikacje</string>
<string name="allow_remote_control_intents_summary_off">Zewnętrzne aplikacje nie mogą przełączać tuneli (zalecane)</string>
<string name="allow_remote_control_intents_summary_on">Zewnętrzne aplikacje mogą przełączać tunele (zaawansowane)</string>
<string name="allow_remote_control_intents_title">Zezwól na kontrolowanie przez zewnętrzne aplikacje</string>
<string name="allowed_ips">Dozwolone adresy IP</string>
<string name="app_name">WireGuard</string>
<string name="bad_config_context">%1$s dla %2$s</string>
<string name="bad_config_context_top_level">%s</string>
<string name="bad_config_error">%1$s w %2$s</string>
<string name="bad_config_explanation_pka">: Musi być dodatnia i nie większa niż 65535</string>
<string name="bad_config_explanation_positive_number">: Musi być dodatni</string>
<string name="bad_config_explanation_udp_port">: Musi być poprawnym numerem portu UDP</string>
<string name="bad_config_reason_invalid_key">Nieprawidłowy klucz</string>
<string name="bad_config_reason_invalid_number">Numer jest nieprawidłowy</string>
<string name="bad_config_reason_invalid_value">Nieprawidłowa wartość</string>
<string name="bad_config_reason_missing_attribute">Brakujący atrybut</string>
<string name="bad_config_reason_missing_section">Brakująca sekcja</string>
<string name="bad_config_reason_syntax_error">Błąd składni</string>
<string name="bad_config_reason_unknown_attribute">Nieznany atrybut</string>
<string name="bad_config_reason_unknown_section">Nieznana sekcja</string>
<string name="bad_config_reason_value_out_of_range">Wartość poza zakresem</string>
<string name="bad_extension_error">Plik musi być .conf lub .zip</string>
<string name="cancel">Anuluj</string>
<string name="config_delete_error">Nie można usunąć pliku konfiguracyjnego %s</string>
<string name="config_exists_error">Konfiguracja dla \"%s\" już istnieje</string>
<string name="config_file_exists_error">Plik konfiguracyjny \"%s\" już istnieje</string>
<string name="config_not_found_error">Plik konfiguracyjny \"%s\" nie został znaleziony</string>
<string name="config_rename_error">Nie można zmienić nazwy pliku konfiguracyjnego \"%s\"</string>
<string name="config_save_error">Nie można zapisać konfiguracji dla \"%1$s\": %2$s</string>
<string name="config_save_success">Pomyślnie zapisano konfigurację dla \"%s\"</string>
<string name="create_activity_title">Utwórz tunel WireGuard</string>
<string name="create_bin_dir_error">Nie można stworzyć tymczasowego folderu lokalnego</string>
<string name="create_downloads_file_error">Nie można utworzyć pliku w folderze pobierania</string>
<string name="create_empty">Utwórz od podstaw</string>
<string name="create_from_file">Utwórz z pliku lub archiwum</string>
<string name="create_from_qr_code">Zeskanuj kod QR</string>
<string name="create_output_dir_error">Nie można utworzyć folderu wyjściowego</string>
<string name="create_temp_dir_error">Nie można stworzyć tymczasowego folderu lokalnego</string>
<string name="create_tunnel">Utwórz Tunel</string>
<string name="dark_theme_summary_off">Obecnie używasz jasnego (dzień) motywu</string>
<string name="dark_theme_summary_on">Obecnie używa ciemnego (nocnego) motywu</string>
<string name="dark_theme_title">Użyj ciemnego motywu</string>
<string name="delete">Usuń</string>
<string name="dns_servers">Serwery DNS</string>
<string name="edit">Edytuj</string>
<string name="endpoint">Punkt końcowy</string>
<string name="error_down">Błąd podczas zamykania tunelu: %s</string>
<string name="error_fetching_apps">Błąd podczas pobierania listy aplikacji: %s</string>
<string name="error_root">Proszę uzyskać dostęp do root\'a i spróbować ponownie</string>
<string name="error_up">Błąd podczas włączania tunelu: %s</string>
<string name="exclude_private_ips">Wyklucz prywatne adresy IP</string>
<string name="generate_new_private_key">Wygeneruj nowy klucz prywatny</string>
<string name="generic_error">Nieznany błąd \"%s\"</string>
<string name="hint_automatic">(automatycznie)</string>
<string name="hint_generated">(wygenerowane)</string>
<string name="hint_optional">(opcjonalnie)</string>
<string name="hint_optional_discouraged">(opcjonalnie, niezalecane)</string>
<string name="hint_random">(losowy)</string>
<string name="illegal_filename_error">Niedozwolona nazwa pliku \"%s\"</string>
<string name="import_error">Nie można zaimportować tunelu: %s</string>
<string name="import_from_qr_code">Importuj tunel za pomocą kodu QR</string>
<string name="import_success">Zaimportowano \"%s\"</string>
<string name="interface_title">Interfejs</string>
<string name="key_contents_error">Niepoprawne znaki w kluczu</string>
<string name="key_length_error">Niewłaściwa długość klucza</string>
<string name="key_length_explanation_base64">: klucze base64 WireGuard\'a muszą mieć długość 44 znaków (32 bajty)</string>
<string name="key_length_explanation_binary">: klucze WireGuard\'a muszą wynosić 32 bajty</string>
<string name="key_length_explanation_hex">: klucze hex WireGuard\'a muszą mieć długość 64 znaków (32 bajty)</string>
<string name="listen_port">Port nasłuchu</string>
<string name="log_export_error">Nie można wyeksportować logu: %s</string>
<string name="log_export_subject">Plik logu programu WireGuard dla systemu Android</string>
<string name="log_export_success">Zapisano w \"%s\"</string>
<string name="log_export_title">Wyeksportuj plik logu</string>
<string name="log_saver_activity_label">Zapisz log</string>
<string name="log_viewer_pref_summary">Logi mogą pomóc w debugowaniu</string>
<string name="log_viewer_pref_title">Wyświetl log aplikacji</string>
<string name="log_viewer_title">Log</string>
<string name="logcat_error">Nie można uruchomić logcat: </string>
<string name="module_disabler_disabled_summary">Eksperymentalny moduł jądra może poprawić wydajność</string>
<string name="module_disabler_disabled_title">Włącz moduł jądra</string>
<string name="module_disabler_enabled_summary">Wolniejsza implementacja w przestrzeni użytkownika może poprawić stabilność</string>
<string name="module_disabler_enabled_title">Wyłącz moduł jądra</string>
<string name="module_installer_error">Coś poszło nie tak. Proszę spróbować ponownie</string>
<string name="module_installer_initial">Eksperymentalny moduł jądra może poprawić wydajność</string>
<string name="module_installer_not_found">Brak dostępnych modułów dla twojego urządzenia</string>
<string name="module_installer_title">Pobierz i zainstaluj moduł jądra</string>
<string name="module_installer_working">Pobieranie i instalowanie…</string>
<string name="module_version_error">Nie można określić wersji modułu jądra</string>
<string name="mtu">MTU</string>
<string name="multiple_tunnels_summary_off">Włączenie jednego tunelu spowoduje wyłączenie innych</string>
<string name="multiple_tunnels_summary_on">Wiele tuneli może być włączonych jednocześnie</string>
<string name="multiple_tunnels_title">Zezwalaj na wiele tuneli równoległych</string>
<string name="name">Nazwa</string>
<string name="no_config_error">Próba przywołania tunelu bez konfiguracji</string>
<string name="no_configs_error">Nie odnaleziono żadnych konfiguracji</string>
<string name="no_tunnels_error">Brak tuneli</string>
<string name="parse_error_generic">ciągu</string>
<string name="parse_error_inet_address">Adresy IP</string>
<string name="parse_error_inet_endpoint">punkt końcowy</string>
<string name="parse_error_inet_network">Sieć IP</string>
<string name="parse_error_integer">numer</string>
<string name="parse_error_reason">Nie można przetworzyć %1$s „%2$s”</string>
<string name="peer">Peer</string>
<string name="permission_description">kontrolowanie tuneli WireGuard, włączanie i wyłączanie tuneli, potencjalnie błędne kierowanie ruchem internetowym</string>
<string name="permission_label">sterowanie tunelami WireGuard</string>
<string name="persistent_keepalive">Utrzymanie połączenia</string>
<string name="pre_shared_key">Klucz wstępnie udostępniony</string>
<string name="pre_shared_key_enabled">włączone</string>
<string name="private_key">Klucz prywatny</string>
<string name="public_key">Klucz publiczny</string>
<string name="qr_code_hint">Wskazówka: wygeneruj za pomocą `qrencode -t ansiutf8 &lt; tunnel.conf`.</string>
<string name="restore_on_boot_summary_off">Włączone tunele nie zostaną przywrócone przy uruchomieniu</string>
<string name="restore_on_boot_summary_on">Włączone tunele zostaną przywrócone przy uruchomieniu</string>
<string name="restore_on_boot_title">Przywróć przy uruchomieniu</string>
<string name="save">Zapisz</string>
<string name="select_all">Wybierz wszystko</string>
<string name="settings">Ustawienia</string>
<string name="shell_exit_status_read_error">Powłoka nie może odczytać statusu wyjścia</string>
<string name="shell_marker_count_error">Powłoka spodziewała się 4 znaczników, otrzymano %d</string>
<string name="shell_start_error">Nie udało się uruchomić powłoki: %d</string>
<string name="success_application_will_restart">Ukończono pomyślnie. Aplikacja zostanie uruchomiona ponownie…</string>
<string name="toggle_all">Przełącz wszystkie</string>
<string name="toggle_error">Błąd podczas przełączania tunelu WireGuard: %s</string>
<string name="tools_installer_already">wg i wg-quick są już zainstalowane</string>
<string name="tools_installer_failure">Nie można zainstalować narzędzi wiersza poleceń (brak root\'a?)</string>
<string name="tools_installer_initial">Zainstaluj opcjonalne narzędzia do tworzenia skryptów</string>
<string name="tools_installer_initial_magisk">Zainstaluj opcjonalne narzędzia do tworzenia skryptów jako moduł Magisk</string>
<string name="tools_installer_initial_system">Zainstaluj opcjonalne narzędzia do tworzenia skryptów w partycji systemowej</string>
<string name="tools_installer_success_magisk">wg i wg-quick zostały zainstalowane jako moduł Magisk (wymagane ponowne uruchomienie)</string>
<string name="tools_installer_success_system">wg i wg-quick zostały zainstalowane w partycji systemowej</string>
<string name="tools_installer_title">Zainstaluj narzędzia wiersza poleceń</string>
<string name="tools_installer_working">Instalowanie wg i wg-quick</string>
<string name="tools_unavailable_error">Wymagane narzędzia są niedostępne</string>
<string name="transfer">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">odebrano: %1$s, wysłano: %2$s</string>
<string name="transfer_tibibytes">%.2f TiB</string>
<string name="tun_create_error">Nie można utworzyć urządzenia tun</string>
<string name="tunnel_config_error">Nie można skonfigurować tunelu (wg-quick zwróciło %d)</string>
<string name="tunnel_create_error">Nie można utworzyć tunelu: %s</string>
<string name="tunnel_create_success">Pomyślnie utworzono tunel \"%s\"</string>
<string name="tunnel_error_already_exists">Tunel \"%s\" już istnieje</string>
<string name="tunnel_error_invalid_name">Nieprawidłowa nazwa</string>
<string name="tunnel_list_placeholder">Dodaj tunel za pomocą niebieskiego przycisku</string>
<string name="tunnel_name">Nazwa tunelu</string>
<string name="tunnel_on_error">Nie można włączyć tunelu (wgTurnOn zwróciło %d)</string>
<string name="tunnel_rename_error">Nie można zmienić nazwy tunelu: %s</string>
<string name="tunnel_rename_success">Pomyślnie zmieniono nazwę tunelu na \"%s\"</string>
<string name="type_name_go_userspace">Przestrzeń użytkownika Go</string>
<string name="type_name_kernel_module">Moduł jądra</string>
<string name="unknown_error">Nieznany błąd</string>
<string name="version_summary">Implementacja %1$s v%2$s</string>
<string name="version_summary_checking">Sprawdzanie wersji %s implementacji</string>
<string name="version_summary_unknown">Nieznana %s wersja</string>
<string name="version_title">WireGuard dla systemu Android v%s</string>
<string name="vpn_not_authorized_error">Usługa VPN nie została autoryzowana przez użytkownika</string>
<string name="vpn_start_error">Nie można uruchomić usługi VPN systemu Android</string>
<string name="zip_export_error">Nie można wyeksportować tuneli: %s</string>
<string name="zip_export_success">Zapisano w \"%s\"</string>
<string name="zip_export_summary">Plik ZIP zostanie zapisany w folderze pobierania</string>
<string name="zip_export_title">Wyeksportuj tunele do pliku ZIP</string>
<string name="biometric_prompt_zip_exporter_title">Uwierzytelnij, aby wyeksportować tunele</string>
<string name="biometric_prompt_private_key_title">Uwierzytelnij, aby zobaczyć klucz prywatny</string>
<string name="biometric_auth_error">Błąd uwierzytelnienia</string>
<string name="biometric_auth_error_reason">Błąd uwierzytelnienia: %s</string>
</resources>
+242
View File
@@ -0,0 +1,242 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="delete_error">
<item quantity="one">Nu se poate șterge %d tunel: %s</item>
<item quantity="few">Nu se pot șterge %d tunele: %s</item>
<item quantity="other">Nu se pot șterge %d de tunele: %s</item>
</plurals>
<plurals name="delete_success">
<item quantity="one">%d tunel a fost șters</item>
<item quantity="few">%d tunele șterse</item>
<item quantity="other">%d de tunele șterse</item>
</plurals>
<plurals name="delete_title">
<item quantity="one">%d tunel selectat</item>
<item quantity="few">%d tuneluri selectate</item>
<item quantity="other">%d de tuneluri selectate</item>
</plurals>
<plurals name="import_partial_success">
<item quantity="one">%1$d din %2$d tuneluri importate</item>
<item quantity="few">%1$d din %2$d tuneluri importate</item>
<item quantity="other">%1$d din %2$d de tuneluri importate</item>
</plurals>
<plurals name="import_total_success">
<item quantity="one">%d tunel importat</item>
<item quantity="few">%d tuneluri importate</item>
<item quantity="other">%d de tuneluri importate</item>
</plurals>
<plurals name="set_excluded_applications">
<item quantity="one">%d aplicație exclusă</item>
<item quantity="few">%d aplicații excluse</item>
<item quantity="other">%d de aplicații excluse</item>
</plurals>
<plurals name="set_included_applications">
<item quantity="one">%d aplicație inclusă</item>
<item quantity="few">%d aplicații incluse</item>
<item quantity="other">%d de aplicații incluse</item>
</plurals>
<plurals name="n_excluded_applications">
<item quantity="one">%d exclusă</item>
<item quantity="few">%d excluse</item>
<item quantity="other">%d excluse</item>
</plurals>
<plurals name="n_included_applications">
<item quantity="one">%d inclusă</item>
<item quantity="few">%d incluse</item>
<item quantity="other">%d incluse</item>
</plurals>
<string name="all_applications">Toate aplicațiile</string>
<string name="exclude_from_tunnel">Excludere</string>
<string name="include_in_tunnel">Includere numai</string>
<plurals name="include_n_applications">
<item quantity="one">Include %d aplicație</item>
<item quantity="few">Include %d aplicații</item>
<item quantity="other">Include %d de aplicații</item>
</plurals>
<plurals name="exclude_n_applications">
<item quantity="one">Exclude %d aplicație</item>
<item quantity="few">Exclude %d aplicații</item>
<item quantity="other">Exclude %d de aplicații</item>
</plurals>
<plurals name="persistent_keepalive_seconds_unit">
<item quantity="one">în fiecare secundă</item>
<item quantity="few">la fiecare %d secunde</item>
<item quantity="other">la fiecare %d secunde</item>
</plurals>
<plurals name="persistent_keepalive_seconds_suffix">
<item quantity="one">secundă</item>
<item quantity="few">secunde</item>
<item quantity="other">secunde</item>
</plurals>
<string name="use_all_applications">Utilizează toate aplicațiile</string>
<string name="add_peer">Adăugare pereche</string>
<string name="addresses">Adrese</string>
<string name="applications">Aplicații</string>
<string name="allow_remote_control_intents_summary_off">Aplicațiile externe nu pot comuta tunelurile (recomandat)</string>
<string name="allow_remote_control_intents_summary_on">Aplicațiile externe pot comuta tunelurile (avansat)</string>
<string name="allow_remote_control_intents_title">Permite aplicații de control la distanță</string>
<string name="allowed_ips">IP-uri permise</string>
<string name="app_name">WireGuard</string>
<string name="bad_config_context">%2$s pentru %1$s</string>
<string name="bad_config_context_top_level">%s</string>
<string name="bad_config_error">%1$s în %2$s</string>
<string name="bad_config_explanation_pka">: Trebuie să fie pozitiv și nu mai mare decât 65535</string>
<string name="bad_config_explanation_positive_number">: Trebuie să fie pozitiv</string>
<string name="bad_config_explanation_udp_port">: Trebuie să fie un număr de port UDP valid</string>
<string name="bad_config_reason_invalid_key">Cheie invalidă</string>
<string name="bad_config_reason_invalid_number">Număr invalid</string>
<string name="bad_config_reason_invalid_value">Valoare invalidă</string>
<string name="bad_config_reason_missing_attribute">Atribuit lipsă</string>
<string name="bad_config_reason_missing_section">Secțiune lipsă</string>
<string name="bad_config_reason_syntax_error">Eroare de sintaxă</string>
<string name="bad_config_reason_unknown_attribute">Atribuit necunoscut</string>
<string name="bad_config_reason_unknown_section">Secțiune necunoscută</string>
<string name="bad_config_reason_value_out_of_range">Valoare în afara intervalului</string>
<string name="bad_extension_error">Fișierul trebuie să fie .conf sau .zip</string>
<string name="cancel">Anulare</string>
<string name="config_delete_error">Fișierul de configurare %s nu poate fi șters</string>
<string name="config_exists_error">Configurația pentru „%s” există deja</string>
<string name="config_file_exists_error">Fișierul de configurare „%s” există deja</string>
<string name="config_not_found_error">Fișier de configurare „%s” negăsit</string>
<string name="config_rename_error">Fișierul de configurare „%s” nu poate fi redenumit</string>
<string name="config_save_error">Nu poate fi salvată configurația pentru „%1$s”: %2$s</string>
<string name="config_save_success">Configurația pentru „%s” a fost salvată</string>
<string name="create_activity_title">Creare tunel WireGuard</string>
<string name="create_bin_dir_error">Nu se poate crea directorul binar local</string>
<string name="create_downloads_file_error">Fișierul nu poate fi creat în directorul pentru descărcări</string>
<string name="create_empty">Creare de la zero</string>
<string name="create_from_file">Importare din fișier sau arhivă</string>
<string name="create_from_qr_code">Scanare din cod QR</string>
<string name="create_output_dir_error">Nu se poate crea directorul de ieșire</string>
<string name="create_temp_dir_error">Nu se poate crea directorul temporar local</string>
<string name="create_tunnel">Creare tunel</string>
<string name="dark_theme_summary_off">În prezent este utilizată tema luminoasă (zi)</string>
<string name="dark_theme_summary_on">În prezent este utilizată tema întunecată (noapte)</string>
<string name="dark_theme_title">Utilizare temă întunecată</string>
<string name="delete">Ștergere</string>
<string name="dns_servers">Servere DNS</string>
<string name="edit">Editare</string>
<string name="endpoint">Punct final</string>
<string name="error_down">Eroare la oprirea tunelului: %s</string>
<string name="error_fetching_apps">Eroare la preluarea listei de aplicații: %s</string>
<string name="error_root">Obține acces root și încearcă din nou</string>
<string name="error_up">Eroare la pornirea tunelului: %s</string>
<string name="exclude_private_ips">Excludere IP-uri private</string>
<string name="generate_new_private_key">Generare cheie privată nouă</string>
<string name="generic_error">Eroare „%s” necunoscută</string>
<string name="hint_automatic">(auto)</string>
<string name="hint_generated">(generată)</string>
<string name="hint_optional">(opțională)</string>
<string name="hint_optional_discouraged">(opțională, nerecomandată)</string>
<string name="hint_random">(aleatorie)</string>
<string name="illegal_filename_error">Nume nepermis de fișier „%s”</string>
<string name="import_error">Tunelul nu poate fi importat: %s</string>
<string name="import_from_qr_code">Importare tunel din cod QR</string>
<string name="import_success">„%s” importat</string>
<string name="interface_title">Interfață</string>
<string name="key_contents_error">Caractere incorecte în cheie</string>
<string name="key_length_error">Lungime incorectă a cheii</string>
<string name="key_length_explanation_base64">: Cheile base64 ale WireGuard trebuie să aibă 44 de caractere (32 de octeți)</string>
<string name="key_length_explanation_binary">: Cheile WireGuard trebuie să aibă 32 de octeți</string>
<string name="key_length_explanation_hex">: Cheile hex WireGuard trebuie să aibă 64 de caractere (32 de octeți)</string>
<string name="listen_port">Port de ascultare</string>
<string name="log_export_error">Jurnalul nu poate fi exportat: %s</string>
<string name="log_export_subject">Fișier de jurnal Android WireGuard</string>
<string name="log_export_success">Salvat în „%s”</string>
<string name="log_export_title">Exportare fișier de jurnal</string>
<string name="log_saver_activity_label">Salvare jurnal</string>
<string name="log_viewer_pref_summary">Jurnalele pot ajuta la depanare</string>
<string name="log_viewer_pref_title">Vizualizare jurnal aplicație</string>
<string name="log_viewer_title">Jurnal</string>
<string name="logcat_error">Programul logcat nu poate fi executat: </string>
<string name="module_disabler_disabled_summary">Modulul experimental de nucleu poate îmbunătăți performanța</string>
<string name="module_disabler_disabled_title">Activează biblioteca modulului de nucleu</string>
<string name="module_disabler_enabled_summary">Biblioteca mai lentă a spațiului utilizatorului poate îmbunătăți stabilitatea</string>
<string name="module_disabler_enabled_title">Dezactivează biblioteca modulului de nucleu</string>
<string name="module_installer_error">A apărut o eroare. Încearcă din nou</string>
<string name="module_installer_initial">Modulul experimental de nucleu poate îmbunătăți performanța</string>
<string name="module_installer_not_found">Nu sunt disponibile module pentru dispozitivul tău</string>
<string name="module_installer_title">Descărcare și instalare modul nucleu</string>
<string name="module_installer_working">Se descarcă și se instalează…</string>
<string name="module_version_error">Nu se poate determina versiunea modulului de nucleu</string>
<string name="mtu">MTU</string>
<string name="multiple_tunnels_summary_off">Pornirea unui tunel va opri celelalte tuneluri</string>
<string name="multiple_tunnels_summary_on">Mai multe tunele pot fi pornite simultan</string>
<string name="multiple_tunnels_title">Permite mai multe tuneluri simultane</string>
<string name="name">Nume</string>
<string name="no_config_error">Se încearcă pornirea unui tunel fără configurație</string>
<string name="no_configs_error">Nu au fost găsite configurații</string>
<string name="no_tunnels_error">Nu există tuneluri</string>
<string name="parse_error_generic">șir</string>
<string name="parse_error_inet_address">Adresă IP</string>
<string name="parse_error_inet_endpoint">punct final</string>
<string name="parse_error_inet_network">Rețea IP</string>
<string name="parse_error_integer">număr</string>
<string name="parse_error_reason">Nu se poate analiza %1$s „%2$s”</string>
<string name="peer">Pereche</string>
<string name="permission_description">controleze tuneluri WireGuard și, astfel, să activeze și să dezactiveze tuneluri în mod automat, existând posibilitatea de a dirija greșit traficul de internet</string>
<string name="permission_label">controleze tuneluri WireGuard</string>
<string name="persistent_keepalive">Mesaj keepalive persistent</string>
<string name="pre_shared_key">Cheie predistribuită</string>
<string name="pre_shared_key_enabled">activată</string>
<string name="private_key">Cheie privată</string>
<string name="public_key">Cheie publică</string>
<string name="qr_code_hint">Sfat: generează cu `qrencode -t ansiutf8 &lt; tunnel.conf`.</string>
<string name="restore_on_boot_summary_off">Tunelurile activate nu vor fi pornite odată cu pornirea dispozitivului</string>
<string name="restore_on_boot_summary_on">Tunelurile activate vor fi pornite odată cu pornirea dispozitivului</string>
<string name="restore_on_boot_title">Restaurare la pornire</string>
<string name="save">Salvare</string>
<string name="select_all">Selectare totală</string>
<string name="settings">Setări</string>
<string name="shell_exit_status_read_error">Interfața de comunicare nu poate citi starea de ieșire</string>
<string name="shell_marker_count_error">Interfața de comunicare a așteptat 4 marcatori, a primit %d</string>
<string name="shell_start_error">Interfața de comunicare nu a putut porni: %d</string>
<string name="success_application_will_restart">Succes. Aplicația se va reporni acum…</string>
<string name="toggle_all">Comutare toate</string>
<string name="toggle_error">Eroare la comutarea tunelului WireGuard: %s</string>
<string name="tools_installer_already">wg și wg-quick sunt deja instalate</string>
<string name="tools_installer_failure">Nu se pot instala instrumentele de linie de comandă (lipsă root?)</string>
<string name="tools_installer_initial">Instalare instrumente opționale pentru scriptare</string>
<string name="tools_installer_initial_magisk">Instalare instrumente opționale pentru scriptare ca modul Magisk</string>
<string name="tools_installer_initial_system">Instalare instrumente opționale pentru scriptare în partiția de sistem</string>
<string name="tools_installer_success_magisk">wg și wg-quick instalate ca modul Magisk (este necesară repornirea)</string>
<string name="tools_installer_success_system">wg și wg-quick instalate în partiția de sistem</string>
<string name="tools_installer_title">Instalare instrumente de linie de comandă</string>
<string name="tools_installer_working">Se instalează wg și wg-quick</string>
<string name="tools_unavailable_error">Instrumentele necesare sunt indisponibile</string>
<string name="transfer">Transferare</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">Nu se poate crea dispozitiv TUN</string>
<string name="tunnel_config_error">Tunelul nu poate fi configurat (wg-quick a returnat %d)</string>
<string name="tunnel_create_error">Tunelul nu poate fi creat: %s</string>
<string name="tunnel_create_success">Tunelul „%s” a fost creat</string>
<string name="tunnel_error_already_exists">Tunelul „%s” există deja</string>
<string name="tunnel_error_invalid_name">Nume invalid</string>
<string name="tunnel_list_placeholder">Adaugă un tunel folosind butonul albastru</string>
<string name="tunnel_name">Numele tunelului</string>
<string name="tunnel_on_error">Tunelul nu poate fi pornit (wgTurnOn a returnat %d)</string>
<string name="tunnel_rename_error">Tunelul nu poate fi redenumit: %s</string>
<string name="tunnel_rename_success">Tunelul a fost redenumit ca „%s”</string>
<string name="type_name_go_userspace">Spațiu de utilizator Go</string>
<string name="type_name_kernel_module">Modul nucleu</string>
<string name="unknown_error">Eroare necunoscută</string>
<string name="version_summary">Bibliotecă %1$s v%2$s</string>
<string name="version_summary_checking">Se verifică versiunea bibliotecii %s</string>
<string name="version_summary_unknown">Versiune %s necunoscută</string>
<string name="version_title">WireGuard pentru Android v%s</string>
<string name="vpn_not_authorized_error">Serviciul VPN nu este autorizat de utilizator</string>
<string name="vpn_start_error">Serviciul VPN Android nu poate fi pornit</string>
<string name="zip_export_error">Tunelurile nu pot fi exportate: %s</string>
<string name="zip_export_success">Salvat în „%s”</string>
<string name="zip_export_summary">Fișierul zip va fi salvat în dosarul pentru descărcări</string>
<string name="zip_export_title">Exportare tuneluri în fișierul zip</string>
<string name="biometric_prompt_zip_exporter_title">Autentifică-te pentru a exporta tunelurile</string>
<string name="biometric_prompt_private_key_title">Autentifică-te pentru a vizualiza cheia privată</string>
<string name="biometric_auth_error">Eroare la autentificare</string>
<string name="biometric_auth_error_reason">Eroare la autentificare: %s</string>
</resources>
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="create_activity_title">WireGuard Tüneli Yarat</string>
<string name="create_downloads_file_error">İndirilenler klasöründe dosya yaratılamadı</string>
<string name="create_empty">Sıfırdan Yaratın</string>
<string name="create_from_file">Arşivden geri yükle</string>
<string name="create_from_qr_code">QR Kodu Tara</string>
<string name="create_output_dir_error">Çıkış klasörü yaratılamadı</string>
<string name="create_tunnel">Tünel Yarat</string>
<string name="dark_theme_title">Koyu renk Temayı Kullan</string>
<string name="delete">Sil</string>
<string name="edit">Düzenle</string>
<string name="hint_automatic">(otomatik)</string>
<string name="hint_optional">(isteğe bağlı)</string>
<string name="hint_optional_discouraged">(İsteğe bağlı-Önerilmez)</string>
<string name="hint_random">(Rastgele)</string>
<string name="import_from_qr_code">Tüneli içeri QR ile al</string>
<string name="interface_title">Arayüz</string>
<string name="listen_port">Dinleme portu</string>
<string name="log_export_title">Dosyayı Dışarı Aktar</string>
<string name="log_saver_activity_label">Günlüğü kaydet</string>
<string name="log_viewer_pref_title">Uygulama günlüğüne bak</string>
<string name="log_viewer_title">Günlük</string>
</resources>
+86
View File
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="delete_error">
<item quantity="other">無法刪除 %d 個通道: %s</item>
</plurals>
<plurals name="delete_success">
<item quantity="other">成功刪除了 %d 個通道</item>
</plurals>
<plurals name="delete_title">
<item quantity="other">選擇了 %d 個通道</item>
</plurals>
<plurals name="import_total_success">
<item quantity="other">匯入了 %d 個通道</item>
</plurals>
<plurals name="set_excluded_applications">
<item quantity="other">排除了 %d 個應用程式</item>
</plurals>
<string name="add_peer">新增端點</string>
<string name="addresses">位址</string>
<string name="allow_remote_control_intents_title">允許遠端控制應用程式</string>
<string name="allowed_ips">允許的 IPs</string>
<string name="bad_config_context">錯誤設定: %1$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">: 必須是不大於 65535 的正整數</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 code</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">請取得 root 存取權再試一次</string>
<string name="error_up">起用通道錯誤: %s</string>
<string name="exclude_private_ips">排除私有 IPs</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_random">(隨機)</string>
<string name="illegal_filename_error">不合規定的檔名「%s」</string>
<string name="import_error">沒辦法匯入通道: %s</string>
<string name="import_from_qr_code">從 QR Code 匯入通道</string>
<string name="import_success">匯入了「%s」</string>
<string name="interface_title">界面</string>
<string name="key_length_explanation_base64">: WireGuard 的 base64 密鑰必須是 44 個字元 (32 個位元組)</string>
<string name="key_length_explanation_binary">: WireGuard 密鑰必須是 32 個位元組</string>
<string name="key_length_explanation_hex">: WireGuard 的16進位密鑰必須是 64 個字元 (32 個位元組)</string>
<string name="listen_port">監聽連接埠</string>
<string name="log_export_success">儲存到「%s」</string>
<string name="log_export_title">匯出日誌檔</string>
<string name="save">儲存</string>
<string name="select_all">全選</string>
<string name="toggle_all">切換全部</string>
</resources>
+4
View File
@@ -101,6 +101,10 @@
<string name="dark_theme_summary_on">Currently using dark (night) theme</string>
<string name="dark_theme_title">Use dark theme</string>
<string name="delete">Delete</string>
<string name="tv_delete">Select tunnel to delete</string>
<string name="tv_select_a_storage_drive">Select a storage drive</string>
<string name="tv_no_file_picker">Please install a file management utility to browse files</string>
<string name="tv_add_tunnel_get_started">Add a tunnel to get started</string>
<string name="disable_config_export_title">Disable config exporting</string>
<string name="disable_config_export_description">Disabling config exporting makes private keys less accessible</string>
<string name="dns_servers">DNS servers</string>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="tv_primary_color">#ff212121</color>
<color name="tv_card_background">@color/tv_primary_color</color>
<color name="tv_card_delete_background">#b00020</color>
</resources>
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="TvTheme" parent="Theme.MaterialComponents.NoActionBar">
<item name="colorPrimary">@color/tv_primary_color</item>
<item name="colorOnPrimary">#fffafafa</item>
<item name="colorPrimaryDark">@color/tv_primary_color</item>
<item name="colorPrimaryVariant">#ff484848</item>
<item name="colorSecondary">#ff4285f4</item>
<item name="colorOnSecondary">#ff0059c1</item>
<item name="colorSurface">@color/tv_primary_color</item>
<item name="colorOnSurface">#fffafafa</item>
<item name="colorBackground">@color/tv_primary_color</item>
<item name="colorMultiselectActiveBackground">@color/list_multiselect_background</item>
<item name="colorControlNormal">#fffafafa</item>
<item name="elevationOverlayEnabled">false</item>
<item name="android:statusBarColor">@color/tv_primary_color</item>
<item name="android:windowBackground">@color/tv_primary_color</item>
<item name="alertDialogTheme">@style/AppTheme.Dialog</item>
<item name="materialAlertDialogTheme">@style/AppTheme.Dialog</item>
<item name="textInputStyle">@style/TextInputLayoutBase</item>
<item name="materialCardViewStyle">@style/TvTheme.MaterialCardView</item>
</style>
<style name="TvTheme.MaterialCardView" parent="Widget.MaterialComponents.CardView">
<item name="cornerRadius">4dp</item>
<item name="cardElevation">8dp</item>
<item name="contentPadding">8dp</item>
<item name="cardBackgroundColor">?attr/elevationOverlayColor</item>
</style>
</resources>
+2 -2
View File
@@ -1,6 +1,6 @@
buildscript {
ext {
wireguardVersionCode = 478
wireguardVersionName = '1.0.20200920'
wireguardVersionCode = 484
wireguardVersionName = '1.0.20201216'
}
}