summaryrefslogtreecommitdiffhomepage
path: root/ui
diff options
context:
space:
mode:
Diffstat (limited to 'ui')
-rw-r--r--ui/build.gradle.kts17
-rw-r--r--ui/src/debug/res/values/strings.xml2
-rw-r--r--ui/src/main/AndroidManifest.xml7
-rw-r--r--ui/src/main/java/com/wireguard/android/QuickTileService.kt12
-rw-r--r--ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt7
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt10
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt27
-rw-r--r--ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt131
-rw-r--r--ui/src/main/java/com/wireguard/android/model/TunnelManager.kt5
-rw-r--r--ui/src/main/java/com/wireguard/android/updater/Updater.kt14
-rw-r--r--ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt2
-rw-r--r--ui/src/main/java/com/wireguard/android/viewmodel/ConfigDetail.kt22
-rw-r--r--ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt72
-rw-r--r--ui/src/main/java/com/wireguard/android/viewmodel/PeerDetail.kt85
-rw-r--r--ui/src/main/res/layout/http_proxy_menu_item.xml8
-rw-r--r--ui/src/main/res/layout/tunnel_detail_fragment.xml111
-rw-r--r--ui/src/main/res/layout/tunnel_detail_peer.xml6
-rw-r--r--ui/src/main/res/layout/tunnel_editor_fragment.xml105
-rw-r--r--ui/src/main/res/values-da-rDK/strings.xml4
-rw-r--r--ui/src/main/res/values-et-rEE/strings.xml4
-rw-r--r--ui/src/main/res/values-ja/strings.xml15
-rw-r--r--ui/src/main/res/values-sv-rSE/strings.xml21
-rw-r--r--ui/src/main/res/values-vi-rVN/strings.xml28
-rw-r--r--ui/src/main/res/values-zh-rCN/strings.xml10
-rw-r--r--ui/src/main/res/values/strings.xml8
25 files changed, 671 insertions, 62 deletions
diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts
index 34a32d5b..fff3acd9 100644
--- a/ui/build.gradle.kts
+++ b/ui/build.gradle.kts
@@ -2,9 +2,9 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-import com.android.build.gradle.tasks.ExtractSupportedLocalesTask
val pkg: String = providers.gradleProperty("wireguardPackageName").get()
+val appID: String = providers.gradleProperty("wireguardApplicationID").get()
plugins {
alias(libs.plugins.android.application)
@@ -14,7 +14,7 @@ plugins {
}
android {
- compileSdk = 33
+ compileSdk = 34
buildFeatures {
buildConfig = true
dataBinding = true
@@ -22,9 +22,9 @@ android {
}
namespace = pkg
defaultConfig {
- applicationId = pkg
+ applicationId = appID
minSdk = 21
- targetSdk = 33
+ targetSdk = 34
versionCode = providers.gradleProperty("wireguardVersionCode").get().toInt()
versionName = grgit.describe {
tags = true
@@ -37,6 +37,14 @@ android {
targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
}
+ signingConfigs {
+ named("debug") {
+ storeFile = File("/src/wireguard-android/keystore/debug.keystore")
+ storePassword = "android"
+ keyAlias = "androiddebugkey"
+ keyPassword = "android"
+ }
+ }
buildTypes {
release {
isMinifyEnabled = true
@@ -53,6 +61,7 @@ android {
debug {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
+ signingConfig = signingConfigs.getByName("debug")
}
create("googleplay") {
initWith(getByName("release"))
diff --git a/ui/src/debug/res/values/strings.xml b/ui/src/debug/res/values/strings.xml
index 947b7381..af58a39d 100644
--- a/ui/src/debug/res/values/strings.xml
+++ b/ui/src/debug/res/values/strings.xml
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
- <string name="app_name" translatable="false">WireGuard β</string>
+ <string name="app_name" translatable="false">(m7n.eu) WireGuard β</string>
</resources>
diff --git a/ui/src/main/AndroidManifest.xml b/ui/src/main/AndroidManifest.xml
index 72617f3b..8f779f79 100644
--- a/ui/src/main/AndroidManifest.xml
+++ b/ui/src/main/AndroidManifest.xml
@@ -3,7 +3,9 @@
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="internalOnly">
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
+ <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
@@ -159,5 +161,10 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent>
+
+ <intent>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
+ </intent>
</queries>
</manifest>
diff --git a/ui/src/main/java/com/wireguard/android/QuickTileService.kt b/ui/src/main/java/com/wireguard/android/QuickTileService.kt
index 7cd13e6e..f74c8bdf 100644
--- a/ui/src/main/java/com/wireguard/android/QuickTileService.kt
+++ b/ui/src/main/java/com/wireguard/android/QuickTileService.kt
@@ -4,6 +4,7 @@
*/
package com.wireguard.android
+import android.app.PendingIntent
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
@@ -53,7 +54,12 @@ class QuickTileService : TileService() {
null -> {
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- startActivityAndCollapse(intent)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ startActivityAndCollapse(PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE))
+ } else {
+ @Suppress("DEPRECATION")
+ startActivityAndCollapse(intent)
+ }
}
else -> {
unlockAndRun {
@@ -82,13 +88,13 @@ class QuickTileService : TileService() {
val icon = SlashDrawable(resources.getDrawable(R.drawable.ic_tile, Application.get().theme))
icon.setAnimationEnabled(false) /* Unfortunately we can't have animations, since Icons are marshaled. */
icon.setSlashed(false)
- var b = Bitmap.createBitmap(icon.intrinsicWidth, icon.intrinsicHeight, Bitmap.Config.ARGB_8888) ?: return
+ var b = Bitmap.createBitmap(icon.intrinsicWidth, icon.intrinsicHeight, Bitmap.Config.ARGB_8888)
var c = Canvas(b)
icon.setBounds(0, 0, c.width, c.height)
icon.draw(c)
iconOn = Icon.createWithBitmap(b)
icon.setSlashed(true)
- b = Bitmap.createBitmap(icon.intrinsicWidth, icon.intrinsicHeight, Bitmap.Config.ARGB_8888) ?: return
+ b = Bitmap.createBitmap(icon.intrinsicWidth, icon.intrinsicHeight, Bitmap.Config.ARGB_8888)
c = Canvas(b)
icon.setBounds(0, 0, c.width, c.height)
icon.draw(c)
diff --git a/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt b/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt
index afba41cb..6b2040e1 100644
--- a/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt
+++ b/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt
@@ -20,6 +20,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.wireguard.android.BR
import com.wireguard.android.R
+import com.wireguard.android.backend.Dhcp;
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler
import com.wireguard.android.widget.ToggleSwitch
import com.wireguard.android.widget.ToggleSwitch.OnBeforeCheckedChangeListener
@@ -170,6 +171,12 @@ object BindingAdapters {
}
@JvmStatic
+ @BindingAdapter("android:text")
+ fun setDhcpLeaseSetText(view: TextView, dhcp: Dhcp?) {
+ view.text = if (dhcp?.leases != null) Attribute.join(dhcp.leases.map { it }) else ""
+ }
+
+ @JvmStatic
fun tryParseInt(s: String?): Int {
if (s == null)
return 0
diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt
index 81d8e8c6..fce14d20 100644
--- a/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt
+++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt
@@ -5,6 +5,7 @@
package com.wireguard.android.fragment
import android.os.Bundle
+import android.util.Log;
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
@@ -21,6 +22,7 @@ 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 com.wireguard.android.viewmodel.ConfigDetail
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -79,7 +81,9 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider {
} else {
lifecycleScope.launch {
try {
- binding.config = newTunnel.getConfigAsync()
+ var config = newTunnel.getConfigDetailAsync()
+ binding.config = config
+ Log.i(TAG, "onSelectedTunnelChanged " + config + ", " + config.config)
} catch (_: Throwable) {
binding.config = null
}
@@ -147,4 +151,8 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider {
}
}
}
+
+ companion object {
+ private const val TAG = "WireGuard/TunnelDetailFragment"
+ }
}
diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt
index edf4b226..6be27f94 100644
--- a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt
+++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt
@@ -16,13 +16,17 @@ import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
+import android.widget.ArrayAdapter
+import android.widget.AutoCompleteTextView
import android.widget.EditText
+import android.widget.Filter
import android.widget.Toast
import androidx.core.os.BundleCompat
import androidx.core.view.MenuProvider
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar
+import com.google.android.material.textfield.TextInputLayout
import com.wireguard.android.Application
import com.wireguard.android.R
import com.wireguard.android.backend.Tunnel
@@ -32,6 +36,7 @@ import com.wireguard.android.util.AdminKnobs
import com.wireguard.android.util.BiometricAuthenticator
import com.wireguard.android.util.ErrorMessages
import com.wireguard.android.viewmodel.ConfigProxy
+import com.wireguard.android.viewmodel.Constants
import com.wireguard.config.Config
import kotlinx.coroutines.launch
@@ -43,6 +48,21 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
private var binding: TunnelEditorFragmentBinding? = null
private var tunnel: ObservableTunnel? = null
+ private class MaterialSpinnerAdapter<T>(context: Context, resource: Int, private val objects: List<T>) : ArrayAdapter<T>(context, resource, objects) {
+ private val _filter: Filter by lazy {
+ object : Filter() {
+ override fun performFiltering(constraint: CharSequence?): FilterResults {
+ return FilterResults()
+ }
+
+ override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
+ }
+ }
+ }
+
+ override fun getFilter(): Filter = _filter
+ }
+
private fun onConfigLoaded(config: Config) {
binding?.config = ConfigProxy(config)
}
@@ -80,6 +100,13 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
executePendingBindings()
privateKeyTextLayout.setEndIconOnClickListener { config?.`interface`?.generateKeyPair() }
}
+
+ var httpProxyMenu = binding?.root?.findViewById<TextInputLayout>(R.id.http_proxy_menu)
+ var httpProxyItems = listOf(Constants.HTTP_PROXY_NONE, Constants.HTTP_PROXY_MANUAL, Constants.HTTP_PROXY_PAC)
+ var httpProxyAdapter = MaterialSpinnerAdapter(requireContext(), R.layout.http_proxy_menu_item, httpProxyItems)
+ var httpProxyMenuText = httpProxyMenu?.editText as? AutoCompleteTextView
+ httpProxyMenuText?.setAdapter(httpProxyAdapter)
+
return binding?.root
}
diff --git a/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt b/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt
index aa237aee..55c84df5 100644
--- a/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt
+++ b/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt
@@ -7,12 +7,20 @@ package com.wireguard.android.model
import android.util.Log
import androidx.databinding.BaseObservable
import androidx.databinding.Bindable
+import com.wireguard.android.Application
import com.wireguard.android.BR
+import com.wireguard.android.backend.Dhcp
import com.wireguard.android.backend.Statistics
import com.wireguard.android.backend.Tunnel
import com.wireguard.android.databinding.Keyed
import com.wireguard.android.util.applicationScope
+import com.wireguard.android.viewmodel.ConfigDetail
+import com.wireguard.android.viewmodel.PeerDetail
import com.wireguard.config.Config
+import com.wireguard.config.InetEndpoint
+import com.wireguard.config.InetNetwork
+import com.wireguard.crypto.Key
+import java.util.Optional
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -55,7 +63,18 @@ class ObservableTunnel internal constructor(
}
fun onStateChanged(state: Tunnel.State): Tunnel.State {
- if (state != Tunnel.State.UP) onStatisticsChanged(null)
+ if (state != Tunnel.State.UP) {
+ onStatisticsChanged(null)
+ onDhcpChanged(null)
+ Application.getCoroutineScope().launch {
+ onPeersReset()
+ }
+ } else {
+ configDetail?.peers?.forEach {
+ var endpoint: InetEndpoint? = it.peer?.endpoint?.orElse(null)
+ it.endpoint = Optional.ofNullable(endpoint?.getResolved()?.orElse(null));
+ }
+ }
this.state = state
notifyPropertyChanged(BR.state)
return state
@@ -68,6 +87,7 @@ class ObservableTunnel internal constructor(
this@ObservableTunnel.state
}
+ private var configDetail: ConfigDetail? = if (config != null) ConfigDetail(config) else null
@get:Bindable
var config = config
@@ -86,7 +106,11 @@ class ObservableTunnel internal constructor(
private set
suspend fun getConfigAsync(): Config = withContext(Dispatchers.Main.immediate) {
- config ?: manager.getTunnelConfig(this@ObservableTunnel)
+ config ?: manager.getTunnelConfig(this@ObservableTunnel).config!!
+ }
+
+ suspend fun getConfigDetailAsync(): ConfigDetail = withContext(Dispatchers.Main.immediate) {
+ configDetail ?: manager.getTunnelConfig(this@ObservableTunnel)
}
suspend fun setConfigAsync(config: Config): Config = withContext(Dispatchers.Main.immediate) {
@@ -98,10 +122,11 @@ class ObservableTunnel internal constructor(
}
}
- fun onConfigChanged(config: Config?): Config? {
+ fun onConfigChanged(config: Config?): ConfigDetail? {
+ this.configDetail = ConfigDetail(config)
this.config = config
notifyPropertyChanged(BR.config)
- return config
+ return configDetail
}
@@ -136,6 +161,104 @@ class ObservableTunnel internal constructor(
return statistics
}
+ @get:Bindable
+ var dhcp: Dhcp? = null
+ private set
+
+ override fun onDhcpChange(newDhcp: Dhcp) {
+ onDhcpChanged(newDhcp)
+ }
+
+ fun onDhcpChanged(dhcp: Dhcp?): Dhcp? {
+ this.dhcp = dhcp
+ notifyPropertyChanged(BR.dhcp)
+ return dhcp
+ }
+
+ // Remove dynamic peers, and reset static peers
+ fun onPeersReset() {
+ Log.i(TAG, "ObservableTunnel onPeersReset")
+ var toRemove: MutableList<PeerDetail> = ArrayList()
+
+ configDetail?.peers?.forEach {
+ if (it.peer == null) {
+ toRemove.add(it)
+ } else {
+ it.endpoint = Optional.empty()
+ }
+ }
+
+ toRemove.forEach {
+ Log.i(TAG, "ObservableTunnel remove " + it)
+ configDetail?.peers?.remove(it)
+ }
+ }
+
+ override fun onEndpointChange(publicKey: Key, newEndpoint: InetEndpoint?) {
+ Application.getCoroutineScope().launch {
+ onEndpointChanged(publicKey, newEndpoint)
+ }
+ }
+
+ private fun onEndpointChanged(publicKey: Key, newEndpoint: InetEndpoint?) {
+
+ Log.i(TAG, "ObservableTunnel onEndpointChange " + newEndpoint)
+ var peer: PeerDetail? = null
+
+ configDetail?.peers?.forEach {
+ if (it.publicKey.equals(publicKey) == true) {
+ Log.i(TAG, "ObservableTunnel peer " + it + ", " + it.peer)
+ peer = it;
+ }
+ }
+
+ if (peer == null) {
+ Log.i(TAG, "ObservableTunnel create peer " + publicKey)
+ peer = PeerDetail(publicKey)
+ configDetail?.peers?.add(peer)
+ }
+
+ var peer2: PeerDetail = peer!!
+
+ if (newEndpoint != null) {
+ peer2.endpoint = newEndpoint.getResolved()
+ } else {
+ var peer3 = peer2.peer
+ peer2.endpoint = if (peer3 != null) peer3.endpoint else Optional.empty()
+ }
+ }
+
+ fun lookupPeer(publicKey: Key): PeerDetail {
+ configDetail?.peers?.forEach {
+ if (it.publicKey.equals(publicKey) == true) {
+ Log.i(TAG, "ObservableTunnel peer " + it + ", " + it.peer)
+ return it
+ }
+ }
+
+ Log.i(TAG, "ObservableTunnel create peer " + publicKey)
+ var peer: PeerDetail = PeerDetail(publicKey)
+ configDetail?.peers?.add(peer)
+
+ return peer
+ }
+
+ override fun onAllowedIpsChange(publicKey: Key, addNetworks: List<InetNetwork>?, removeNetworks: List<InetNetwork>?) {
+ Application.getCoroutineScope().launch {
+ onAllowedIpsChanged(publicKey, addNetworks, removeNetworks)
+ }
+ }
+
+ private fun onAllowedIpsChanged(publicKey: Key, addNetworks: List<InetNetwork>?, removeNetworks: List<InetNetwork>?) {
+ var peer: PeerDetail = lookupPeer(publicKey)
+
+ removeNetworks?.let() {
+ peer.allowedIps.removeAll(removeNetworks)
+ }
+ addNetworks?.let() {
+ peer.allowedIps.addAll(addNetworks)
+ }
+ }
suspend fun deleteAsync() = manager.delete(this)
diff --git a/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt
index ba873fa6..960adcaa 100644
--- a/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt
+++ b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt
@@ -24,6 +24,7 @@ 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.android.viewmodel.ConfigDetail
import com.wireguard.config.Config
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
@@ -94,7 +95,7 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
applicationScope.launch { UserKnobs.setLastUsedTunnel(value?.name) }
}
- suspend fun getTunnelConfig(tunnel: ObservableTunnel): Config = withContext(Dispatchers.Main.immediate) {
+ suspend fun getTunnelConfig(tunnel: ObservableTunnel): ConfigDetail = withContext(Dispatchers.Main.immediate) {
tunnel.onConfigChanged(withContext(Dispatchers.IO) { configStore.load(tunnel.name) })!!
}
@@ -156,7 +157,7 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
tunnel.onConfigChanged(withContext(Dispatchers.IO) {
getBackend().setState(tunnel, tunnel.state, config)
configStore.save(tunnel.name, config)
- })!!
+ })!!.config!!
}
suspend fun setTunnelName(tunnel: ObservableTunnel, name: String): String = withContext(Dispatchers.Main.immediate) {
diff --git a/ui/src/main/java/com/wireguard/android/updater/Updater.kt b/ui/src/main/java/com/wireguard/android/updater/Updater.kt
index adcb3836..87adad96 100644
--- a/ui/src/main/java/com/wireguard/android/updater/Updater.kt
+++ b/ui/src/main/java/com/wireguard/android/updater/Updater.kt
@@ -386,13 +386,15 @@ object Updater {
context.packageManager.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong()))
}.requestedPermissions.contains(Manifest.permission.REQUEST_INSTALL_PACKAGES)
) {
- updaterScope.launch {
- val update = try {
- checkForUpdates()
- } catch (_: Throwable) {
- null
+ if (installer(context).isNotEmpty()) {
+ updaterScope.launch {
+ val update = try {
+ checkForUpdates()
+ } catch (_: Throwable) {
+ null
+ }
+ emitProgress(Progress.Corrupt(update?.fileName))
}
- emitProgress(Progress.Corrupt(update?.fileName))
}
return
}
diff --git a/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt b/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt
index d617adec..97be8c99 100644
--- a/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt
+++ b/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt
@@ -122,6 +122,8 @@ object ErrorMessages {
return resources.getString(R.string.bad_config_explanation_udp_port)
} else if (bce.location == BadConfigException.Location.MTU) {
return resources.getString(R.string.bad_config_explanation_positive_number)
+ } else if (bce.location == BadConfigException.Location.HTTP_PROXY) {
+ return resources.getString(R.string.bad_config_explanation_http_proxy)
} else if (bce.location == BadConfigException.Location.PERSISTENT_KEEPALIVE) {
return resources.getString(R.string.bad_config_explanation_pka)
}
diff --git a/ui/src/main/java/com/wireguard/android/viewmodel/ConfigDetail.kt b/ui/src/main/java/com/wireguard/android/viewmodel/ConfigDetail.kt
new file mode 100644
index 00000000..af95a86a
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/viewmodel/ConfigDetail.kt
@@ -0,0 +1,22 @@
+package com.wireguard.android.viewmodel
+
+import androidx.databinding.ObservableArrayList
+import androidx.databinding.ObservableList
+
+import com.wireguard.config.Config
+
+class ConfigDetail {
+ val config: Config?
+ val peers: ObservableList<PeerDetail> = ObservableArrayList()
+
+ constructor(other: Config?) {
+ config = other
+ if (other != null) {
+ other.peers.forEach {
+ val detail = PeerDetail(it)
+ peers.add(detail)
+ detail.bind(this)
+ }
+ }
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt b/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt
index 004ebed1..81a548f9 100644
--- a/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt
+++ b/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt
@@ -4,6 +4,8 @@
*/
package com.wireguard.android.viewmodel
+import android.net.Uri
+import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import androidx.databinding.BaseObservable
@@ -18,6 +20,12 @@ import com.wireguard.crypto.Key
import com.wireguard.crypto.KeyFormatException
import com.wireguard.crypto.KeyPair
+object Constants {
+ const val HTTP_PROXY_NONE = "None"
+ const val HTTP_PROXY_MANUAL = "Manual"
+ const val HTTP_PROXY_PAC = "Proxy Auto-Config"
+}
+
class InterfaceProxy : BaseObservable, Parcelable {
@get:Bindable
val excludedApplications: ObservableList<String> = ObservableArrayList()
@@ -54,6 +62,44 @@ class InterfaceProxy : BaseObservable, Parcelable {
}
@get:Bindable
+ var httpProxyMenu: String = ""
+ set(value) {
+ field = value
+ notifyPropertyChanged(BR.httpProxyMenu)
+ notifyPropertyChanged(BR.httpProxyManualVisibility)
+ notifyPropertyChanged(BR.httpProxyPacVisibility)
+ }
+
+ @get:Bindable
+ var httpProxyManualVisibility: Int = 0
+ get() = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) android.view.View.GONE else (if (httpProxyMenu == Constants.HTTP_PROXY_MANUAL) android.view.View.VISIBLE else android.view.View.GONE)
+
+ @get:Bindable
+ var httpProxyHostname: String = ""
+ set(value) {
+ field = value
+ notifyPropertyChanged(BR.httpProxyHostname)
+ }
+
+ @get:Bindable
+ var httpProxyPort: String = ""
+ set(value) {
+ field = value
+ notifyPropertyChanged(BR.httpProxyPort)
+ }
+
+ @get:Bindable
+ var httpProxyPac: String = ""
+ set(value) {
+ field = value
+ notifyPropertyChanged(BR.httpProxyPac)
+ }
+
+ @get:Bindable
+ var httpProxyPacVisibility: Int = 0
+ get() = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) android.view.View.GONE else (if (httpProxyMenu == Constants.HTTP_PROXY_PAC) android.view.View.VISIBLE else android.view.View.GONE)
+
+ @get:Bindable
var privateKey: String = ""
set(value) {
field = value
@@ -76,6 +122,10 @@ class InterfaceProxy : BaseObservable, Parcelable {
parcel.readStringList(includedApplications)
listenPort = parcel.readString() ?: ""
mtu = parcel.readString() ?: ""
+ httpProxyMenu = parcel.readString() ?: ""
+ httpProxyHostname = parcel.readString() ?: ""
+ httpProxyPort = parcel.readString() ?: ""
+ httpProxyPac = parcel.readString() ?: ""
privateKey = parcel.readString() ?: ""
}
@@ -87,6 +137,10 @@ class InterfaceProxy : BaseObservable, Parcelable {
includedApplications.addAll(other.includedApplications)
listenPort = other.listenPort.map { it.toString() }.orElse("")
mtu = other.mtu.map { it.toString() }.orElse("")
+ httpProxyHostname = other.httpProxy.map { if (it.getHost().startsWith('[') && it.getHost().endsWith(']')) it.getHost().substring(1, it.getHost().length-1) else it.getHost() }.orElse("")
+ httpProxyPort = other.httpProxy.map { if (it.getPort() <= 0) "8080" else it.getPort().toString() }.orElse("")
+ httpProxyPac = other.httpProxy.map { it.getPacFileUrl().toString() }.orElse("")
+ httpProxyMenu = other.httpProxy.map { if (it.getPacFileUrl() != Uri.EMPTY) Constants.HTTP_PROXY_PAC else if (it.getHost() != "") Constants.HTTP_PROXY_MANUAL else Constants.HTTP_PROXY_NONE }.orElse(Constants.HTTP_PROXY_NONE)
val keyPair = other.keyPair
privateKey = keyPair.privateKey.toBase64()
}
@@ -111,6 +165,20 @@ class InterfaceProxy : BaseObservable, Parcelable {
if (includedApplications.isNotEmpty()) builder.includeApplications(includedApplications)
if (listenPort.isNotEmpty()) builder.parseListenPort(listenPort)
if (mtu.isNotEmpty()) builder.parseMtu(mtu)
+ if (Constants.HTTP_PROXY_MANUAL.equals(httpProxyMenu) && httpProxyHostname.isNotEmpty()) {
+ var httpProxy: String
+ if (httpProxyHostname.contains(":")) {
+ httpProxy = "[" + httpProxyHostname + "]"
+ } else {
+ httpProxy = httpProxyHostname
+ }
+ if (httpProxyPort.isNotEmpty()) {
+ httpProxy += ":" + httpProxyPort;
+ }
+ builder.parseHttpProxy(httpProxy)
+ } else if (Constants.HTTP_PROXY_PAC.equals(httpProxyMenu) && httpProxyPac.isNotEmpty()) {
+ builder.parseHttpProxy("pac:" + httpProxyPac)
+ }
if (privateKey.isNotEmpty()) builder.parsePrivateKey(privateKey)
return builder.build()
}
@@ -122,6 +190,10 @@ class InterfaceProxy : BaseObservable, Parcelable {
dest.writeStringList(includedApplications)
dest.writeString(listenPort)
dest.writeString(mtu)
+ dest.writeString(httpProxyMenu)
+ dest.writeString(httpProxyHostname)
+ dest.writeString(httpProxyPort)
+ dest.writeString(httpProxyPac)
dest.writeString(privateKey)
}
diff --git a/ui/src/main/java/com/wireguard/android/viewmodel/PeerDetail.kt b/ui/src/main/java/com/wireguard/android/viewmodel/PeerDetail.kt
new file mode 100644
index 00000000..80b32fd5
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/viewmodel/PeerDetail.kt
@@ -0,0 +1,85 @@
+package com.wireguard.android.viewmodel
+
+import android.util.Log
+import androidx.databinding.BaseObservable
+import androidx.databinding.Bindable
+import androidx.databinding.Observable
+import androidx.databinding.ObservableList
+import androidx.databinding.ObservableArrayList
+
+import com.wireguard.android.BR
+import com.wireguard.config.InetEndpoint
+import com.wireguard.config.InetNetwork
+import com.wireguard.config.Peer
+import com.wireguard.crypto.Key;
+
+import java.util.Optional;
+
+import kotlin.collections.LinkedHashSet
+
+
+class PeerDetail : BaseObservable {
+ var peer: Peer?
+ private var owner: ConfigDetail? = null
+
+ @get:Bindable
+ var publicKey: Key
+
+ @get:Bindable
+ var allowedIps: ObservableList<InetNetwork> = ObservableArrayList<InetNetwork>()
+
+ @get:Bindable
+ var endpoint: Optional<InetEndpoint> = Optional.empty()
+ get() {
+ if (!field.isEmpty()) {
+ return field
+ } else {
+ return Optional.ofNullable(peer?.endpoint?.get())
+ }
+ }
+
+ set(value) {
+ Log.i(TAG, "notifyPropertyChanged endpoint " + this + ", " + value)
+ field = value
+ notifyPropertyChanged(BR.endpoint)
+ }
+
+ @get:Bindable
+ var persistentKeepalive: Optional<Int> = Optional.empty()
+
+ constructor(other: Peer) {
+ peer = other
+ publicKey = other.getPublicKey()
+ allowedIps.addAll(other.getAllowedIps())
+ endpoint = other.getEndpoint();
+ persistentKeepalive = other.getPersistentKeepalive()
+ }
+
+ constructor(publicKey: Key) {
+ peer = null
+ this.publicKey = publicKey
+ }
+
+ fun bind(owner: ConfigDetail) {
+ this.owner = owner
+ }
+
+ override fun addOnPropertyChangedCallback (callback: Observable.OnPropertyChangedCallback) {
+ Log.i(TAG, "addOnPropertyChangedCallback " + this + ", " + callback)
+ super.addOnPropertyChangedCallback(callback)
+ }
+
+ /**
+ * Converts the {@code Peer} into a string suitable for debugging purposes. The {@code Peer} is
+ * identified by its public key and (if known) its endpoint.
+ *
+ * @return a concise single-line identifier for the {@code Peer}
+ */
+ override fun toString(): String {
+ return "(Peer " + publicKey.toBase64() + ")"
+ }
+
+ companion object {
+ private const val TAG = "WireGuard/PeerDetail"
+ }
+}
diff --git a/ui/src/main/res/layout/http_proxy_menu_item.xml b/ui/src/main/res/layout/http_proxy_menu_item.xml
new file mode 100644
index 00000000..8ad5c026
--- /dev/null
+++ b/ui/src/main/res/layout/http_proxy_menu_item.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="16dp"
+ android:ellipsize="end"
+ android:maxLines="1"
+/>
diff --git a/ui/src/main/res/layout/tunnel_detail_fragment.xml b/ui/src/main/res/layout/tunnel_detail_fragment.xml
index 332df04a..9b17a06c 100644
--- a/ui/src/main/res/layout/tunnel_detail_fragment.xml
+++ b/ui/src/main/res/layout/tunnel_detail_fragment.xml
@@ -5,6 +5,8 @@
<data>
+ <import type="android.os.Build" />
+
<import type="com.wireguard.android.backend.Tunnel.State" />
<import type="com.wireguard.android.util.ClipboardUtils" />
@@ -19,7 +21,7 @@
<variable
name="config"
- type="com.wireguard.config.Config" />
+ type="com.wireguard.android.viewmodel.ConfigDetail" />
</data>
<ScrollView
@@ -116,7 +118,7 @@
android:nextFocusForward="@id/addresses_text"
android:onClick="@{ClipboardUtils::copyTextView}"
android:singleLine="true"
- android:text="@{config.interface.keyPair.publicKey.toBase64}"
+ android:text="@{config.config.interface.keyPair.publicKey.toBase64}"
android:textAppearance="?attr/textAppearanceBodyLarge"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/public_key_label"
@@ -129,7 +131,7 @@
android:layout_marginTop="8dp"
android:labelFor="@+id/addresses_text"
android:text="@string/addresses"
- android:visibility="@{config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{config.config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/public_key_text" />
@@ -139,14 +141,41 @@
android:layout_height="wrap_content"
android:contentDescription="@string/addresses"
android:nextFocusUp="@id/public_key_text"
+ android:nextFocusDown="@id/dynamic_addresses_text"
+ android:nextFocusForward="@id/dynamic_addresses_text"
+ android:onClick="@{ClipboardUtils::copyTextView}"
+ android:text="@{config.config.interface.addresses}"
+ android:textAppearance="?attr/textAppearanceBodyLarge"
+ android:visibility="@{config.config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/addresses_label"
+ tools:text="fc00:bbbb:bbbb:bb11::3:368b/128" />
+
+ <TextView
+ android:id="@+id/dynamic_addresses_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:labelFor="@+id/dynamic_addresses_text"
+ android:text="@string/dynamic_addresses"
+ android:visibility="@{tunnel.dhcp == null ? android.view.View.GONE : android.view.View.VISIBLE}"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/addresses_text" />
+
+ <TextView
+ android:id="@+id/dynamic_addresses_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/dynamic_addresses"
+ android:nextFocusUp="@id/addresses_text"
android:nextFocusDown="@id/dns_servers_text"
android:nextFocusForward="@id/dns_servers_text"
android:onClick="@{ClipboardUtils::copyTextView}"
- android:text="@{config.interface.addresses}"
+ android:text="@{tunnel.dhcp}"
android:textAppearance="?attr/textAppearanceBodyLarge"
- android:visibility="@{config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{tunnel.dhcp == null ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/addresses_label"
+ app:layout_constraintTop_toBottomOf="@+id/dynamic_addresses_label"
tools:text="fc00:bbbb:bbbb:bb11::3:368b/128" />
<TextView
@@ -156,22 +185,22 @@
android:layout_marginTop="8dp"
android:labelFor="@+id/dns_servers_text"
android:text="@string/dns_servers"
- android:visibility="@{config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{config.config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/addresses_text" />
+ app:layout_constraintTop_toBottomOf="@id/dynamic_addresses_text" />
<TextView
android:id="@+id/dns_servers_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/dns_servers"
- android:nextFocusUp="@id/addresses_text"
+ android:nextFocusUp="@id/dynamic_addresses_text"
android:nextFocusDown="@id/dns_search_domains_text"
android:nextFocusForward="@id/dns_search_domains_text"
android:onClick="@{ClipboardUtils::copyTextView}"
- android:text="@{config.interface.dnsServers}"
+ android:text="@{config.config.interface.dnsServers}"
android:textAppearance="?attr/textAppearanceBodyLarge"
- android:visibility="@{config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{config.config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dns_servers_label"
tools:text="8.8.8.8, 8.8.4.4" />
@@ -183,7 +212,7 @@
android:layout_marginTop="8dp"
android:labelFor="@+id/dns_search_domain_text"
android:text="@string/dns_search_domains"
- android:visibility="@{config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{config.config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/dns_servers_text" />
@@ -196,9 +225,9 @@
android:nextFocusDown="@id/listen_port_text"
android:nextFocusForward="@id/listen_port_text"
android:onClick="@{ClipboardUtils::copyTextView}"
- android:text="@{config.interface.dnsSearchDomains}"
+ android:text="@{config.config.interface.dnsSearchDomains}"
android:textAppearance="?attr/textAppearanceBodyLarge"
- android:visibility="@{config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{config.config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dns_search_domains_label"
tools:text="zx2c4.com" />
@@ -210,7 +239,7 @@
android:layout_marginTop="8dp"
android:labelFor="@+id/listen_port_text"
android:text="@string/listen_port"
- android:visibility="@{!config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{!config.config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintEnd_toStartOf="@id/mtu_label"
app:layout_constraintHorizontal_weight="0.5"
app:layout_constraintStart_toStartOf="parent"
@@ -223,12 +252,12 @@
android:contentDescription="@string/listen_port"
android:nextFocusRight="@id/mtu_text"
android:nextFocusUp="@id/dns_search_domains_text"
- android:nextFocusDown="@id/applications_text"
+ android:nextFocusDown="@id/http_proxy_text"
android:nextFocusForward="@id/mtu_text"
android:onClick="@{ClipboardUtils::copyTextView}"
- android:text="@{config.interface.listenPort}"
+ android:text="@{config.config.interface.listenPort}"
android:textAppearance="?attr/textAppearanceBodyLarge"
- android:visibility="@{!config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{!config.config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintEnd_toStartOf="@id/mtu_label"
app:layout_constraintHorizontal_weight="0.5"
app:layout_constraintStart_toStartOf="parent"
@@ -242,7 +271,7 @@
android:layout_marginTop="8dp"
android:labelFor="@+id/mtu_text"
android:text="@string/mtu"
- android:visibility="@{!config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{!config.config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="0.5"
app:layout_constraintLeft_toRightOf="@id/listen_port_label"
@@ -256,11 +285,11 @@
android:contentDescription="@string/mtu"
android:nextFocusLeft="@id/listen_port_text"
android:nextFocusUp="@id/dns_servers_text"
- android:nextFocusForward="@id/applications_text"
+ android:nextFocusForward="@id/http_proxy_text"
android:onClick="@{ClipboardUtils::copyTextView}"
- android:text="@{config.interface.mtu}"
+ android:text="@{config.config.interface.mtu}"
android:textAppearance="?attr/textAppearanceBodyLarge"
- android:visibility="@{!config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{!config.config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="0.5"
app:layout_constraintStart_toEndOf="@id/listen_port_label"
@@ -276,15 +305,42 @@
app:constraint_referenced_ids="listen_port_text,mtu_text" />
<TextView
+ android:id="@+id/http_proxy_label"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:labelFor="@+id/http_proxy_text"
+ android:text="@string/http_proxy"
+ android:visibility="@{(Build.VERSION.SDK_INT &lt; Build.VERSION_CODES.Q || !config.config.interface.httpProxy.isPresent()) ? android.view.View.GONE : android.view.View.VISIBLE}"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/listen_port_mtu_barrier" />
+
+ <TextView
+ android:id="@+id/http_proxy_text"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/http_proxy"
+ android:nextFocusUp="@id/listen_port_text"
+ android:nextFocusDown="@id/applications_text"
+ android:nextFocusForward="@id/applications_text"
+ android:onClick="@{ClipboardUtils::copyTextView}"
+ android:text="@{config.config.interface.httpProxy}"
+ android:textAppearance="?attr/textAppearanceBodyLarge"
+ android:visibility="@{(Build.VERSION.SDK_INT &lt; Build.VERSION_CODES.Q || !config.config.interface.httpProxy.isPresent()) ? android.view.View.GONE : android.view.View.VISIBLE}"
+ app:layout_constraintTop_toBottomOf="@id/http_proxy_label"
+ app:layout_constraintStart_toStartOf="parent"
+ tools:text="http://example.com:8888" />
+
+ <TextView
android:id="@+id/applications_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/applications_text"
android:text="@string/applications"
- android:visibility="@{config.interface.includedApplications.isEmpty() &amp;&amp; config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{config.config.interface.includedApplications.isEmpty() &amp;&amp; config.config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/listen_port_mtu_barrier" />
+ app:layout_constraintTop_toBottomOf="@+id/http_proxy_text" />
<TextView
android:id="@+id/applications_text"
@@ -295,9 +351,9 @@
android:nextFocusDown="@id/peers_layout"
android:nextFocusForward="@id/peers_layout"
android:onClick="@{ClipboardUtils::copyTextView}"
- android:text="@{config.interface.includedApplications.isEmpty() ? @plurals/n_excluded_applications(config.interface.excludedApplications.size(), config.interface.excludedApplications.size()) : @plurals/n_included_applications(config.interface.includedApplications.size(), config.interface.includedApplications.size())}"
+ android:text="@{config.config.interface.includedApplications.isEmpty() ? @plurals/n_excluded_applications(config.config.interface.excludedApplications.size(), config.config.interface.excludedApplications.size()) : @plurals/n_included_applications(config.config.interface.includedApplications.size(), config.config.interface.includedApplications.size())}"
android:textAppearance="?attr/textAppearanceBodyLarge"
- android:visibility="@{config.interface.includedApplications.isEmpty() &amp;&amp; config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{config.config.interface.includedApplications.isEmpty() &amp;&amp; config.config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/applications_label"
tools:text="8 excluded" />
@@ -311,6 +367,7 @@
android:layout_marginTop="8dp"
android:divider="@null"
android:orientation="vertical"
+ app:fragment="@{fragment}"
app:items="@{config.peers}"
app:layout="@{@layout/tunnel_detail_peer}"
app:layout_constraintStart_toStartOf="parent"
@@ -318,4 +375,4 @@
tools:ignore="UselessLeaf" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
-</layout> \ No newline at end of file
+</layout>
diff --git a/ui/src/main/res/layout/tunnel_detail_peer.xml b/ui/src/main/res/layout/tunnel_detail_peer.xml
index 25081cea..89bb85ec 100644
--- a/ui/src/main/res/layout/tunnel_detail_peer.xml
+++ b/ui/src/main/res/layout/tunnel_detail_peer.xml
@@ -9,7 +9,7 @@
<variable
name="item"
- type="com.wireguard.config.Peer" />
+ type="com.wireguard.android.viewmodel.PeerDetail" />
</data>
<com.google.android.material.card.MaterialCardView
@@ -64,7 +64,7 @@
android:layout_marginTop="8dp"
android:labelFor="@+id/pre_shared_key_text"
android:text="@string/pre_shared_key"
- android:visibility="@{!item.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{!item.peer.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/public_key_text" />
@@ -81,7 +81,7 @@
android:singleLine="true"
android:text="@string/pre_shared_key_enabled"
android:textAppearance="?attr/textAppearanceBodyLarge"
- android:visibility="@{!item.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{!item.peer.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pre_shared_key_label"
tools:text="8VyS8W8XeMcBWfKp1GuG3/fZlnUQFkqMNbrdmZtVQIM=" />
diff --git a/ui/src/main/res/layout/tunnel_editor_fragment.xml b/ui/src/main/res/layout/tunnel_editor_fragment.xml
index 0350486b..42222399 100644
--- a/ui/src/main/res/layout/tunnel_editor_fragment.xml
+++ b/ui/src/main/res/layout/tunnel_editor_fragment.xml
@@ -5,6 +5,8 @@
<data>
+ <import type="android.os.Build" />
+
<import type="com.wireguard.android.util.ClipboardUtils" />
<import type="com.wireguard.android.widget.KeyInputFilter" />
@@ -210,7 +212,7 @@
android:imeOptions="actionNext"
android:inputType="textNoSuggestions|textVisiblePassword"
android:nextFocusUp="@id/addresses_label_text"
- android:nextFocusDown="@id/set_excluded_applications"
+ android:nextFocusDown="@id/http_proxy_hostname_text"
android:nextFocusForward="@id/mtu_text"
android:text="@={config.interface.dnsServers}" />
</com.google.android.material.textfield.TextInputLayout>
@@ -235,19 +237,112 @@
android:imeOptions="actionDone"
android:inputType="number"
android:nextFocusUp="@id/listen_port_text"
- android:nextFocusDown="@id/set_excluded_applications"
- android:nextFocusForward="@id/set_excluded_applications"
+ android:nextFocusDown="@id/http_proxy_hostname_text"
+ android:nextFocusForward="@id/http_proxy_hostname_text"
android:text="@={config.interface.mtu}"
android:textAlignment="center" />
</com.google.android.material.textfield.TextInputLayout>
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/http_proxy_menu"
+ style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:hint="@string/http_proxy"
+ app:expandedHintEnabled="false"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/dns_servers_label_layout">
+
+ <AutoCompleteTextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="none"
+ android:text="@={config.interface.httpProxyMenu}"
+ />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/http_proxy_hostname_label_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:hint="@string/http_proxy_hostname"
+ android:visibility="@{config.interface.httpProxyManualVisibility}"
+ app:layout_constraintEnd_toStartOf="@id/http_proxy_port_label_layout"
+ app:layout_constraintHorizontal_chainStyle="spread"
+ app:layout_constraintHorizontal_weight="0.7"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/http_proxy_menu">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/http_proxy_hostname_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:imeOptions="actionNext"
+ android:inputType="textNoSuggestions|textVisiblePassword"
+ android:nextFocusUp="@id/mtu_text"
+ android:nextFocusDown="@id/set_excluded_applications"
+ android:nextFocusForward="@id/http_proxy_port_text"
+ android:text="@={config.interface.httpProxyHostname}" />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/http_proxy_port_label_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:hint="@string/http_proxy_port"
+ android:visibility="@{config.interface.httpProxyManualVisibility}"
+ app:expandedHintEnabled="false"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_weight="0.3"
+ app:layout_constraintStart_toEndOf="@id/http_proxy_hostname_label_layout"
+ app:layout_constraintTop_toBottomOf="@id/http_proxy_menu">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/http_proxy_port_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:imeOptions="actionDone"
+ android:nextFocusUp="@id/mtu_text"
+ android:nextFocusDown="@id/http_proxy_pac_label_layout"
+ android:nextFocusForward="@id/http_proxy_pac_label_layout"
+ android:text="@={config.interface.httpProxyPort}"
+ android:textAlignment="center" />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/http_proxy_pac_label_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:hint="@string/http_proxy_pac"
+ android:visibility="@{config.interface.httpProxyPacVisibility}"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/http_proxy_hostname_label_layout">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/http_proxy_pac_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:imeOptions="actionNext"
+ android:inputType="textNoSuggestions|textVisiblePassword"
+ android:nextFocusUp="@id/http_proxy_hostname_text"
+ android:nextFocusDown="@id/set_excluded_applications"
+ android:nextFocusForward="@id/set_excluded_applications"
+ android:text="@={config.interface.httpProxyPac}" />
+ </com.google.android.material.textfield.TextInputLayout>
+
<com.google.android.material.button.MaterialButton
android:id="@+id/set_excluded_applications"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="4dp"
- android:nextFocusUp="@id/dns_servers_text"
+ android:nextFocusUp="@id/http_proxy_hostname_text"
android:nextFocusDown="@id/peers_layout"
android:nextFocusForward="@id/peers_layout"
android:onClick="@{fragment::onRequestSetExcludedIncludedApplications}"
@@ -256,7 +351,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/mtu_label_layout"
+ app:layout_constraintTop_toBottomOf="@id/http_proxy_pac_label_layout"
app:rippleColor="?attr/colorSecondary"
tools:text="4 excluded applications" />
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/ui/src/main/res/values-da-rDK/strings.xml b/ui/src/main/res/values-da-rDK/strings.xml
index 98f8de6e..88f13dc1 100644
--- a/ui/src/main/res/values-da-rDK/strings.xml
+++ b/ui/src/main/res/values-da-rDK/strings.xml
@@ -51,6 +51,9 @@
<string name="add_peer">Tilføj modpart</string>
<string name="addresses">Adresser</string>
<string name="applications">Applikationer</string>
+ <string name="allow_remote_control_intents_summary_off">Eksterne apps kan ikke slå tunneler til/fra (Anbefales)</string>
+ <string name="allow_remote_control_intents_summary_on">Eksterne apps må slå tunneler til/fra (Avanceret)</string>
+ <string name="allow_remote_control_intents_title">Tillad fjernstyring fra eksterne apps</string>
<string name="allowed_ips">Tilladte IP-adresser</string>
<string name="bad_config_context">%1$s\'s %2$s</string>
<string name="bad_config_context_top_level">%s</string>
@@ -85,6 +88,7 @@
<string name="tv_delete">Vælg tunnel du vil slette</string>
<string name="tv_select_a_storage_drive">Vælg et lagerdrev</string>
<string name="tv_add_tunnel_get_started">Tilføj en tunnel for at komme i gang</string>
+ <string name="donate_title">♥ Donér til WireGuard projektet</string>
<string name="disable_config_export_title">Deaktivér eksportering af konfiguration</string>
<string name="dns_servers">DNS-servere</string>
<string name="dns_search_domains">DNS-søgedomæner</string>
diff --git a/ui/src/main/res/values-et-rEE/strings.xml b/ui/src/main/res/values-et-rEE/strings.xml
index 692a0b8a..9beaafd9 100644
--- a/ui/src/main/res/values-et-rEE/strings.xml
+++ b/ui/src/main/res/values-et-rEE/strings.xml
@@ -185,6 +185,7 @@ Aitäh veelkord sinu panuse eest.</string>
<string name="private_key">Privaatvõti</string>
<string name="public_key">Avalik võti</string>
<string name="qr_code_hint">Vihje: tekita käsuga `qrencode -t ansiutf8 &lt; tunnel.conf`.</string>
+ <string name="quick_settings_tile_action">Lülita tunnel</string>
<string name="restore_on_boot_summary_off">Ei lülita seadme käivitumisel lubatud tunneleid sisse</string>
<string name="restore_on_boot_summary_on">Lülitab seadme käivitumisel lubatud tunnelid sisse</string>
<string name="restore_on_boot_title">Taasta seadme käivitumisel</string>
@@ -236,6 +237,9 @@ Aitäh veelkord sinu panuse eest.</string>
<string name="updater_download_progress_nototal">Uuenduse allalaadimine: %s</string>
<string name="updater_installing">Uuenduse paigaldamine…</string>
<string name="updater_failure">Uuendamine ebaõnnestus: %s. Uus katse hetke pärast…</string>
+ <string name="updater_corrupt_title">Rakendus rikutud</string>
+ <string name="updater_corrupt_message">See rakendus on rikutud. Palun laadi APK uuesti allpool lingitud veebilehelt. Pärast seda desinstalli rakendus ja installi allalaaditud APK uuesti.</string>
+ <string name="updater_corrupt_navigate">Ava veebileht</string>
<string name="version_summary">%1$s taustsüsteem %2$s</string>
<string name="version_summary_checking">Kontrollin %s taustsüsteemi versiooni</string>
<string name="version_summary_unknown">Tundmatu %s versioon</string>
diff --git a/ui/src/main/res/values-ja/strings.xml b/ui/src/main/res/values-ja/strings.xml
index c320d476..26e99af3 100644
--- a/ui/src/main/res/values-ja/strings.xml
+++ b/ui/src/main/res/values-ja/strings.xml
@@ -96,6 +96,7 @@
<string name="tv_add_tunnel_get_started">トンネルを追加して開始する</string>
<string name="donate_title">♥ WireGuard プロジェクトに寄付する</string>
<string name="donate_summary">すべての貢献が役立ちます</string>
+ <string name="donate_google_play_disappointment">WireGuard プロジェクトを支援していただきありがとうございます!\n\n残念ながら、Google のポリシーの影響で寄付のページへのリンクを記載することができません。見つけていただけることを願っています。\n\nもう一度、あなたの貢献に深く感謝します。</string>
<string name="disable_config_export_title">設定のエクスポートを無効にする</string>
<string name="disable_config_export_description">設定のエクスポートを無効にすると、秘密鍵にアクセスされにくくなります</string>
<string name="dns_servers">DNS サーバ</string>
@@ -170,6 +171,10 @@
<string name="private_key">秘密鍵</string>
<string name="public_key">公開鍵</string>
<string name="qr_code_hint">Tip: `qrencode -t ansiutf8 &lt; tunnel.conf` で生成できます</string>
+ <string name="quick_settings_tile_add_title">クイック設定パネルを追加</string>
+ <string name="quick_settings_tile_add_summary">ショートカットタイルを使用すると、最新のトンネルに切り替わります</string>
+ <string name="quick_settings_tile_add_failure">ショートカットタイルを追加できません: エラー %d</string>
+ <string name="quick_settings_tile_action">トンネルを切り替え</string>
<string name="restore_on_boot_summary_off">起動時にトンネルを有効化しない</string>
<string name="restore_on_boot_summary_on">起動時に、前回有効だったトンネルを有効化する</string>
<string name="restore_on_boot_title">起動時に復元</string>
@@ -214,6 +219,16 @@
<string name="type_name_go_userspace">Go ユーザースペース</string>
<string name="type_name_kernel_module">カーネルモジュール</string>
<string name="unknown_error">未知のエラー</string>
+ <string name="updater_avalable">アプリを更新できます。今すぐ更新してください。</string>
+ <string name="updater_action">ダウンロードして更新</string>
+ <string name="updater_rechecking">更新のメタデータを取得しています…</string>
+ <string name="updater_download_progress">更新のダウンロード中: %1$s / %2$s (%3$.2f%%)</string>
+ <string name="updater_download_progress_nototal">更新のダウンロード中: %s</string>
+ <string name="updater_installing">更新をインストール中…</string>
+ <string name="updater_failure">更新に失敗しました: %s. 一定時間後に再試行します…</string>
+ <string name="updater_corrupt_title">アプリケーションが破損しています</string>
+ <string name="updater_corrupt_message">このアプリケーションは破損しています。下記のリンク先のウェブサイトから APK を再ダウンロードしてください。その後、このアプリケーションをアンインストールし、ダウンロードした APK を再インストールしてください。</string>
+ <string name="updater_corrupt_navigate">ウェブサイトを開く</string>
<string name="version_summary">%1$s バックエンド %2$s</string>
<string name="version_summary_checking">%s バックエンドのバージョンを確認中</string>
<string name="version_summary_unknown">未知の %s バージョン</string>
diff --git a/ui/src/main/res/values-sv-rSE/strings.xml b/ui/src/main/res/values-sv-rSE/strings.xml
index c919d15c..910f9e05 100644
--- a/ui/src/main/res/values-sv-rSE/strings.xml
+++ b/ui/src/main/res/values-sv-rSE/strings.xml
@@ -107,6 +107,9 @@
<string name="tv_select_a_storage_drive">Välj en lagringsenhet</string>
<string name="tv_no_file_picker">Installera ett filhanteringsverktyg för att bläddra bland filer</string>
<string name="tv_add_tunnel_get_started">Lägg till en tunnel för att komma igång</string>
+ <string name="donate_title">♥ Donera till WireGuard Projektet</string>
+ <string name="donate_summary">Varje bidrag hjälper</string>
+ <string name="donate_google_play_disappointment">Tack för att du stödjer WireGuard Projektet!\n\nPå grund av Googles policyer får vi dessvärre inte till den del av projektets webbsida där du kan göra en donation. Förhoppningsvis kan du hitta dit ändå!\n\nTack igen för ditt bidrag.</string>
<string name="disable_config_export_title">Inaktivera export av konfiguration</string>
<string name="disable_config_export_description">Inaktivering av konfigurationsexport gör privata nycklar mindre tillgängliga</string>
<string name="dns_servers">DNS-servrar</string>
@@ -116,6 +119,7 @@
<string name="error_down">Fel vid nedtagning av tunnel: %s</string>
<string name="error_fetching_apps">Fel vid hämtning av applista: %s</string>
<string name="error_root">Vänligen få rootbehörighet och försök igen</string>
+ <string name="error_prepare">Fel vid förberedelse av tunnel: %s</string>
<string name="error_up">Fel vid uppstart av tunnel: %s</string>
<string name="exclude_private_ips">Uteslut privata IP-adresser</string>
<string name="generate_new_private_key">Skapa ny privat nyckel</string>
@@ -135,6 +139,8 @@
<string name="key_length_explanation_base64">: WireGuard base64 nycklar måste vara 44 tecken (32 bytes)</string>
<string name="key_length_explanation_binary">: WireGuard nycklar måste vara 32 bytes</string>
<string name="key_length_explanation_hex">: WireGuard hex nycklar måste vara 64 tecken (32 bytes)</string>
+ <string name="latest_handshake">Senaste handskakning</string>
+ <string name="latest_handshake_ago">%s sedan</string>
<string name="listen_port">Lyssningsport</string>
<string name="log_export_error">Kan inte exportera loggen: %s</string>
<string name="log_export_subject">WireGuard Android loggfil</string>
@@ -178,6 +184,10 @@
<string name="private_key">Privat nyckel</string>
<string name="public_key">Offentlig nyckel</string>
<string name="qr_code_hint">Tips: generera med `qrencode -t ansiutf8 &lt; tunnel.conf`.</string>
+ <string name="quick_settings_tile_add_title">Lägg till tile i snabbinställningarna</string>
+ <string name="quick_settings_tile_add_summary">Tilen växlar din senaste tunnel mellan på och av</string>
+ <string name="quick_settings_tile_add_failure">Misslyckades med att skapa tile: fel %d</string>
+ <string name="quick_settings_tile_action">Växla tunnel på/av</string>
<string name="restore_on_boot_summary_off">Kommer inte ta upp aktiverade tunnlar vid uppstart</string>
<string name="restore_on_boot_summary_on">Kommer ta upp aktiverade tunnlar vid uppstart</string>
<string name="restore_on_boot_title">Återställ vid uppstart</string>
@@ -213,6 +223,7 @@
<string name="tunnel_create_success">Lyckades skapa tunnel “%s”</string>
<string name="tunnel_error_already_exists">Tunnel ”%s” finns redan</string>
<string name="tunnel_error_invalid_name">Ogiltigt namn</string>
+ <string name="tunnel_list_placeholder">Lägg till en tunnel med knappen nedan</string>
<string name="tunnel_name">Tunnelns namn</string>
<string name="tunnel_on_error">Kunde inte aktivera tunneln (wgTurnOn returnerade %d)</string>
<string name="tunnel_dns_failure">Det går inte att lösa DNS-värdnamn: ”%s”</string>
@@ -221,6 +232,16 @@
<string name="type_name_go_userspace">Användarutrymme för Go</string>
<string name="type_name_kernel_module">Kärnmodul</string>
<string name="unknown_error">Okänt fel</string>
+ <string name="updater_avalable">Det finns en uppdatering till appen. Vänligen uppdatera nu.</string>
+ <string name="updater_action">Ladda ner &amp; uppdatera</string>
+ <string name="updater_rechecking">Hämtar uppdateringens metadata…</string>
+ <string name="updater_download_progress">Laddar ner uppdatering: %1$s / %2$s (%3$.2f%%)</string>
+ <string name="updater_download_progress_nototal">Laddar ner uppdatering: %s</string>
+ <string name="updater_installing">Installerar uppdatering…</string>
+ <string name="updater_failure">Uppdatering misslyckades: %s. Försöker igen inom kort…</string>
+ <string name="updater_corrupt_title">Applikationen är korrupt</string>
+ <string name="updater_corrupt_message">Applikationen är korrupt. Vänligen ladda ner en APK från hemsidan länkad nedan. Avinstallera därefter denna applikation och installera den nerladdade APKn.</string>
+ <string name="updater_corrupt_navigate">Öppna hemsida</string>
<string name="version_summary">%1$s bakstycke %2$s</string>
<string name="version_summary_checking">Kontrollerar %s backstycke utgåva</string>
<string name="version_summary_unknown">Okänd %s utgåva</string>
diff --git a/ui/src/main/res/values-vi-rVN/strings.xml b/ui/src/main/res/values-vi-rVN/strings.xml
index 8149d7e5..79d8d6c3 100644
--- a/ui/src/main/res/values-vi-rVN/strings.xml
+++ b/ui/src/main/res/values-vi-rVN/strings.xml
@@ -94,6 +94,9 @@
<string name="tv_select_a_storage_drive">Chọn bộ lưu trữ</string>
<string name="tv_no_file_picker">Vui lòng cài đặt tệp tiện ích lưu trữ để tìm kiếm các tệp</string>
<string name="tv_add_tunnel_get_started">Thêm một tunnel để bắt đầu</string>
+ <string name="donate_title">❤️ Đóng góp cho Dự án Wireguard</string>
+ <string name="donate_summary">Mọi đóng góp đều giúp ích</string>
+ <string name="donate_google_play_disappointment">Cảm ơn bạn đã ủng hộ WireGuard!\n\nThật tiếc, dựa trên điều khoản của Google, chúng tôi không thể đưa vào liên kết dẫn đến trang đóng góp ở trang chủ của dự án. Mong rằng bạn có thể tìm cách cho việc này!\n\nXin cảm ơn bạn một lần nữa vì đã đóng góp.</string>
<string name="disable_config_export_title">Vô hiệu hóa xuất cấu hình</string>
<string name="disable_config_export_description">Vô hiệu hóa xuất cấu hình sẽ giúp giảm khả năng truy cập vào private keys</string>
<string name="dns_servers">DNS servers</string>
@@ -103,9 +106,34 @@
<string name="error_down">Có lỗi khi tắt tunnel: %s</string>
<string name="error_fetching_apps">Lỗi khi lấy danh sách ứng dụng: %s</string>
<string name="error_root">Vui lòng truy cập bằng quyền root và thử lại</string>
+ <string name="error_prepare">Lỗi khi chuẩn bị tunnel: %s</string>
<string name="error_up">Có lỗi khi bật tunnel: %s</string>
<string name="exclude_private_ips">Loại trừ IPs private</string>
<string name="generate_new_private_key">Tạo private key mới</string>
+ <string name="generic_error">Lỗi \"%s\" không xác định</string>
+ <string name="hint_automatic">(tự động)</string>
+ <string name="hint_generated">(được tạo tự động)</string>
+ <string name="hint_optional">(tùy chọn)</string>
+ <string name="hint_optional_discouraged">(tùy chọn, không khuyến khích)</string>
+ <string name="hint_random">(ngẫu nhiên)</string>
+ <string name="illegal_filename_error">Tên file không hợp lệ \"%s\"</string>
+ <string name="import_error">Không thể nhập tunnel: %s</string>
+ <string name="import_from_qr_code">Nhập tunnel từ mã QR</string>
+ <string name="import_success">Đã nhập \"%s\"</string>
+ <string name="interface_title">Giao diện</string>
+ <string name="key_contents_error">Kí tự không hợp lệ trong khoá</string>
+ <string name="key_length_error">Độ dài khoá không hợp lệ</string>
+ <string name="key_length_explanation_base64">: Khoá WireGuard base64 phải đủ 44 ký tự (32 bytes)</string>
+ <string name="key_length_explanation_binary">: Khoá WireGuard phải đủ 32 bytes</string>
+ <string name="key_length_explanation_hex">: Khoá WireGuard hex phải đủ 64 ký tự (32 bytes)</string>
+ <string name="latest_handshake">Lần bắt tay cuối</string>
+ <string name="latest_handshake_ago">%s giây trước</string>
+ <string name="listen_port">Cổng</string>
+ <string name="log_export_error">Không thể xuất nhật ký: %s</string>
+ <string name="log_export_subject">File nhật ký WireGuard Android</string>
+ <string name="log_export_success">Đã lưu vào \"%s\"</string>
+ <string name="log_export_title">Xuất file nhật ký</string>
+ <string name="log_saver_activity_label">Lưu nhật ký</string>
<string name="parse_error_inet_address">Địa chỉ IP</string>
<string name="peer">Đồng trang lứa</string>
</resources>
diff --git a/ui/src/main/res/values-zh-rCN/strings.xml b/ui/src/main/res/values-zh-rCN/strings.xml
index cd282317..7757e2fe 100644
--- a/ui/src/main/res/values-zh-rCN/strings.xml
+++ b/ui/src/main/res/values-zh-rCN/strings.xml
@@ -172,9 +172,9 @@
<string name="public_key">公钥</string>
<string name="qr_code_hint">提示:使用命令 `qrencode -t ansiutf8 &lt; tunnel.conf` 生成二维码</string>
<string name="quick_settings_tile_add_title">添加磁贴到快速设置面板</string>
- <string name="quick_settings_tile_add_summary">快捷方式磁贴开/关最近使用的隧道。</string>
- <string name="quick_settings_tile_add_failure">无法添加快捷方式磁贴:错误 %d</string>
- <string name="quick_settings_tile_action">开/关隧道</string>
+ <string name="quick_settings_tile_add_summary">通过快捷磁贴开启/关闭上次使用的隧道</string>
+ <string name="quick_settings_tile_add_failure">无法添加快捷磁贴:错误 %d</string>
+ <string name="quick_settings_tile_action">开启/关闭隧道</string>
<string name="restore_on_boot_summary_off">未启用</string>
<string name="restore_on_boot_summary_on">设备启动时自动开启上次使用的隧道</string>
<string name="restore_on_boot_title">启动时恢复</string>
@@ -226,8 +226,8 @@
<string name="updater_download_progress_nototal">正在下载更新:%s</string>
<string name="updater_installing">正在安装更新…</string>
<string name="updater_failure">更新失败:%s。将在稍后重试…</string>
- <string name="updater_corrupt_title">应用程序损坏</string>
- <string name="updater_corrupt_message">此应用程序已损坏。请从下面链接的网站重新下载APK。然后卸载此应用程序并从下载的APK重新安装它。</string>
+ <string name="updater_corrupt_title">应用损坏</string>
+ <string name="updater_corrupt_message">此应用已损坏。请从下方链接的网站中重新下载 APK,然后卸载此应用并重新安装。</string>
<string name="updater_corrupt_navigate">打开网站</string>
<string name="version_summary">%1$s backend %2$s</string>
<string name="version_summary_checking">正在检查 %s backend 版本</string>
diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml
index df3d3340..1cb642e2 100644
--- a/ui/src/main/res/values/strings.xml
+++ b/ui/src/main/res/values/strings.xml
@@ -63,10 +63,11 @@
<string name="allow_remote_control_intents_summary_on">External apps may toggle tunnels (advanced)</string>
<string name="allow_remote_control_intents_title">Allow remote control apps</string>
<string name="allowed_ips">Allowed IPs</string>
- <string name="app_name" translatable="false">WireGuard</string>
+ <string name="app_name" translatable="false">(m7n.eu) 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 in %2$s</string>
+ <string name="bad_config_explanation_http_proxy">: Must be valid proxy hostname and port</string>
<string name="bad_config_explanation_pka">: Must be positive and no more than 65535</string>
<string name="bad_config_explanation_positive_number">: Must be positive</string>
<string name="bad_config_explanation_udp_port">: Must be a valid UDP port number</string>
@@ -104,6 +105,7 @@
<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="dynamic_addresses">Dynamic addresses</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>
@@ -130,6 +132,10 @@
<string name="hint_optional">(optional)</string>
<string name="hint_optional_discouraged">(optional, not recommended)</string>
<string name="hint_random">(random)</string>
+ <string name="http_proxy">Proxy</string>
+ <string name="http_proxy_hostname">Proxy hostname</string>
+ <string name="http_proxy_pac">Proxy Auto-Config URL</string>
+ <string name="http_proxy_port">Proxy port</string>
<string name="illegal_filename_error">Illegal file name “%s”</string>
<string name="import_error">Unable to import tunnel: %s</string>
<string name="import_from_qr_code">Import Tunnel from QR Code</string>