aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/ui/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'ui/src/main')
-rw-r--r--ui/src/main/AndroidManifest.xml34
-rw-r--r--ui/src/main/java/com/wireguard/android/Application.kt42
-rw-r--r--ui/src/main/java/com/wireguard/android/BootShutdownReceiver.kt4
-rw-r--r--ui/src/main/java/com/wireguard/android/QuickTileService.kt110
-rw-r--r--ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt49
-rw-r--r--ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt157
-rw-r--r--ui/src/main/java/com/wireguard/android/activity/MainActivity.kt38
-rw-r--r--ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt22
-rw-r--r--ui/src/main/java/com/wireguard/android/activity/ThemeChangeAwareActivity.kt33
-rw-r--r--ui/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.kt5
-rw-r--r--ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt5
-rw-r--r--ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt66
-rw-r--r--ui/src/main/java/com/wireguard/android/configStore/ConfigStore.kt2
-rw-r--r--ui/src/main/java/com/wireguard/android/configStore/FileConfigStore.kt8
-rw-r--r--ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt57
-rw-r--r--ui/src/main/java/com/wireguard/android/databinding/ItemChangeListener.kt26
-rw-r--r--ui/src/main/java/com/wireguard/android/databinding/Keyed.kt2
-rw-r--r--ui/src/main/java/com/wireguard/android/databinding/ObservableKeyedArrayList.kt2
-rw-r--r--ui/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.kt2
-rw-r--r--ui/src/main/java/com/wireguard/android/databinding/ObservableSortedKeyedArrayList.kt2
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/AddTunnelsSheet.kt8
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt31
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt26
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt53
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt58
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt64
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt61
-rw-r--r--ui/src/main/java/com/wireguard/android/model/ApplicationData.kt2
-rw-r--r--ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt10
-rw-r--r--ui/src/main/java/com/wireguard/android/model/TunnelComparator.kt2
-rw-r--r--ui/src/main/java/com/wireguard/android/model/TunnelManager.kt5
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/DonatePreference.kt42
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/KernelModuleEnablerPreference.kt2
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/PreferencesPreferenceDataStore.kt2
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/QuickTilePreference.kt50
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.kt2
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt8
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt19
-rw-r--r--ui/src/main/java/com/wireguard/android/updater/Ed25519.java2507
-rw-r--r--ui/src/main/java/com/wireguard/android/updater/SnackbarUpdateShower.kt173
-rw-r--r--ui/src/main/java/com/wireguard/android/updater/Updater.kt451
-rw-r--r--ui/src/main/java/com/wireguard/android/util/AdminKnobs.kt4
-rw-r--r--ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt38
-rw-r--r--ui/src/main/java/com/wireguard/android/util/ClipboardUtils.kt7
-rw-r--r--ui/src/main/java/com/wireguard/android/util/DownloadsFileSaver.kt6
-rw-r--r--ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt99
-rw-r--r--ui/src/main/java/com/wireguard/android/util/Extensions.kt4
-rw-r--r--ui/src/main/java/com/wireguard/android/util/QrCodeFromFileScanner.kt112
-rw-r--r--ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt46
-rw-r--r--ui/src/main/java/com/wireguard/android/util/TunnelImporter.kt17
-rw-r--r--ui/src/main/java/com/wireguard/android/util/UserKnobs.kt38
-rw-r--r--ui/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.kt25
-rw-r--r--ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt2
-rw-r--r--ui/src/main/java/com/wireguard/android/viewmodel/PeerProxy.kt42
-rw-r--r--ui/src/main/java/com/wireguard/android/widget/KeyInputFilter.kt17
-rw-r--r--ui/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.kt10
-rw-r--r--ui/src/main/java/com/wireguard/android/widget/NameInputFilter.kt15
-rw-r--r--ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt10
-rw-r--r--ui/src/main/java/com/wireguard/android/widget/ToggleSwitch.kt6
-rw-r--r--ui/src/main/java/com/wireguard/android/widget/TvCardView.kt44
-rw-r--r--ui/src/main/res/color/tv_list_item_tint.xml7
-rw-r--r--ui/src/main/res/drawable/ic_action_add_white.xml7
-rw-r--r--ui/src/main/res/drawable/ic_action_delete.xml3
-rw-r--r--ui/src/main/res/drawable/ic_action_edit.xml3
-rw-r--r--ui/src/main/res/drawable/ic_action_edit_white.xml9
-rw-r--r--ui/src/main/res/drawable/ic_action_generate.xml3
-rw-r--r--ui/src/main/res/drawable/ic_action_open.xml (renamed from ui/src/main/res/drawable/ic_action_open_white.xml)3
-rw-r--r--ui/src/main/res/drawable/ic_action_save.xml3
-rw-r--r--ui/src/main/res/drawable/ic_action_scan_qr_code.xml (renamed from ui/src/main/res/drawable/ic_action_scan_qr_code_white.xml)3
-rw-r--r--ui/src/main/res/drawable/ic_action_select_all.xml3
-rw-r--r--ui/src/main/res/drawable/ic_action_share_white.xml1
-rw-r--r--ui/src/main/res/drawable/ic_arrow_back.xml5
-rw-r--r--ui/src/main/res/drawable/ic_settings.xml3
-rw-r--r--ui/src/main/res/drawable/list_item_background.xml5
-rw-r--r--ui/src/main/res/drawable/tv_logo_banner.xml2
-rw-r--r--ui/src/main/res/layout/add_tunnels_bottom_sheet.xml10
-rw-r--r--ui/src/main/res/layout/app_list_dialog_fragment.xml2
-rw-r--r--ui/src/main/res/layout/app_list_item.xml2
-rw-r--r--ui/src/main/res/layout/config_naming_dialog_fragment.xml5
-rw-r--r--ui/src/main/res/layout/log_viewer_activity.xml2
-rw-r--r--ui/src/main/res/layout/log_viewer_entry.xml7
-rw-r--r--ui/src/main/res/layout/tunnel_detail_fragment.xml57
-rw-r--r--ui/src/main/res/layout/tunnel_detail_peer.xml70
-rw-r--r--ui/src/main/res/layout/tunnel_editor_fragment.xml8
-rw-r--r--ui/src/main/res/layout/tunnel_editor_peer.xml2
-rw-r--r--ui/src/main/res/layout/tunnel_list_fragment.xml4
-rw-r--r--ui/src/main/res/layout/tunnel_list_item.xml9
-rw-r--r--ui/src/main/res/layout/tv_activity.xml10
-rw-r--r--ui/src/main/res/layout/tv_file_list_item.xml4
-rw-r--r--ui/src/main/res/layout/tv_tunnel_list_item.xml15
-rw-r--r--ui/src/main/res/mipmap-anydpi-v26/ic_launcher.xml1
-rw-r--r--ui/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml1
-rw-r--r--ui/src/main/res/resources.properties1
-rw-r--r--ui/src/main/res/values-ar-rSA/strings.xml316
-rw-r--r--ui/src/main/res/values-ca-rES/strings.xml1
-rw-r--r--ui/src/main/res/values-da-rDK/strings.xml181
-rw-r--r--ui/src/main/res/values-de/strings.xml36
-rw-r--r--ui/src/main/res/values-el-rGR/strings.xml64
-rw-r--r--ui/src/main/res/values-es-rES/strings.xml37
-rw-r--r--ui/src/main/res/values-et-rEE/strings.xml257
-rw-r--r--ui/src/main/res/values-fa-rIR/strings.xml9
-rw-r--r--ui/src/main/res/values-fi-rFI/strings.xml27
-rw-r--r--ui/src/main/res/values-fr/strings.xml39
-rw-r--r--ui/src/main/res/values-hi-rIN/strings.xml3
-rw-r--r--ui/src/main/res/values-hi/strings.xml1
-rw-r--r--ui/src/main/res/values-hu-rHU/strings.xml36
-rw-r--r--ui/src/main/res/values-in/strings.xml4
-rw-r--r--ui/src/main/res/values-it/strings.xml41
-rw-r--r--ui/src/main/res/values-ja/strings.xml25
-rw-r--r--ui/src/main/res/values-ko-rKR/strings.xml4
-rw-r--r--ui/src/main/res/values-night/colors.xml23
-rw-r--r--ui/src/main/res/values-night/logviewer_colors.xml6
-rw-r--r--ui/src/main/res/values-night/themes.xml31
-rw-r--r--ui/src/main/res/values-nl-rNL/strings.xml233
-rw-r--r--ui/src/main/res/values-no-rNO/strings.xml13
-rw-r--r--ui/src/main/res/values-pa-rIN/strings.xml1
-rw-r--r--ui/src/main/res/values-pl-rPL/strings.xml5
-rw-r--r--ui/src/main/res/values-pt-rBR/strings.xml226
-rw-r--r--ui/src/main/res/values-pt-rPT/strings.xml1
-rw-r--r--ui/src/main/res/values-ro-rRO/strings.xml12
-rw-r--r--ui/src/main/res/values-ru/strings.xml61
-rw-r--r--ui/src/main/res/values-si-rLK/strings.xml127
-rw-r--r--ui/src/main/res/values-sk-rSK/strings.xml69
-rw-r--r--ui/src/main/res/values-sl/strings.xml10
-rw-r--r--ui/src/main/res/values-sv-rSE/strings.xml110
-rw-r--r--ui/src/main/res/values-tr-rTR/strings.xml27
-rw-r--r--ui/src/main/res/values-uk-rUA/strings.xml10
-rw-r--r--ui/src/main/res/values-v23/styles.xml8
-rw-r--r--ui/src/main/res/values-v27/styles.xml7
-rw-r--r--ui/src/main/res/values-vi-rVN/strings.xml139
-rw-r--r--ui/src/main/res/values-zh-rCN/strings.xml49
-rw-r--r--ui/src/main/res/values-zh-rTW/strings.xml130
-rw-r--r--ui/src/main/res/values/attrs.xml7
-rw-r--r--ui/src/main/res/values/colors.xml89
-rw-r--r--ui/src/main/res/values/dimens.xml2
-rw-r--r--ui/src/main/res/values/logviewer_colors.xml6
-rw-r--r--ui/src/main/res/values/strings.xml24
-rw-r--r--ui/src/main/res/values/styles.xml70
-rw-r--r--ui/src/main/res/values/themes.xml31
-rw-r--r--ui/src/main/res/values/tv_colors.xml6
-rw-r--r--ui/src/main/res/values/tv_styles.xml31
-rw-r--r--ui/src/main/res/xml/app_restrictions.xml2
-rw-r--r--ui/src/main/res/xml/preferences.xml7
143 files changed, 6784 insertions, 924 deletions
diff --git a/ui/src/main/AndroidManifest.xml b/ui/src/main/AndroidManifest.xml
index 4c957bd7..754992d5 100644
--- a/ui/src/main/AndroidManifest.xml
+++ b/ui/src/main/AndroidManifest.xml
@@ -1,12 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
- package="com.wireguard.android"
android:installLocation="internalOnly">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+ <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
+ <uses-permission
+ android:name="android.permission.SYSTEM_ALERT_WINDOW"
+ android:minSdkVersion="34" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"
@@ -21,6 +24,9 @@
<uses-feature
android:name="android.hardware.camera.any"
android:required="false" />
+ <uses-feature
+ android:name="android.hardware.camera"
+ android:required="false" />
<permission
android:name="${applicationId}.permission.CONTROL_TUNNELS"
@@ -33,6 +39,7 @@
android:name=".Application"
android:allowBackup="false"
android:banner="@mipmap/banner"
+ android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
@@ -41,9 +48,12 @@
<activity
android:name=".activity.TunnelToggleActivity"
+ android:excludeFromRecents="true"
android:theme="@style/NoBackgroundTheme" />
- <activity android:name=".activity.MainActivity">
+ <activity
+ android:name=".activity.MainActivity"
+ android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -57,6 +67,7 @@
<activity
android:name=".activity.TvMainActivity"
+ android:exported="true"
android:theme="@style/TvTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -81,6 +92,7 @@
<activity
android:name=".activity.LogViewerActivity"
+ android:exported="false"
android:label="@string/log_viewer_title">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -93,7 +105,9 @@
android:exported="false"
android:grantUriPermissions="true" />
- <receiver android:name=".BootShutdownReceiver">
+ <receiver
+ android:name=".BootShutdownReceiver"
+ android:exported="true">
<intent-filter>
<action android:name="android.intent.action.ACTION_SHUTDOWN" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
@@ -101,7 +115,16 @@
</receiver>
<receiver
+ android:name=".updater.Updater$AppUpdatedReceiver"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
+ </intent-filter>
+ </receiver>
+
+ <receiver
android:name=".model.TunnelManager$IntentReceiver"
+ android:exported="true"
android:permission="${applicationId}.permission.CONTROL_TUNNELS">
<intent-filter>
<action android:name="com.wireguard.android.action.REFRESH_TUNNEL_STATES" />
@@ -112,6 +135,7 @@
<service
android:name=".QuickTileService"
+ android:exported="true"
android:icon="@drawable/ic_tile"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
@@ -122,6 +146,10 @@
<meta-data
android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="false" />
+
+ <meta-data
+ android:name="android.service.quicksettings.TOGGLEABLE_TILE"
+ android:value="true" />
</service>
<meta-data
diff --git a/ui/src/main/java/com/wireguard/android/Application.kt b/ui/src/main/java/com/wireguard/android/Application.kt
index d9dcb637..4dcec508 100644
--- a/ui/src/main/java/com/wireguard/android/Application.kt
+++ b/ui/src/main/java/com/wireguard/android/Application.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android
@@ -16,11 +16,13 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStoreFile
+import com.google.android.material.color.DynamicColors
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.GoBackend
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.configStore.FileConfigStore
import com.wireguard.android.model.TunnelManager
+import com.wireguard.android.updater.Updater
import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller
import com.wireguard.android.util.UserKnobs
@@ -34,6 +36,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
import java.lang.ref.WeakReference
import java.util.Locale
@@ -56,19 +59,13 @@ class Application : android.app.Application() {
startActivity(intent)
System.exit(0)
}
- if (BuildConfig.DEBUG) {
- StrictMode.setVmPolicy(VmPolicy.Builder().detectAll().penaltyLog().build())
- StrictMode.setThreadPolicy(ThreadPolicy.Builder().detectAll().penaltyLog().build())
- }
}
private suspend fun determineBackend(): Backend {
var backend: Backend? = null
- var didStartRootShell = false
if (UserKnobs.enableKernelModule.first() && WgQuickBackend.hasKernelSupport()) {
try {
- if (!didStartRootShell)
- rootShell.start()
+ rootShell.start()
val wgQuickBackend = WgQuickBackend(applicationContext, rootShell, toolsInstaller)
wgQuickBackend.setMultipleTunnels(UserKnobs.multipleTunnels.first())
backend = wgQuickBackend
@@ -88,14 +85,24 @@ class Application : android.app.Application() {
override fun onCreate() {
Log.i(TAG, USER_AGENT)
super.onCreate()
+ DynamicColors.applyToActivitiesIfAvailable(this)
rootShell = RootShell(applicationContext)
toolsInstaller = ToolsInstaller(applicationContext, rootShell)
preferencesDataStore = PreferenceDataStoreFactory.create { applicationContext.preferencesDataStoreFile("settings") }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
- coroutineScope.launch {
- AppCompatDelegate.setDefaultNightMode(
- if (UserKnobs.darkTheme.first()) AppCompatDelegate.MODE_NIGHT_YES else AppCompatDelegate.MODE_NIGHT_NO)
+ runBlocking {
+ AppCompatDelegate.setDefaultNightMode(if (UserKnobs.darkTheme.first()) AppCompatDelegate.MODE_NIGHT_YES else AppCompatDelegate.MODE_NIGHT_NO)
}
+ UserKnobs.darkTheme.onEach {
+ val newMode = if (it) {
+ AppCompatDelegate.MODE_NIGHT_YES
+ } else {
+ AppCompatDelegate.MODE_NIGHT_NO
+ }
+ if (AppCompatDelegate.getDefaultNightMode() != newMode) {
+ AppCompatDelegate.setDefaultNightMode(newMode)
+ }
+ }.launchIn(coroutineScope)
} else {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
}
@@ -109,6 +116,12 @@ class Application : android.app.Application() {
Log.e(TAG, Log.getStackTraceString(e))
}
}
+ Updater.monitorForUpdates()
+
+ if (BuildConfig.DEBUG) {
+ StrictMode.setVmPolicy(VmPolicy.Builder().detectAll().penaltyLog().build())
+ StrictMode.setThreadPolicy(ThreadPolicy.Builder().detectAll().penaltyLog().build())
+ }
}
override fun onTerminate() {
@@ -121,27 +134,20 @@ class Application : android.app.Application() {
private const val TAG = "WireGuard/Application"
private lateinit var weakSelf: WeakReference<Application>
- @JvmStatic
fun get(): Application {
return weakSelf.get()!!
}
- @JvmStatic
suspend fun getBackend() = get().futureBackend.await()
- @JvmStatic
fun getRootShell() = get().rootShell
- @JvmStatic
fun getPreferencesDataStore() = get().preferencesDataStore
- @JvmStatic
fun getToolsInstaller() = get().toolsInstaller
- @JvmStatic
fun getTunnelManager() = get().tunnelManager
- @JvmStatic
fun getCoroutineScope() = get().coroutineScope
}
diff --git a/ui/src/main/java/com/wireguard/android/BootShutdownReceiver.kt b/ui/src/main/java/com/wireguard/android/BootShutdownReceiver.kt
index b8bb4f6a..a859bf71 100644
--- a/ui/src/main/java/com/wireguard/android/BootShutdownReceiver.kt
+++ b/ui/src/main/java/com/wireguard/android/BootShutdownReceiver.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android
@@ -14,9 +14,9 @@ import kotlinx.coroutines.launch
class BootShutdownReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
+ val action = intent.action ?: return
applicationScope.launch {
if (Application.getBackend() !is WgQuickBackend) return@launch
- val action = intent.action ?: return@launch
val tunnelManager = Application.getTunnelManager()
if (Intent.ACTION_BOOT_COMPLETED == action) {
Log.i(TAG, "Broadcast receiver restoring state (boot)")
diff --git a/ui/src/main/java/com/wireguard/android/QuickTileService.kt b/ui/src/main/java/com/wireguard/android/QuickTileService.kt
index 8b35f9b4..9be2cc2b 100644
--- a/ui/src/main/java/com/wireguard/android/QuickTileService.kt
+++ b/ui/src/main/java/com/wireguard/android/QuickTileService.kt
@@ -1,15 +1,18 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android
+import android.app.PendingIntent
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.Icon
+import android.net.Uri
import android.os.Build
import android.os.IBinder
+import android.provider.Settings
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import android.util.Log
@@ -49,32 +52,42 @@ class QuickTileService : TileService() {
}
override fun onClick() {
- if (tunnel != null) {
- unlockAndRun {
- val tile = qsTile
- if (tile != null) {
- tile.icon = if (tile.icon == iconOn) iconOff else iconOn
- tile.updateTile()
+ when (val tunnel = tunnel) {
+ null -> {
+ val intent = Intent(this, MainActivity::class.java)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ 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)
}
- applicationScope.launch {
- try {
- tunnel!!.setStateAsync(Tunnel.State.TOGGLE)
- updateTile()
- } catch (_: Throwable) {
- val toggleIntent = Intent(this@QuickTileService, TunnelToggleActivity::class.java)
- toggleIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- startActivity(toggleIntent)
+ }
+ else -> {
+ unlockAndRun {
+ applicationScope.launch {
+ try {
+ tunnel.setStateAsync(Tunnel.State.TOGGLE)
+ updateTile()
+ } catch (_: Throwable) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !Settings.canDrawOverlays(this@QuickTileService)) {
+ val permissionIntent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName"))
+ permissionIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivityAndCollapse(PendingIntent.getActivity(this@QuickTileService, 0, permissionIntent, PendingIntent.FLAG_IMMUTABLE))
+ return@launch
+ }
+ val toggleIntent = Intent(this@QuickTileService, TunnelToggleActivity::class.java)
+ toggleIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivity(toggleIntent)
+ }
}
}
}
- } else {
- val intent = Intent(this, MainActivity::class.java)
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- startActivityAndCollapse(intent)
}
}
override fun onCreate() {
+ isAdded = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
iconOn = Icon.createWithResource(this, R.drawable.ic_tile)
iconOff = iconOn
@@ -84,55 +97,64 @@ class QuickTileService : TileService() {
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 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
c = Canvas(b)
icon.setBounds(0, 0, c.width, c.height)
icon.draw(c)
iconOff = Icon.createWithBitmap(b)
}
+ override fun onDestroy() {
+ super.onDestroy()
+ isAdded = false
+ }
+
override fun onStartListening() {
Application.getTunnelManager().addOnPropertyChangedCallback(onTunnelChangedCallback)
- if (tunnel != null) tunnel!!.addOnPropertyChangedCallback(onStateChangedCallback)
+ tunnel?.addOnPropertyChangedCallback(onStateChangedCallback)
updateTile()
}
override fun onStopListening() {
- if (tunnel != null) tunnel!!.removeOnPropertyChangedCallback(onStateChangedCallback)
+ tunnel?.removeOnPropertyChangedCallback(onStateChangedCallback)
Application.getTunnelManager().removeOnPropertyChangedCallback(onTunnelChangedCallback)
}
+ override fun onTileAdded() {
+ isAdded = true
+ }
+
+ override fun onTileRemoved() {
+ isAdded = false
+ }
+
private fun updateTile() {
// Update the tunnel.
val newTunnel = Application.getTunnelManager().lastUsedTunnel
if (newTunnel != tunnel) {
- if (tunnel != null) tunnel!!.removeOnPropertyChangedCallback(onStateChangedCallback)
+ tunnel?.removeOnPropertyChangedCallback(onStateChangedCallback)
tunnel = newTunnel
- if (tunnel != null) tunnel!!.addOnPropertyChangedCallback(onStateChangedCallback)
+ tunnel?.addOnPropertyChangedCallback(onStateChangedCallback)
}
// Update the tile contents.
- val label: String
- val state: Int
- val tile = qsTile
- if (tunnel != null) {
- label = tunnel!!.name
- state = if (tunnel!!.state == Tunnel.State.UP) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
- } else {
- label = getString(R.string.app_name)
- state = Tile.STATE_INACTIVE
- }
- if (tile == null) return
- tile.label = label
- if (tile.state != state) {
- tile.icon = if (state == Tile.STATE_ACTIVE) iconOn else iconOff
- tile.state = state
+ val tile = qsTile ?: return
+
+ when (val tunnel = tunnel) {
+ null -> {
+ tile.label = getString(R.string.app_name)
+ tile.state = Tile.STATE_INACTIVE
+ tile.icon = iconOff
+ }
+ else -> {
+ tile.label = tunnel.name
+ tile.state = if (tunnel.state == Tunnel.State.UP) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
+ tile.icon = if (tunnel.state == Tunnel.State.UP) iconOn else iconOff
+ }
}
tile.updateTile()
}
@@ -143,19 +165,23 @@ class QuickTileService : TileService() {
sender.removeOnPropertyChangedCallback(this)
return
}
- if (propertyId != 0 && propertyId != BR.state) return
+ if (propertyId != 0 && propertyId != BR.state)
+ return
updateTile()
}
}
private inner class OnTunnelChangedCallback : OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable, propertyId: Int) {
- if (propertyId != 0 && propertyId != BR.lastUsedTunnel) return
+ if (propertyId != 0 && propertyId != BR.lastUsedTunnel)
+ return
updateTile()
}
}
companion object {
private const val TAG = "WireGuard/QuickTileService"
+ var isAdded: Boolean = false
+ private set
}
}
diff --git a/ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt b/ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt
index bdf0f8d4..56810377 100644
--- a/ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt
+++ b/ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt
@@ -1,28 +1,36 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity
import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.CallbackRegistry
import androidx.databinding.CallbackRegistry.NotifierCallback
import androidx.lifecycle.lifecycleScope
import com.wireguard.android.Application
import com.wireguard.android.model.ObservableTunnel
+import kotlinx.coroutines.launch
/**
* Base class for activities that need to remember the currently-selected tunnel.
*/
-abstract class BaseActivity : ThemeChangeAwareActivity() {
+abstract class BaseActivity : AppCompatActivity() {
private val selectionChangeRegistry = SelectionChangeRegistry()
+ private var created = false
var selectedTunnel: ObservableTunnel? = null
set(value) {
val oldTunnel = field
if (oldTunnel == value) return
field = value
- onSelectedTunnelChanged(oldTunnel, value)
- selectionChangeRegistry.notifyCallbacks(oldTunnel, 0, value)
+ if (created) {
+ if (!onSelectedTunnelChanged(oldTunnel, value)) {
+ field = oldTunnel
+ } else {
+ selectionChangeRegistry.notifyCallbacks(oldTunnel, 0, value)
+ }
+ }
}
fun addOnSelectedTunnelChangedListener(listener: OnSelectedTunnelChangedListener) {
@@ -30,17 +38,25 @@ abstract class BaseActivity : ThemeChangeAwareActivity() {
}
override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
// Restore the saved tunnel if there is one; otherwise grab it from the arguments.
val savedTunnelName = when {
savedInstanceState != null -> savedInstanceState.getString(KEY_SELECTED_TUNNEL)
intent != null -> intent.getStringExtra(KEY_SELECTED_TUNNEL)
else -> null
}
- if (savedTunnelName != null)
- lifecycleScope.launchWhenCreated { selectedTunnel = Application.getTunnelManager().getTunnels()[savedTunnelName] }
-
- // The selected tunnel must be set before the superclass method recreates fragments.
- super.onCreate(savedInstanceState)
+ if (savedTunnelName != null) {
+ lifecycleScope.launch {
+ val tunnel = Application.getTunnelManager().getTunnels()[savedTunnelName]
+ if (tunnel == null)
+ created = true
+ selectedTunnel = tunnel
+ created = true
+ }
+ } else {
+ created = true
+ }
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -48,10 +64,11 @@ abstract class BaseActivity : ThemeChangeAwareActivity() {
super.onSaveInstanceState(outState)
}
- protected abstract fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?)
+ protected abstract fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?): Boolean
fun removeOnSelectedTunnelChangedListener(
- listener: OnSelectedTunnelChangedListener) {
+ listener: OnSelectedTunnelChangedListener
+ ) {
selectionChangeRegistry.remove(listener)
}
@@ -61,17 +78,17 @@ abstract class BaseActivity : ThemeChangeAwareActivity() {
private class SelectionChangeNotifier : NotifierCallback<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel>() {
override fun onNotifyCallback(
- listener: OnSelectedTunnelChangedListener,
- oldTunnel: ObservableTunnel?,
- ignored: Int,
- newTunnel: ObservableTunnel?
+ listener: OnSelectedTunnelChangedListener,
+ oldTunnel: ObservableTunnel?,
+ ignored: Int,
+ newTunnel: ObservableTunnel?
) {
listener.onSelectedTunnelChanged(oldTunnel, newTunnel)
}
}
private class SelectionChangeRegistry :
- CallbackRegistry<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel>(SelectionChangeNotifier())
+ CallbackRegistry<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel>(SelectionChangeNotifier())
companion object {
private const val KEY_SELECTED_TUNNEL = "selected_tunnel"
diff --git a/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt b/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt
index dd100cf9..69e33bc9 100644
--- a/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt
+++ b/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
@@ -27,6 +27,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
+import androidx.collection.CircularArray
import androidx.core.app.ShareCompat
import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.lifecycleScope
@@ -40,6 +41,7 @@ import com.wireguard.android.R
import com.wireguard.android.databinding.LogViewerActivityBinding
import com.wireguard.android.util.DownloadsFileSaver
import com.wireguard.android.util.ErrorMessages
+import com.wireguard.android.util.resolveAttribute
import com.wireguard.crypto.KeyPair
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -61,8 +63,8 @@ import java.util.regex.Pattern
class LogViewerActivity : AppCompatActivity() {
private lateinit var binding: LogViewerActivityBinding
private lateinit var logAdapter: LogEntryAdapter
- private var logLines = arrayListOf<LogLine>()
- private var rawLogLines = StringBuffer()
+ private var logLines = CircularArray<LogLine>()
+ private var rawLogLines = CircularArray<String>()
private var recyclerView: RecyclerView? = null
private var saveButton: MenuItem? = null
private val year by lazy {
@@ -70,7 +72,7 @@ class LogViewerActivity : AppCompatActivity() {
yearFormatter.format(Date())
}
- private val defaultColor by lazy { ResourcesCompat.getColor(resources, R.color.primary_text_color, theme) }
+ private val defaultColor by lazy { resolveAttribute(com.google.android.material.R.attr.colorOnSurface) }
private val debugColor by lazy { ResourcesCompat.getColor(resources, R.color.debug_tag_color, theme) }
@@ -110,19 +112,21 @@ class LogViewerActivity : AppCompatActivity() {
}
binding.shareFab.setOnClickListener {
- revokeLastUri()
- val key = KeyPair().privateKey.toHex()
- LOGS[key] = rawLogLines.toString().toByteArray(Charsets.UTF_8)
- lastUri = Uri.parse("content://${BuildConfig.APPLICATION_ID}.exported-log/$key")
- val shareIntent = ShareCompat.IntentBuilder(this)
+ lifecycleScope.launch {
+ revokeLastUri()
+ val key = KeyPair().privateKey.toHex()
+ LOGS[key] = rawLogBytes()
+ lastUri = Uri.parse("content://${BuildConfig.APPLICATION_ID}.exported-log/$key")
+ val shareIntent = ShareCompat.IntentBuilder(this@LogViewerActivity)
.setType("text/plain")
.setSubject(getString(R.string.log_export_subject))
.setStream(lastUri)
.setChooserTitle(R.string.log_export_title)
.createChooserIntent()
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- grantUriPermission("android", lastUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
- revokeLastActivityResultLauncher.launch(shareIntent)
+ grantUriPermission("android", lastUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ revokeLastActivityResultLauncher.launch(shareIntent)
+ }
}
}
@@ -138,24 +142,37 @@ class LogViewerActivity : AppCompatActivity() {
finish()
true
}
+
R.id.save_log -> {
saveButton?.isEnabled = false
lifecycleScope.launch { saveLog() }
true
}
+
else -> super.onOptionsItemSelected(item)
}
}
private val downloadsFileSaver = DownloadsFileSaver(this)
+ private suspend fun rawLogBytes(): ByteArray {
+ val builder = StringBuilder()
+ withContext(Dispatchers.IO) {
+ for (i in 0 until rawLogLines.size()) {
+ builder.append(rawLogLines[i])
+ builder.append('\n')
+ }
+ }
+ return builder.toString().toByteArray(Charsets.UTF_8)
+ }
+
private suspend fun saveLog() {
var exception: Throwable? = null
var outputFile: DownloadsFileSaver.DownloadsFile? = null
withContext(Dispatchers.IO) {
try {
outputFile = downloadsFileSaver.save("wireguard-log.txt", "text/plain", true)
- outputFile?.outputStream?.write(rawLogLines.toString().toByteArray(Charsets.UTF_8))
+ outputFile?.outputStream?.write(rawLogBytes())
} catch (e: Throwable) {
outputFile?.delete()
exception = e
@@ -164,12 +181,14 @@ class LogViewerActivity : AppCompatActivity() {
saveButton?.isEnabled = true
if (outputFile == null)
return
- Snackbar.make(findViewById(android.R.id.content),
- if (exception == null) getString(R.string.log_export_success, outputFile?.fileName)
- else getString(R.string.log_export_error, ErrorMessages[exception]),
- if (exception == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG)
- .setAnchorView(binding.shareFab)
- .show()
+ Snackbar.make(
+ findViewById(android.R.id.content),
+ if (exception == null) getString(R.string.log_export_success, outputFile?.fileName)
+ else getString(R.string.log_export_error, ErrorMessages[exception]),
+ if (exception == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG
+ )
+ .setAnchorView(binding.shareFab)
+ .show()
}
private suspend fun streamingLog() = withContext(Dispatchers.IO) {
@@ -184,36 +203,60 @@ class LogViewerActivity : AppCompatActivity() {
return@withContext
}
val stdout = BufferedReader(InputStreamReader(process!!.inputStream, StandardCharsets.UTF_8))
- var haveScrolled = false
- val start = System.nanoTime()
- var startPeriod = start
+
+ var posStart = 0
+ var timeLastNotify = System.nanoTime()
+ var priorModified = false
+ val bufferedLogLines = arrayListOf<LogLine>()
+ var timeout = 1000000000L / 2 // The timeout is initially small so that the view gets populated immediately.
+ val MAX_LINES = (1 shl 16) - 1
+ val MAX_BUFFERED_LINES = (1 shl 14) - 1
+
while (true) {
val line = stdout.readLine() ?: break
- rawLogLines.append(line)
- rawLogLines.append('\n')
+ if (rawLogLines.size() >= MAX_LINES)
+ rawLogLines.popFirst()
+ rawLogLines.addLast(line)
val logLine = parseLine(line)
+ if (logLine != null) {
+ bufferedLogLines.add(logLine)
+ } else {
+ if (bufferedLogLines.isNotEmpty()) {
+ bufferedLogLines.last().msg += "\n$line"
+ } else if (!logLines.isEmpty()) {
+ logLines[logLines.size() - 1].msg += "\n$line"
+ priorModified = true
+ }
+ }
+ val timeNow = System.nanoTime()
+ if (bufferedLogLines.size < MAX_BUFFERED_LINES && (timeNow - timeLastNotify) < timeout && stdout.ready())
+ continue
+ timeout = 1000000000L * 5 / 2 // Increase the timeout after the initial view has something in it.
+ timeLastNotify = timeNow
+
withContext(Dispatchers.Main.immediate) {
- if (logLine != null) {
- recyclerView?.let {
- val shouldScroll = haveScrolled && !it.canScrollVertically(1)
- logLines.add(logLine)
- if (haveScrolled) logAdapter.notifyDataSetChanged()
- if (shouldScroll)
- it.scrollToPosition(logLines.size - 1)
- }
- } else {
- logLines.lastOrNull()?.msg += "\n$line"
- if (haveScrolled) logAdapter.notifyDataSetChanged()
+ val isScrolledToBottomAlready = recyclerView?.canScrollVertically(1) == false
+ if (priorModified) {
+ logAdapter.notifyItemChanged(posStart - 1)
+ priorModified = false
+ }
+ val fullLen = logLines.size() + bufferedLogLines.size
+ if (fullLen >= MAX_LINES) {
+ val numToRemove = fullLen - MAX_LINES + 1
+ logLines.removeFromStart(numToRemove)
+ logAdapter.notifyItemRangeRemoved(0, numToRemove)
+ posStart -= numToRemove
+
+ }
+ for (bufferedLine in bufferedLogLines) {
+ logLines.addLast(bufferedLine)
}
- if (!haveScrolled) {
- val end = System.nanoTime()
- val scroll = (end - start) > 1000000000L * 2.5 || !stdout.ready()
- if (logLines.isNotEmpty() && (scroll || (end - startPeriod) > 1000000000L / 4)) {
- logAdapter.notifyDataSetChanged()
- recyclerView?.scrollToPosition(logLines.size - 1)
- startPeriod = end
- }
- if (scroll) haveScrolled = true
+ bufferedLogLines.clear()
+ logAdapter.notifyItemRangeInserted(posStart, logLines.size() - posStart)
+ posStart = logLines.size()
+
+ if (isScrolledToBottomAlready) {
+ recyclerView?.scrollToPosition(logLines.size() - 1)
}
}
}
@@ -248,7 +291,8 @@ class LogViewerActivity : AppCompatActivity() {
*
* <pre>05-26 11:02:36.886 5689 5689 D AndroidRuntime: CheckJNI is OFF.</pre>
*/
- 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 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"
}
@@ -259,7 +303,7 @@ class LogViewerActivity : AppCompatActivity() {
private fun levelToColor(level: String): Int {
return when (level) {
- "D" -> debugColor
+ "V", "D" -> debugColor
"E" -> errorColor
"I" -> infoColor
"W" -> warningColor
@@ -267,11 +311,11 @@ class LogViewerActivity : AppCompatActivity() {
}
}
- override fun getItemCount() = logLines.size
+ override fun getItemCount() = logLines.size()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
- .inflate(R.layout.log_viewer_entry, parent, false)
+ .inflate(R.layout.log_viewer_entry, parent, false)
return ViewHolder(view)
}
@@ -282,8 +326,10 @@ class LogViewerActivity : AppCompatActivity() {
else
SpannableString("${line.tag}: ${line.msg}").apply {
setSpan(StyleSpan(BOLD), 0, "${line.tag}:".length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- setSpan(ForegroundColorSpan(levelToColor(line.level)),
- 0, "${line.tag}:".length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+ setSpan(
+ ForegroundColorSpan(levelToColor(line.level)),
+ 0, "${line.tag}:".length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
}
holder.layout.apply {
findViewById<MaterialTextView>(R.id.log_date).text = line.time.toString()
@@ -305,11 +351,11 @@ class LogViewerActivity : AppCompatActivity() {
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? =
- logForUri(uri)?.let {
- val m = MatrixCursor(arrayOf(android.provider.OpenableColumns.DISPLAY_NAME, android.provider.OpenableColumns.SIZE), 1)
- m.addRow(arrayOf("wireguard-log.txt", it.size.toLong()))
- m
- }
+ logForUri(uri)?.let {
+ val m = MatrixCursor(arrayOf(android.provider.OpenableColumns.DISPLAY_NAME, android.provider.OpenableColumns.SIZE), 1)
+ m.addRow(arrayOf("wireguard-log.txt", it.size.toLong()))
+ m
+ }
override fun onCreate(): Boolean = true
@@ -319,7 +365,8 @@ class LogViewerActivity : AppCompatActivity() {
override fun getType(uri: Uri): String? = logForUri(uri)?.let { "text/plain" }
- override fun getStreamTypes(uri: Uri, mimeTypeFilter: String): Array<String>? = getType(uri)?.let { if (compareMimeTypes(it, mimeTypeFilter)) arrayOf(it) else null }
+ override fun getStreamTypes(uri: Uri, mimeTypeFilter: String): Array<String>? =
+ getType(uri)?.let { if (compareMimeTypes(it, mimeTypeFilter)) arrayOf(it) else null }
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
if (mode != "r") return null
diff --git a/ui/src/main/java/com/wireguard/android/activity/MainActivity.kt b/ui/src/main/java/com/wireguard/android/activity/MainActivity.kt
index aac08e18..80c4868c 100644
--- a/ui/src/main/java/com/wireguard/android/activity/MainActivity.kt
+++ b/ui/src/main/java/com/wireguard/android/activity/MainActivity.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity
@@ -9,6 +9,8 @@ import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
+import androidx.activity.OnBackPressedCallback
+import androidx.activity.addCallback
import androidx.appcompat.app.ActionBar
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
@@ -26,27 +28,29 @@ import com.wireguard.android.model.ObservableTunnel
class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener {
private var actionBar: ActionBar? = null
private var isTwoPaneLayout = false
+ private var backPressedCallback: OnBackPressedCallback? = null
- override fun onBackPressed() {
+ private fun handleBackPressed() {
val backStackEntries = supportFragmentManager.backStackEntryCount
// If the two-pane layout does not have an editor open, going back should exit the app.
if (isTwoPaneLayout && backStackEntries <= 1) {
finish()
return
}
- // Deselect the current tunnel on navigating back from the detail pane to the one-pane list.
- if (!isTwoPaneLayout && backStackEntries == 1) {
+
+ if (backStackEntries >= 1)
supportFragmentManager.popBackStack()
+
+ // Deselect the current tunnel on navigating back from the detail pane to the one-pane list.
+ if (backStackEntries == 1)
selectedTunnel = null
- return
- }
- super.onBackPressed()
}
override fun onBackStackChanged() {
+ val backStackEntries = supportFragmentManager.backStackEntryCount
+ backPressedCallback?.isEnabled = backStackEntries >= 1
if (actionBar == null) return
// Do not show the home menu when the two-pane layout is at the detail view (see above).
- val backStackEntries = supportFragmentManager.backStackEntryCount
val minBackStackEntries = if (isTwoPaneLayout) 2 else 1
actionBar!!.setDisplayHomeAsUpEnabled(backStackEntries >= minBackStackEntries)
}
@@ -57,6 +61,7 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener
actionBar = supportActionBar
isTwoPaneLayout = findViewById<View?>(R.id.master_detail_wrapper) != null
supportFragmentManager.addOnBackStackChangedListener(this)
+ backPressedCallback = onBackPressedDispatcher.addCallback(this) { handleBackPressed() }
onBackStackChanged()
}
@@ -69,9 +74,10 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener
return when (item.itemId) {
android.R.id.home -> {
// The back arrow in the action bar should act the same as the back button.
- onBackPressed()
+ onBackPressedDispatcher.onBackPressed()
true
}
+
R.id.menu_action_edit -> {
supportFragmentManager.commit {
replace(R.id.detail_container, TunnelEditorFragment())
@@ -86,18 +92,25 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener
startActivity(Intent(this, SettingsActivity::class.java))
true
}
+
else -> super.onOptionsItemSelected(item)
}
}
- override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?,
- newTunnel: ObservableTunnel?) {
+ override fun onSelectedTunnelChanged(
+ oldTunnel: ObservableTunnel?,
+ newTunnel: ObservableTunnel?
+ ): Boolean {
val fragmentManager = supportFragmentManager
+ if (fragmentManager.isStateSaved) {
+ return false
+ }
+
val backStackEntries = fragmentManager.backStackEntryCount
if (newTunnel == null) {
// Clear everything off the back stack (all editors and detail fragments).
fragmentManager.popBackStackImmediate(0, FragmentManager.POP_BACK_STACK_INCLUSIVE)
- return
+ return true
}
if (backStackEntries == 2) {
// Pop the editor off the back stack to reveal the detail fragment. Use the immediate
@@ -111,5 +124,6 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener
addToBackStack(null)
}
}
+ return true
}
}
diff --git a/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt b/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt
index df025163..bd6e1f78 100644
--- a/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt
+++ b/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt
@@ -1,18 +1,22 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity
+import android.content.ComponentName
import android.content.Intent
import android.os.Build
import android.os.Bundle
+import android.service.quicksettings.TileService
import android.view.MenuItem
+import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.wireguard.android.Application
+import com.wireguard.android.QuickTileService
import com.wireguard.android.R
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.preference.PreferencesPreferenceDataStore
@@ -24,7 +28,7 @@ import kotlinx.coroutines.withContext
/**
* Interface for changing application-global persistent settings.
*/
-class SettingsActivity : ThemeChangeAwareActivity() {
+class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (supportFragmentManager.findFragmentById(android.R.id.content) == null) {
@@ -46,7 +50,13 @@ class SettingsActivity : ThemeChangeAwareActivity() {
override fun onCreatePreferences(savedInstanceState: Bundle?, key: String?) {
preferenceManager.preferenceDataStore = PreferencesPreferenceDataStore(lifecycleScope, Application.getPreferencesDataStore())
addPreferencesFromResource(R.xml.preferences)
- preferenceScreen.initialExpandedChildrenCount = 4
+ preferenceScreen.initialExpandedChildrenCount = 5
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || QuickTileService.isAdded) {
+ val quickTile = preferenceManager.findPreference<Preference>("quick_tile")
+ quickTile?.parent?.removePreference(quickTile)
+ --preferenceScreen.initialExpandedChildrenCount
+ }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val darkTheme = preferenceManager.findPreference<Preference>("dark_theme")
darkTheme?.parent?.removePreference(darkTheme)
@@ -61,9 +71,9 @@ class SettingsActivity : ThemeChangeAwareActivity() {
zipExporter?.parent?.removePreference(zipExporter)
}
val wgQuickOnlyPrefs = arrayOf(
- preferenceManager.findPreference("tools_installer"),
- preferenceManager.findPreference("restore_on_boot"),
- preferenceManager.findPreference<Preference>("multiple_tunnels")
+ preferenceManager.findPreference("tools_installer"),
+ preferenceManager.findPreference("restore_on_boot"),
+ preferenceManager.findPreference<Preference>("multiple_tunnels")
).filterNotNull()
wgQuickOnlyPrefs.forEach { it.isVisible = false }
lifecycleScope.launch {
diff --git a/ui/src/main/java/com/wireguard/android/activity/ThemeChangeAwareActivity.kt b/ui/src/main/java/com/wireguard/android/activity/ThemeChangeAwareActivity.kt
deleted file mode 100644
index 2158858b..00000000
--- a/ui/src/main/java/com/wireguard/android/activity/ThemeChangeAwareActivity.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-package com.wireguard.android.activity
-
-import android.os.Build
-import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
-import androidx.appcompat.app.AppCompatDelegate
-import androidx.lifecycle.lifecycleScope
-import com.wireguard.android.util.UserKnobs
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-
-abstract class ThemeChangeAwareActivity : AppCompatActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
- UserKnobs.darkTheme.onEach {
- val newMode = if (it) {
- AppCompatDelegate.MODE_NIGHT_YES
- } else {
- AppCompatDelegate.MODE_NIGHT_NO
- }
- if (AppCompatDelegate.getDefaultNightMode() != newMode) {
- AppCompatDelegate.setDefaultNightMode(newMode)
- recreate()
- }
- }.launchIn(lifecycleScope)
- }
- }
-}
diff --git a/ui/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.kt b/ui/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.kt
index 28d5da3a..bdf798ca 100644
--- a/ui/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.kt
+++ b/ui/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity
@@ -22,7 +22,8 @@ class TunnelCreatorActivity : BaseActivity() {
}
}
- override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?) {
+ override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?): Boolean {
finish()
+ return true
}
}
diff --git a/ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt b/ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt
index ebf059a5..59b9349f 100644
--- a/ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt
+++ b/ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity
@@ -24,7 +24,8 @@ import kotlinx.coroutines.launch
@RequiresApi(Build.VERSION_CODES.N)
class TunnelToggleActivity : AppCompatActivity() {
- private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { toggleTunnelWithPermissionsResult() }
+ private val permissionActivityResultLauncher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { toggleTunnelWithPermissionsResult() }
private fun toggleTunnelWithPermissionsResult() {
val tunnel = Application.getTunnelManager().lastUsedTunnel ?: return
diff --git a/ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt b/ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt
index 7b0737e8..4c86b4c8 100644
--- a/ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt
+++ b/ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt
@@ -1,11 +1,14 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.activity
import android.Manifest
+import android.content.ActivityNotFoundException
+import android.content.Context
+import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
@@ -16,17 +19,21 @@ import android.os.storage.StorageVolume
import android.util.Log
import android.view.View
import android.widget.Toast
+import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.view.forEach
import androidx.databinding.DataBindingUtil
+import androidx.databinding.Observable
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.google.android.material.dialog.MaterialAlertDialogBuilder
import com.wireguard.android.Application
import com.wireguard.android.R
import com.wireguard.android.backend.GoBackend
@@ -41,6 +48,8 @@ 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 com.wireguard.android.util.UserKnobs
+import com.wireguard.android.util.applicationScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -48,7 +57,27 @@ import kotlinx.coroutines.withContext
import java.io.File
class TvMainActivity : AppCompatActivity() {
- private val tunnelFileImportResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { data ->
+ private val tunnelFileImportResultLauncher = registerForActivityResult(object : ActivityResultContracts.GetContent() {
+ override fun createIntent(context: Context, input: String): Intent {
+ val intent = super.createIntent(context, input)
+
+ /* AndroidTV now comes with stubs that do nothing but display a Toast less helpful than
+ * what we can do, so detect this and throw an exception that we can catch later. */
+ val activitiesToResolveIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ context.packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()))
+ } else {
+ @Suppress("DEPRECATION")
+ context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
+ }
+ if (activitiesToResolveIntent.all {
+ val name = it.activityInfo.packageName
+ name.startsWith("com.google.android.tv.frameworkpackagestubs") || name.startsWith("com.android.tv.frameworkpackagestubs")
+ }) {
+ throw ActivityNotFoundException()
+ }
+ return intent
+ }
+ }) { data ->
if (data == null) return@registerForActivityResult
lifecycleScope.launch {
TunnelImporter.importTunnel(contentResolver, data) {
@@ -84,6 +113,14 @@ class TvMainActivity : AppCompatActivity() {
private val filesRoot = ObservableField("")
override fun onCreate(savedInstanceState: Bundle?) {
+ if (AppCompatDelegate.getDefaultNightMode() != AppCompatDelegate.MODE_NIGHT_YES) {
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ applicationScope.launch {
+ UserKnobs.setDarkTheme(true)
+ }
+ }
+ }
super.onCreate(savedInstanceState)
binding = TvActivityBinding.inflate(layoutInflater)
lifecycleScope.launch {
@@ -174,7 +211,13 @@ class TvMainActivity : AppCompatActivity() {
try {
tunnelFileImportResultLauncher.launch("*/*")
} catch (_: Throwable) {
- Toast.makeText(this@TvMainActivity, getString(R.string.tv_no_file_picker), Toast.LENGTH_LONG).show()
+ MaterialAlertDialogBuilder(binding.root.context).setMessage(R.string.tv_no_file_picker).setCancelable(false)
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ try {
+ startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://webstoreredirect")))
+ } catch (_: Throwable) {
+ }
+ }.show()
}
}
}
@@ -185,6 +228,17 @@ class TvMainActivity : AppCompatActivity() {
binding.tunnelList.requestFocus()
}
}
+
+ val backPressedCallback = onBackPressedDispatcher.addCallback(this) { handleBackPressed() }
+ val updateBackPressedCallback = object : Observable.OnPropertyChangedCallback() {
+ override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
+ backPressedCallback.isEnabled = isDeleting.get() || filesRoot.get()?.isNotEmpty() == true
+ }
+ }
+ isDeleting.addOnPropertyChangedCallback(updateBackPressedCallback)
+ filesRoot.addOnPropertyChangedCallback(updateBackPressedCallback)
+ backPressedCallback.isEnabled = false
+
binding.executePendingBindings()
setContentView(binding.root)
@@ -298,7 +352,7 @@ class TvMainActivity : AppCompatActivity() {
}
}
- override fun onBackPressed() {
+ private fun handleBackPressed() {
when {
isDeleting.get() -> {
isDeleting.set(false)
@@ -306,6 +360,7 @@ class TvMainActivity : AppCompatActivity() {
binding.tunnelList.requestFocus()
}
}
+
filesRoot.get()?.isNotEmpty() == true -> {
files.clear()
filesRoot.set("")
@@ -313,14 +368,13 @@ class TvMainActivity : AppCompatActivity() {
binding.tunnelList.requestFocus()
}
}
- else -> super.onBackPressed()
}
}
private suspend fun updateStats() {
binding.tunnelList.forEach { viewItem ->
val listItem = DataBindingUtil.findBinding<TvTunnelListItemBinding>(viewItem)
- ?: return@forEach
+ ?: return@forEach
try {
val tunnel = listItem.item!!
if (tunnel.state != Tunnel.State.UP || isDeleting.get()) {
diff --git a/ui/src/main/java/com/wireguard/android/configStore/ConfigStore.kt b/ui/src/main/java/com/wireguard/android/configStore/ConfigStore.kt
index 5b66f830..7336e78f 100644
--- a/ui/src/main/java/com/wireguard/android/configStore/ConfigStore.kt
+++ b/ui/src/main/java/com/wireguard/android/configStore/ConfigStore.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.configStore
diff --git a/ui/src/main/java/com/wireguard/android/configStore/FileConfigStore.kt b/ui/src/main/java/com/wireguard/android/configStore/FileConfigStore.kt
index 1099382d..17e3221b 100644
--- a/ui/src/main/java/com/wireguard/android/configStore/FileConfigStore.kt
+++ b/ui/src/main/java/com/wireguard/android/configStore/FileConfigStore.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.configStore
@@ -40,9 +40,9 @@ class FileConfigStore(private val context: Context) : ConfigStore {
override fun enumerate(): Set<String> {
return context.fileList()
- .filter { it.endsWith(".conf") }
- .map { it.substring(0, it.length - ".conf".length) }
- .toSet()
+ .filter { it.endsWith(".conf") }
+ .map { it.substring(0, it.length - ".conf".length) }
+ .toSet()
}
private fun fileFor(name: String): File {
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 9c8a0dc2..afba41cb 100644
--- a/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt
+++ b/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.databinding
@@ -23,6 +23,7 @@ import com.wireguard.android.R
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler
import com.wireguard.android.widget.ToggleSwitch
import com.wireguard.android.widget.ToggleSwitch.OnBeforeCheckedChangeListener
+import com.wireguard.android.widget.TvCardView
import com.wireguard.config.Attribute
import com.wireguard.config.InetNetwork
import java.net.InetAddress
@@ -46,9 +47,11 @@ object BindingAdapters {
@JvmStatic
@BindingAdapter("items", "layout", "fragment")
- fun <E> setItems(view: LinearLayout,
- oldList: ObservableList<E>?, oldLayoutId: Int, @Suppress("UNUSED_PARAMETER") oldFragment: Fragment?,
- newList: ObservableList<E>?, newLayoutId: Int, newFragment: Fragment?) {
+ fun <E> setItems(
+ view: LinearLayout,
+ oldList: ObservableList<E>?, oldLayoutId: Int, @Suppress("UNUSED_PARAMETER") oldFragment: Fragment?,
+ newList: ObservableList<E>?, newLayoutId: Int, newFragment: Fragment?
+ ) {
if (oldList === newList && oldLayoutId == newLayoutId)
return
var listener: ItemChangeListener<E>? = ListenerUtil.getListener(view, R.id.item_change_listener)
@@ -72,9 +75,11 @@ object BindingAdapters {
@JvmStatic
@BindingAdapter("items", "layout")
- fun <E> setItems(view: LinearLayout,
- oldList: Iterable<E>?, oldLayoutId: Int,
- newList: Iterable<E>?, newLayoutId: Int) {
+ fun <E> setItems(
+ view: LinearLayout,
+ oldList: Iterable<E>?, oldLayoutId: Int,
+ newList: Iterable<E>?, newLayoutId: Int
+ ) {
if (oldList === newList && oldLayoutId == newLayoutId)
return
view.removeAllViews()
@@ -92,11 +97,13 @@ object BindingAdapters {
@JvmStatic
@BindingAdapter(requireAll = false, value = ["items", "layout", "configurationHandler"])
- fun <K, E : Keyed<out K>> setItems(view: RecyclerView,
- oldList: ObservableKeyedArrayList<K, E>?, oldLayoutId: Int,
- @Suppress("UNUSED_PARAMETER") oldRowConfigurationHandler: RowConfigurationHandler<*, *>?,
- newList: ObservableKeyedArrayList<K, E>?, newLayoutId: Int,
- newRowConfigurationHandler: RowConfigurationHandler<*, *>?) {
+ fun <K, E : Keyed<out K>> setItems(
+ view: RecyclerView,
+ oldList: ObservableKeyedArrayList<K, E>?, oldLayoutId: Int,
+ @Suppress("UNUSED_PARAMETER") oldRowConfigurationHandler: RowConfigurationHandler<*, *>?,
+ newList: ObservableKeyedArrayList<K, E>?, newLayoutId: Int,
+ newRowConfigurationHandler: RowConfigurationHandler<*, *>?
+ ) {
if (view.layoutManager == null)
view.layoutManager = LinearLayoutManager(view.context, RecyclerView.VERTICAL, false)
if (oldList === newList && oldLayoutId == newLayoutId)
@@ -122,16 +129,20 @@ object BindingAdapters {
@JvmStatic
@BindingAdapter("onBeforeCheckedChanged")
- fun setOnBeforeCheckedChanged(view: ToggleSwitch,
- listener: OnBeforeCheckedChangeListener?) {
+ fun setOnBeforeCheckedChanged(
+ view: ToggleSwitch,
+ listener: OnBeforeCheckedChangeListener?
+ ) {
view.setOnBeforeCheckedChangeListener(listener)
}
@JvmStatic
@BindingAdapter("onFocusChange")
- fun setOnFocusChange(view: EditText,
- listener: View.OnFocusChangeListener?) {
- view.setOnFocusChangeListener(listener)
+ fun setOnFocusChange(
+ view: EditText,
+ listener: View.OnFocusChangeListener?
+ ) {
+ view.onFocusChangeListener = listener
}
@JvmStatic
@@ -168,4 +179,16 @@ object BindingAdapters {
0
}
}
+
+ @JvmStatic
+ @BindingAdapter("isUp")
+ fun setIsUp(card: TvCardView, up: Boolean) {
+ card.isUp = up
+ }
+
+ @JvmStatic
+ @BindingAdapter("isDeleting")
+ fun setIsDeleting(card: TvCardView, deleting: Boolean) {
+ card.isDeleting = deleting
+ }
}
diff --git a/ui/src/main/java/com/wireguard/android/databinding/ItemChangeListener.kt b/ui/src/main/java/com/wireguard/android/databinding/ItemChangeListener.kt
index d1a1352b..da153bbe 100644
--- a/ui/src/main/java/com/wireguard/android/databinding/ItemChangeListener.kt
+++ b/ui/src/main/java/com/wireguard/android/databinding/ItemChangeListener.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.databinding
@@ -61,8 +61,10 @@ internal class ItemChangeListener<T>(private val container: ViewGroup, private v
}
}
- override fun onItemRangeChanged(sender: ObservableList<T>, positionStart: Int,
- itemCount: Int) {
+ override fun onItemRangeChanged(
+ sender: ObservableList<T>, positionStart: Int,
+ itemCount: Int
+ ) {
val listener = weakListener.get()
if (listener != null) {
for (i in positionStart until positionStart + itemCount) {
@@ -75,8 +77,10 @@ internal class ItemChangeListener<T>(private val container: ViewGroup, private v
}
}
- override fun onItemRangeInserted(sender: ObservableList<T>, positionStart: Int,
- itemCount: Int) {
+ override fun onItemRangeInserted(
+ sender: ObservableList<T>, positionStart: Int,
+ itemCount: Int
+ ) {
val listener = weakListener.get()
if (listener != null) {
for (i in positionStart until positionStart + itemCount)
@@ -86,8 +90,10 @@ internal class ItemChangeListener<T>(private val container: ViewGroup, private v
}
}
- override fun onItemRangeMoved(sender: ObservableList<T>, fromPosition: Int,
- toPosition: Int, itemCount: Int) {
+ override fun onItemRangeMoved(
+ sender: ObservableList<T>, fromPosition: Int,
+ toPosition: Int, itemCount: Int
+ ) {
val listener = weakListener.get()
if (listener != null) {
val views = arrayOfNulls<View>(itemCount)
@@ -99,8 +105,10 @@ internal class ItemChangeListener<T>(private val container: ViewGroup, private v
}
}
- override fun onItemRangeRemoved(sender: ObservableList<T>, positionStart: Int,
- itemCount: Int) {
+ override fun onItemRangeRemoved(
+ sender: ObservableList<T>, positionStart: Int,
+ itemCount: Int
+ ) {
val listener = weakListener.get()
if (listener != null) {
listener.container.removeViews(positionStart, itemCount)
diff --git a/ui/src/main/java/com/wireguard/android/databinding/Keyed.kt b/ui/src/main/java/com/wireguard/android/databinding/Keyed.kt
index 1122f552..f91581d0 100644
--- a/ui/src/main/java/com/wireguard/android/databinding/Keyed.kt
+++ b/ui/src/main/java/com/wireguard/android/databinding/Keyed.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.databinding
diff --git a/ui/src/main/java/com/wireguard/android/databinding/ObservableKeyedArrayList.kt b/ui/src/main/java/com/wireguard/android/databinding/ObservableKeyedArrayList.kt
index 9963255a..947644b3 100644
--- a/ui/src/main/java/com/wireguard/android/databinding/ObservableKeyedArrayList.kt
+++ b/ui/src/main/java/com/wireguard/android/databinding/ObservableKeyedArrayList.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.databinding
diff --git a/ui/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.kt b/ui/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.kt
index 003ff74e..91223ad1 100644
--- a/ui/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.kt
+++ b/ui/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.databinding
diff --git a/ui/src/main/java/com/wireguard/android/databinding/ObservableSortedKeyedArrayList.kt b/ui/src/main/java/com/wireguard/android/databinding/ObservableSortedKeyedArrayList.kt
index a8738627..a09d726f 100644
--- a/ui/src/main/java/com/wireguard/android/databinding/ObservableSortedKeyedArrayList.kt
+++ b/ui/src/main/java/com/wireguard/android/databinding/ObservableSortedKeyedArrayList.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.databinding
diff --git a/ui/src/main/java/com/wireguard/android/fragment/AddTunnelsSheet.kt b/ui/src/main/java/com/wireguard/android/fragment/AddTunnelsSheet.kt
index c56462d6..78b9f3d7 100644
--- a/ui/src/main/java/com/wireguard/android/fragment/AddTunnelsSheet.kt
+++ b/ui/src/main/java/com/wireguard/android/fragment/AddTunnelsSheet.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment
@@ -34,10 +34,6 @@ class AddTunnelsSheet : BottomSheetDialogFragment() {
}
}
- override fun getTheme(): Int {
- return R.style.BottomSheetDialogTheme
- }
-
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)
@@ -76,7 +72,7 @@ class AddTunnelsSheet : BottomSheetDialogFragment() {
}
})
val gradientDrawable = GradientDrawable().apply {
- setColor(requireContext().resolveAttribute(R.attr.colorBackground))
+ setColor(requireContext().resolveAttribute(com.google.android.material.R.attr.colorSurface))
}
view.background = gradientDrawable
}
diff --git a/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt
index f6f57ddb..1cd19934 100644
--- a/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt
+++ b/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt
@@ -1,11 +1,15 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment
import android.Manifest
import android.app.Dialog
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.PackageInfoFlags
+import android.os.Build
import android.os.Bundle
import android.widget.Button
import android.widget.Toast
@@ -15,6 +19,7 @@ import androidx.databinding.Observable
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.setFragmentResult
import androidx.lifecycle.lifecycleScope
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.tabs.TabLayout
import com.wireguard.android.BR
import com.wireguard.android.R
@@ -40,11 +45,12 @@ class AppListDialogFragment : DialogFragment() {
try {
val applicationData: MutableList<ApplicationData> = ArrayList()
withContext(Dispatchers.IO) {
- val packageInfos = pm.getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET), 0)
+ val packageInfos = getPackagesHoldingPermissions(pm, arrayOf(Manifest.permission.INTERNET))
packageInfos.forEach {
val packageName = it.packageName
val appInfo = it.applicationInfo
- val appData = ApplicationData(appInfo.loadIcon(pm), appInfo.loadLabel(pm).toString(), packageName, currentlySelectedApps.contains(packageName))
+ 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) {
@@ -58,6 +64,7 @@ class AppListDialogFragment : DialogFragment() {
withContext(Dispatchers.Main.immediate) {
appData.clear()
appData.addAll(applicationData)
+ setButtonText()
}
} catch (e: Throwable) {
withContext(Dispatchers.Main.immediate) {
@@ -76,6 +83,15 @@ class AppListDialogFragment : DialogFragment() {
initiallyExcluded = arguments?.getBoolean(KEY_IS_EXCLUDED) ?: true
}
+ private fun getPackagesHoldingPermissions(pm: PackageManager, permissions: Array<String>): List<PackageInfo> {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ pm.getPackagesHoldingPermissions(permissions, PackageInfoFlags.of(0L))
+ } else {
+ @Suppress("DEPRECATION")
+ pm.getPackagesHoldingPermissions(permissions, 0)
+ }
+ }
+
private fun setButtonText() {
val numSelected = appData.count { it.isSelected }
button?.text = if (numSelected == 0)
@@ -88,7 +104,7 @@ class AppListDialogFragment : DialogFragment() {
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- val alertDialogBuilder = AlertDialog.Builder(requireActivity())
+ val alertDialogBuilder = MaterialAlertDialogBuilder(requireActivity())
val binding = AppListDialogFragmentBinding.inflate(requireActivity().layoutInflater, null, false)
binding.executePendingBindings()
alertDialogBuilder.setView(binding.root)
@@ -128,10 +144,12 @@ class AppListDialogFragment : DialogFragment() {
selectedApps.add(data.packageName)
}
}
- setFragmentResult(REQUEST_SELECTION, bundleOf(
+ setFragmentResult(
+ REQUEST_SELECTION, bundleOf(
KEY_SELECTED_APPS to selectedApps.toTypedArray(),
KEY_IS_EXCLUDED to (tabs?.selectedTabPosition == 0)
- ))
+ )
+ )
dismiss()
}
@@ -139,6 +157,7 @@ class AppListDialogFragment : DialogFragment() {
const val KEY_SELECTED_APPS = "selected_apps"
const val KEY_IS_EXCLUDED = "is_excluded"
const val REQUEST_SELECTION = "request_selection"
+
fun newInstance(selectedApps: ArrayList<String?>?, isExcluded: Boolean): AppListDialogFragment {
val extras = Bundle()
extras.putStringArrayList(KEY_SELECTED_APPS, selectedApps)
diff --git a/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt
index 783f5722..d5c1723f 100644
--- a/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt
+++ b/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment
@@ -67,12 +67,20 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
val activity = activity ?: return
activity.lifecycleScope.launch {
if (Application.getBackend() is GoBackend) {
- val intent = GoBackend.VpnService.prepare(activity)
- if (intent != null) {
- pendingTunnel = tunnel
- pendingTunnelUp = checked
- permissionActivityResultLauncher.launch(intent)
- return@launch
+ try {
+ val intent = GoBackend.VpnService.prepare(activity)
+ if (intent != null) {
+ pendingTunnel = tunnel
+ pendingTunnelUp = checked
+ permissionActivityResultLauncher.launch(intent)
+ return@launch
+ }
+ } catch (e: Throwable) {
+ val message = activity.getString(R.string.error_prepare, ErrorMessages[e])
+ Snackbar.make(view, message, Snackbar.LENGTH_LONG)
+ .setAnchorView(view.findViewById(R.id.create_fab))
+ .show()
+ Log.e(TAG, message, e)
}
}
setTunnelStateWithPermissionsResult(tunnel, checked)
@@ -91,8 +99,8 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
val view = view
if (view != null)
Snackbar.make(view, message, Snackbar.LENGTH_LONG)
- .setAnchorView(view.findViewById(R.id.create_fab))
- .show()
+ .setAnchorView(view.findViewById(R.id.create_fab))
+ .show()
else
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
Log.e(TAG, message, e)
diff --git a/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt
index d20f21c6..34c96505 100644
--- a/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt
+++ b/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt
@@ -1,18 +1,15 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment
import android.app.Dialog
-import android.content.DialogInterface
import android.os.Bundle
-import android.view.inputmethod.InputMethodManager
-import androidx.appcompat.app.AlertDialog
-import androidx.core.content.getSystemService
+import android.view.WindowManager
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
-import com.google.android.material.textfield.TextInputEditText
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.wireguard.android.Application
import com.wireguard.android.R
import com.wireguard.android.databinding.ConfigNamingDialogFragmentBinding
@@ -26,7 +23,6 @@ import java.nio.charset.StandardCharsets
class ConfigNamingDialogFragment : DialogFragment() {
private var binding: ConfigNamingDialogFragmentBinding? = null
private var config: Config? = null
- private var imm: InputMethodManager? = null
private fun createTunnelAndDismiss() {
val binding = binding ?: return
@@ -42,11 +38,6 @@ class ConfigNamingDialogFragment : DialogFragment() {
}
}
- override fun dismiss() {
- setKeyboardVisible(false)
- super.dismiss()
- }
-
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val configText = requireArguments().getString(KEY_CONFIG_TEXT)
@@ -63,51 +54,23 @@ class ConfigNamingDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val activity = requireActivity()
- imm = activity.getSystemService()
- val alertDialogBuilder = AlertDialog.Builder(activity)
+ val alertDialogBuilder = MaterialAlertDialogBuilder(activity)
alertDialogBuilder.setTitle(R.string.import_from_qr_code)
binding = ConfigNamingDialogFragmentBinding.inflate(activity.layoutInflater, null, false)
binding?.apply {
executePendingBindings()
alertDialogBuilder.setView(root)
}
- alertDialogBuilder.setPositiveButton(R.string.create_tunnel, null)
+ alertDialogBuilder.setPositiveButton(R.string.create_tunnel) { _, _ -> createTunnelAndDismiss() }
alertDialogBuilder.setNegativeButton(R.string.cancel) { _, _ -> dismiss() }
- return alertDialogBuilder.create().apply {
- setOnShowListener {
- findViewById<TextInputEditText>(R.id.tunnel_name_text)?.apply {
- setOnFocusChangeListener { v, _ ->
- v.post {
- imm?.showSoftInput(v, InputMethodManager.SHOW_IMPLICIT)
- }
- }
- requestFocus()
- }
- }
- }
- }
-
- override fun onResume() {
- super.onResume()
- val dialog = dialog as AlertDialog?
- if (dialog != null) {
- dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener { createTunnelAndDismiss() }
- setKeyboardVisible(true)
- }
- }
-
- private fun setKeyboardVisible(visible: Boolean) {
- if (visible) {
- imm!!.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0)
- } else if (binding != null) {
- imm!!.hideSoftInputFromWindow(binding!!.tunnelNameText.windowToken, 0)
- }
+ val dialog = alertDialogBuilder.create()
+ dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
+ return dialog
}
companion object {
private const val KEY_CONFIG_TEXT = "config_text"
- @JvmStatic
fun newInstance(configText: String?): ConfigNamingDialogFragment {
val extras = Bundle()
extras.putString(KEY_CONFIG_TEXT, configText)
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 7046cb96..81d8e8c6 100644
--- a/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt
+++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment
@@ -8,9 +8,12 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
+import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
+import androidx.core.view.MenuProvider
import androidx.databinding.DataBindingUtil
+import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import com.wireguard.android.R
import com.wireguard.android.backend.Tunnel
@@ -24,28 +27,34 @@ import kotlinx.coroutines.launch
/**
* Fragment that shows details about a specific tunnel.
*/
-class TunnelDetailFragment : BaseFragment() {
+class TunnelDetailFragment : BaseFragment(), MenuProvider {
private var binding: TunnelDetailFragmentBinding? = null
private var lastState = Tunnel.State.TOGGLE
private var timerActive = true
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setHasOptionsMenu(true)
+ override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
+ return false
}
- override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
- inflater.inflate(R.menu.tunnel_detail, menu)
+ override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
+ menuInflater.inflate(R.menu.tunnel_detail, menu)
}
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?): View? {
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
super.onCreateView(inflater, container, savedInstanceState)
binding = TunnelDetailFragmentBinding.inflate(inflater, container, false)
binding?.executePendingBindings()
return binding?.root
}
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
+ }
+
override fun onDestroyView() {
binding = null
super.onDestroyView()
@@ -103,25 +112,38 @@ class TunnelDetailFragment : BaseFragment() {
val statistics = tunnel.getStatisticsAsync()
for (i in 0 until binding.peersLayout.childCount) {
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i))
- ?: continue
+ ?: continue
val publicKey = peer.item!!.publicKey
- val rx = statistics.peerRx(publicKey)
- val tx = statistics.peerTx(publicKey)
- if (rx == 0L && tx == 0L) {
+ val peerStats = statistics.peer(publicKey)
+ if (peerStats == null || (peerStats.rxBytes == 0L && peerStats.txBytes == 0L)) {
peer.transferLabel.visibility = View.GONE
peer.transferText.visibility = View.GONE
- continue
+ } else {
+ peer.transferText.text = getString(
+ R.string.transfer_rx_tx,
+ QuantityFormatter.formatBytes(peerStats.rxBytes),
+ QuantityFormatter.formatBytes(peerStats.txBytes)
+ )
+ peer.transferLabel.visibility = View.VISIBLE
+ peer.transferText.visibility = View.VISIBLE
+ }
+ if (peerStats == null || peerStats.latestHandshakeEpochMillis == 0L) {
+ peer.latestHandshakeLabel.visibility = View.GONE
+ peer.latestHandshakeText.visibility = View.GONE
+ } else {
+ peer.latestHandshakeText.text = QuantityFormatter.formatEpochAgo(peerStats.latestHandshakeEpochMillis)
+ peer.latestHandshakeLabel.visibility = View.VISIBLE
+ peer.latestHandshakeText.visibility = View.VISIBLE
}
- 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
+ ?: continue
peer.transferLabel.visibility = View.GONE
peer.transferText.visibility = View.GONE
+ peer.latestHandshakeLabel.visibility = View.GONE
+ peer.latestHandshakeText.visibility = View.GONE
}
}
}
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 716f7b25..edf4b226 100644
--- a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt
+++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment
@@ -18,6 +18,9 @@ import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
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.wireguard.android.Application
@@ -35,7 +38,7 @@ import kotlinx.coroutines.launch
/**
* Fragment for editing a WireGuard configuration.
*/
-class TunnelEditorFragment : BaseFragment() {
+class TunnelEditorFragment : BaseFragment(), MenuProvider {
private var haveShownKeys = false
private var binding: TunnelEditorFragmentBinding? = null
private var tunnel: ObservableTunnel? = null
@@ -63,17 +66,14 @@ class TunnelEditorFragment : BaseFragment() {
}
}
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setHasOptionsMenu(true)
+ override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
+ menuInflater.inflate(R.menu.config_editor, menu)
}
- override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
- inflater.inflate(R.menu.config_editor, menu)
- }
-
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?): View? {
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
super.onCreateView(inflater, container, savedInstanceState)
binding = TunnelEditorFragmentBinding.inflate(inflater, container, false)
binding?.apply {
@@ -83,6 +83,11 @@ class TunnelEditorFragment : BaseFragment() {
return binding?.root
}
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
+ }
+
override fun onDestroyView() {
activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
binding = null
@@ -95,8 +100,10 @@ class TunnelEditorFragment : BaseFragment() {
val focusedView = activity.currentFocus
if (focusedView != null) {
val inputManager = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
- inputManager?.hideSoftInputFromWindow(focusedView.windowToken,
- InputMethodManager.HIDE_NOT_ALWAYS)
+ inputManager?.hideSoftInputFromWindow(
+ focusedView.windowToken,
+ InputMethodManager.HIDE_NOT_ALWAYS
+ )
}
parentFragmentManager.popBackStackImmediate()
@@ -105,8 +112,8 @@ class TunnelEditorFragment : BaseFragment() {
selectedTunnel = tunnel
}
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- if (item.itemId == R.id.menu_action_save) {
+ override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
+ if (menuItem.itemId == R.id.menu_action_save) {
binding ?: return false
val newConfig = try {
binding!!.config!!.resolve()
@@ -130,6 +137,7 @@ class TunnelEditorFragment : BaseFragment() {
onTunnelCreated(null, e)
}
}
+
tunnel!!.name != binding!!.name -> {
Log.d(TAG, "Attempting to rename tunnel to " + binding!!.name)
try {
@@ -139,6 +147,7 @@ class TunnelEditorFragment : BaseFragment() {
onTunnelRenamed(tunnel!!, newConfig, e)
}
}
+
else -> {
Log.d(TAG, "Attempting to save config of " + tunnel!!.name)
try {
@@ -152,7 +161,7 @@ class TunnelEditorFragment : BaseFragment() {
}
return true
}
- return super.onOptionsItemSelected(item)
+ return false
}
@Suppress("UNUSED_PARAMETER")
@@ -194,8 +203,10 @@ class TunnelEditorFragment : BaseFragment() {
super.onSaveInstanceState(outState)
}
- override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?,
- newTunnel: ObservableTunnel?) {
+ override fun onSelectedTunnelChanged(
+ oldTunnel: ObservableTunnel?,
+ newTunnel: ObservableTunnel?
+ ) {
tunnel = newTunnel
if (binding == null) return
binding!!.config = ConfigProxy()
@@ -232,8 +243,10 @@ class TunnelEditorFragment : BaseFragment() {
}
}
- private suspend fun onTunnelRenamed(renamedTunnel: ObservableTunnel, newConfig: Config,
- throwable: Throwable?) {
+ private suspend fun onTunnelRenamed(
+ renamedTunnel: ObservableTunnel, newConfig: Config,
+ throwable: Throwable?
+ ) {
val ctx = activity ?: Application.get()
if (throwable == null) {
val message = ctx.getString(R.string.tunnel_rename_success, renamedTunnel.name)
@@ -265,7 +278,7 @@ class TunnelEditorFragment : BaseFragment() {
onSelectedTunnelChanged(null, selectedTunnel)
} else {
tunnel = selectedTunnel
- val config: ConfigProxy = savedInstanceState.getParcelable(KEY_LOCAL_CONFIG)!!
+ val config = BundleCompat.getParcelable(savedInstanceState, KEY_LOCAL_CONFIG, ConfigProxy::class.java)!!
val originalName = savedInstanceState.getString(KEY_ORIGINAL_NAME)
if (tunnel != null && tunnel!!.name != originalName) onSelectedTunnelChanged(null, tunnel) else binding!!.config = config
}
@@ -290,13 +303,16 @@ class TunnelEditorFragment : BaseFragment() {
haveShownKeys = true
showPrivateKey(edit)
}
+
is BiometricAuthenticator.Result.Failure -> {
Snackbar.make(
- binding!!.mainContainer,
- it.message,
- Snackbar.LENGTH_SHORT
+ binding!!.mainContainer,
+ it.message,
+ Snackbar.LENGTH_SHORT
).show()
}
+
+ is BiometricAuthenticator.Result.Cancelled -> {}
}
}
} else {
diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt
index 71e409a7..cba7c476 100644
--- a/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt
+++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.fragment
@@ -16,11 +16,14 @@ import android.view.ViewGroup
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.Toast
+import androidx.activity.OnBackPressedCallback
+import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar
+import com.google.zxing.qrcode.QRCodeReader
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
import com.wireguard.android.Application
@@ -30,15 +33,15 @@ import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowC
import com.wireguard.android.databinding.TunnelListFragmentBinding
import com.wireguard.android.databinding.TunnelListItemBinding
import com.wireguard.android.model.ObservableTunnel
+import com.wireguard.android.updater.SnackbarUpdateShower
import com.wireguard.android.util.ErrorMessages
+import com.wireguard.android.util.QrCodeFromFileScanner
import com.wireguard.android.util.TunnelImporter
import com.wireguard.android.widget.MultiselectableRelativeLayout
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
-import java.util.ArrayList
-import java.util.HashSet
/**
* Fragment containing a list of known WireGuard tunnels. It allows creating and deleting tunnels.
@@ -46,13 +49,27 @@ import java.util.HashSet
class TunnelListFragment : BaseFragment() {
private val actionModeListener = ActionModeListener()
private var actionMode: ActionMode? = null
+ private var backPressedCallback: OnBackPressedCallback? = null
private var binding: TunnelListFragmentBinding? = null
private val tunnelFileImportResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { data ->
if (data == null) return@registerForActivityResult
val activity = activity ?: return@registerForActivityResult
val contentResolver = activity.contentResolver ?: return@registerForActivityResult
activity.lifecycleScope.launch {
- TunnelImporter.importTunnel(contentResolver, data) { showSnackbar(it) }
+ if (QrCodeFromFileScanner.validContentType(contentResolver, data)) {
+ try {
+ val qrCodeFromFileScanner = QrCodeFromFileScanner(contentResolver, QRCodeReader())
+ val result = qrCodeFromFileScanner.scan(data)
+ TunnelImporter.importTunnel(parentFragmentManager, result.text) { showSnackbar(it) }
+ } catch (e: Exception) {
+ val error = ErrorMessages[e]
+ val message = Application.get().resources.getString(R.string.import_error, error)
+ Log.e(TAG, message, e)
+ showSnackbar(message)
+ }
+ } else {
+ TunnelImporter.importTunnel(contentResolver, data) { showSnackbar(it) }
+ }
}
}
@@ -64,6 +81,8 @@ class TunnelListFragment : BaseFragment() {
}
}
+ private val snackbarUpdateShower = SnackbarUpdateShower(this)
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (savedInstanceState != null) {
@@ -74,33 +93,45 @@ class TunnelListFragment : BaseFragment() {
}
}
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?): View? {
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
super.onCreateView(inflater, container, savedInstanceState)
binding = TunnelListFragmentBinding.inflate(inflater, container, false)
val bottomSheet = AddTunnelsSheet()
binding?.apply {
createFab.setOnClickListener {
+ if (childFragmentManager.findFragmentByTag("BOTTOM_SHEET") != null)
+ return@setOnClickListener
childFragmentManager.setFragmentResultListener(AddTunnelsSheet.REQUEST_KEY_NEW_TUNNEL, viewLifecycleOwner) { _, bundle ->
when (bundle.getString(AddTunnelsSheet.REQUEST_METHOD)) {
AddTunnelsSheet.REQUEST_CREATE -> {
startActivity(Intent(requireActivity(), TunnelCreatorActivity::class.java))
}
+
AddTunnelsSheet.REQUEST_IMPORT -> {
tunnelFileImportResultLauncher.launch("*/*")
}
+
AddTunnelsSheet.REQUEST_SCAN -> {
- qrImportResultLauncher.launch(ScanOptions()
+ qrImportResultLauncher.launch(
+ ScanOptions()
.setOrientationLocked(false)
.setBeepEnabled(false)
- .setPrompt(getString(R.string.qr_code_hint)))
+ .setPrompt(getString(R.string.qr_code_hint))
+ )
}
}
}
- bottomSheet.show(childFragmentManager, "BOTTOM_SHEET")
+ bottomSheet.showNow(childFragmentManager, "BOTTOM_SHEET")
}
executePendingBindings()
+ snackbarUpdateShower.attach(mainContainer, createFab)
}
+ backPressedCallback = requireActivity().onBackPressedDispatcher.addCallback(this) { actionMode?.finish() }
+ backPressedCallback?.isEnabled = false
+
return binding?.root
}
@@ -167,8 +198,8 @@ class TunnelListFragment : BaseFragment() {
val binding = binding
if (binding != null)
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG)
- .setAnchorView(binding.createFab)
- .show()
+ .setAnchorView(binding.createFab)
+ .show()
else
Toast.makeText(activity ?: Application.get(), message, Toast.LENGTH_SHORT).show()
}
@@ -210,6 +241,7 @@ class TunnelListFragment : BaseFragment() {
mode.finish()
true
}
+
R.id.menu_action_select_all -> {
lifecycleScope.launch {
val tunnels = Application.getTunnelManager().getTunnels()
@@ -219,12 +251,14 @@ class TunnelListFragment : BaseFragment() {
}
true
}
+
else -> false
}
}
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
actionMode = mode
+ backPressedCallback?.isEnabled = true
if (activity != null) {
resources = activity!!.resources
}
@@ -236,6 +270,7 @@ class TunnelListFragment : BaseFragment() {
override fun onDestroyActionMode(mode: ActionMode) {
actionMode = null
+ backPressedCallback?.isEnabled = false
resources = null
animateFab(binding?.createFab, true)
checkedItems.clear()
@@ -255,7 +290,7 @@ class TunnelListFragment : BaseFragment() {
}
val adapter = if (binding == null) null else binding!!.tunnelList.adapter
if (actionMode == null && !checkedItems.isEmpty() && activity != null) {
- (activity as AppCompatActivity?)!!.startSupportActionMode(this)
+ (activity as AppCompatActivity).startSupportActionMode(this)
} else if (actionMode != null && checkedItems.isEmpty()) {
actionMode!!.finish()
}
@@ -282,7 +317,7 @@ class TunnelListFragment : BaseFragment() {
private fun animateFab(view: View?, show: Boolean) {
view ?: return
val animation = AnimationUtils.loadAnimation(
- context, if (show) R.anim.scale_up else R.anim.scale_down
+ context, if (show) R.anim.scale_up else R.anim.scale_down
)
animation.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationRepeat(animation: Animation?) {
diff --git a/ui/src/main/java/com/wireguard/android/model/ApplicationData.kt b/ui/src/main/java/com/wireguard/android/model/ApplicationData.kt
index c4cb168b..f3892424 100644
--- a/ui/src/main/java/com/wireguard/android/model/ApplicationData.kt
+++ b/ui/src/main/java/com/wireguard/android/model/ApplicationData.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.model
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 252e8759..aa237aee 100644
--- a/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt
+++ b/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.model
@@ -21,10 +21,10 @@ import kotlinx.coroutines.withContext
* Encapsulates the volatile and nonvolatile state of a WireGuard tunnel.
*/
class ObservableTunnel internal constructor(
- private val manager: TunnelManager,
- private var name: String,
- config: Config?,
- state: Tunnel.State
+ private val manager: TunnelManager,
+ private var name: String,
+ config: Config?,
+ state: Tunnel.State
) : BaseObservable(), Keyed<String>, Tunnel {
override val key
get() = name
diff --git a/ui/src/main/java/com/wireguard/android/model/TunnelComparator.kt b/ui/src/main/java/com/wireguard/android/model/TunnelComparator.kt
index 0baa44e8..e6b46a54 100644
--- a/ui/src/main/java/com/wireguard/android/model/TunnelComparator.kt
+++ b/ui/src/main/java/com/wireguard/android/model/TunnelComparator.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
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 ec796164..ba873fa6 100644
--- a/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt
+++ b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.model
@@ -140,7 +140,8 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
if (previouslyRunning.isEmpty()) return
withContext(Dispatchers.IO) {
try {
- tunnelMap.filter { previouslyRunning.contains(it.name) }.map { async(Dispatchers.IO + SupervisorJob()) { setTunnelState(it, Tunnel.State.UP) } }.awaitAll()
+ tunnelMap.filter { previouslyRunning.contains(it.name) }.map { async(Dispatchers.IO + SupervisorJob()) { setTunnelState(it, Tunnel.State.UP) } }
+ .awaitAll()
} catch (e: Throwable) {
Log.e(TAG, Log.getStackTraceString(e))
}
diff --git a/ui/src/main/java/com/wireguard/android/preference/DonatePreference.kt b/ui/src/main/java/com/wireguard/android/preference/DonatePreference.kt
new file mode 100644
index 00000000..16920923
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/preference/DonatePreference.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.preference
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.util.AttributeSet
+import android.widget.Toast
+import androidx.preference.Preference
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.wireguard.android.R
+import com.wireguard.android.updater.Updater
+import com.wireguard.android.util.ErrorMessages
+
+class DonatePreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
+ override fun getSummary() = context.getString(R.string.donate_summary)
+
+ override fun getTitle() = context.getString(R.string.donate_title)
+
+ override fun onClick() {
+ /* Google Play Store forbids links to our donation page. */
+ if (Updater.installerIsGooglePlay(context)) {
+ MaterialAlertDialogBuilder(context)
+ .setTitle(R.string.donate_title)
+ .setMessage(R.string.donate_google_play_disappointment)
+ .show()
+ return
+ }
+
+ val intent = Intent(Intent.ACTION_VIEW)
+ intent.data = Uri.parse("https://www.wireguard.com/donations/")
+ try {
+ context.startActivity(intent)
+ } catch (e: Throwable) {
+ Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_SHORT).show()
+ }
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/preference/KernelModuleEnablerPreference.kt b/ui/src/main/java/com/wireguard/android/preference/KernelModuleEnablerPreference.kt
index 15f1dcec..20de8e93 100644
--- a/ui/src/main/java/com/wireguard/android/preference/KernelModuleEnablerPreference.kt
+++ b/ui/src/main/java/com/wireguard/android/preference/KernelModuleEnablerPreference.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference
diff --git a/ui/src/main/java/com/wireguard/android/preference/PreferencesPreferenceDataStore.kt b/ui/src/main/java/com/wireguard/android/preference/PreferencesPreferenceDataStore.kt
index 3f1a7223..1a491684 100644
--- a/ui/src/main/java/com/wireguard/android/preference/PreferencesPreferenceDataStore.kt
+++ b/ui/src/main/java/com/wireguard/android/preference/PreferencesPreferenceDataStore.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
diff --git a/ui/src/main/java/com/wireguard/android/preference/QuickTilePreference.kt b/ui/src/main/java/com/wireguard/android/preference/QuickTilePreference.kt
new file mode 100644
index 00000000..9081818b
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/preference/QuickTilePreference.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.preference
+
+import android.app.StatusBarManager
+import android.content.ComponentName
+import android.content.Context
+import android.graphics.drawable.Icon
+import android.os.Build
+import android.util.AttributeSet
+import android.widget.Toast
+import androidx.annotation.RequiresApi
+import androidx.preference.Preference
+import com.wireguard.android.QuickTileService
+import com.wireguard.android.R
+
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+class QuickTilePreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
+ override fun getSummary() = context.getString(R.string.quick_settings_tile_add_summary)
+
+ override fun getTitle() = context.getString(R.string.quick_settings_tile_add_title)
+
+ override fun onClick() {
+ val statusBarManager = context.getSystemService(StatusBarManager::class.java)
+ statusBarManager.requestAddTileService(
+ ComponentName(context, QuickTileService::class.java),
+ context.getString(R.string.quick_settings_tile_action),
+ Icon.createWithResource(context, R.drawable.ic_tile),
+ context.mainExecutor
+ ) {
+ when (it) {
+ StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED,
+ StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED -> {
+ parent?.removePreference(this)
+ --preferenceManager.preferenceScreen.initialExpandedChildrenCount
+ }
+ StatusBarManager.TILE_ADD_REQUEST_ERROR_MISMATCHED_PACKAGE,
+ StatusBarManager.TILE_ADD_REQUEST_ERROR_REQUEST_IN_PROGRESS,
+ StatusBarManager.TILE_ADD_REQUEST_ERROR_BAD_COMPONENT,
+ StatusBarManager.TILE_ADD_REQUEST_ERROR_NOT_CURRENT_USER,
+ StatusBarManager.TILE_ADD_REQUEST_ERROR_APP_NOT_IN_FOREGROUND,
+ StatusBarManager.TILE_ADD_REQUEST_ERROR_NO_STATUS_BAR_SERVICE ->
+ Toast.makeText(context, context.getString(R.string.quick_settings_tile_add_failure, it), Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.kt b/ui/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.kt
index 7c6283ba..dac80e88 100644
--- a/ui/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.kt
+++ b/ui/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference
diff --git a/ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt b/ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt
index 27065d28..7d997e8a 100644
--- a/ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt
+++ b/ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt
@@ -1,14 +1,14 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference
-import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.AttributeSet
+import android.widget.Toast
import androidx.preference.Preference
import com.wireguard.android.Application
import com.wireguard.android.BuildConfig
@@ -16,6 +16,7 @@ import com.wireguard.android.R
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.GoBackend
import com.wireguard.android.backend.WgQuickBackend
+import com.wireguard.android.util.ErrorMessages
import com.wireguard.android.util.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -33,7 +34,8 @@ class VersionPreference(context: Context, attrs: AttributeSet?) : Preference(con
intent.data = Uri.parse("https://www.wireguard.com/")
try {
context.startActivity(intent)
- } catch (_: ActivityNotFoundException) {
+ } catch (e: Throwable) {
+ Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_SHORT).show()
}
}
diff --git a/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt b/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt
index 27d69538..220796e0 100644
--- a/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt
+++ b/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.preference
@@ -70,14 +70,16 @@ class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference
val message = context.getString(R.string.zip_export_error, error)
Log.e(TAG, message, e)
Snackbar.make(
- activity.findViewById(android.R.id.content),
- message, Snackbar.LENGTH_LONG).show()
+ activity.findViewById(android.R.id.content),
+ message, Snackbar.LENGTH_LONG
+ ).show()
isEnabled = true
}
}
}
- override fun getSummary() = if (exportedFilePath == null) context.getString(R.string.zip_export_summary) else context.getString(R.string.zip_export_success, exportedFilePath)
+ override fun getSummary() =
+ if (exportedFilePath == null) context.getString(R.string.zip_export_summary) else context.getString(R.string.zip_export_success, exportedFilePath)
override fun getTitle() = context.getString(R.string.zip_export_title)
@@ -91,13 +93,16 @@ class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference
isEnabled = false
exportZip()
}
+
is BiometricAuthenticator.Result.Failure -> {
Snackbar.make(
- activity.findViewById(android.R.id.content),
- it.message,
- Snackbar.LENGTH_SHORT
+ activity.findViewById(android.R.id.content),
+ it.message,
+ Snackbar.LENGTH_SHORT
).show()
}
+
+ is BiometricAuthenticator.Result.Cancelled -> {}
}
}
}
diff --git a/ui/src/main/java/com/wireguard/android/updater/Ed25519.java b/ui/src/main/java/com/wireguard/android/updater/Ed25519.java
new file mode 100644
index 00000000..d27caf1c
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/updater/Ed25519.java
@@ -0,0 +1,2507 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.updater;
+
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.util.Arrays;
+
+/**
+ * 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:
+ *
+ * <pre>
+ * -x^2 + y^2 = 1 + (-121665 / 121666 mod 2^255-19)*x^2*y^2
+ * </pre>
+ *
+ * @see <a href="https://eprint.iacr.org/2008/013.pdf">Bernstein D.J., Birkner P., Joye M., Lange
+ * T., Peters C. (2008) Twisted Edwards Curves</a>
+ * @see <a href="https://eprint.iacr.org/2008/522.pdf">Hisil H., Wong K.KH., Carter G., Dawson E.
+ * (2008) Twisted Edwards Curves Revisited</a>
+ */
+final class Ed25519 {
+
+ // d = -121665 / 121666 mod 2^255-19
+ private static final long[] D;
+ // 2d
+ private static final long[] D2;
+ // 2^((p-1)/4) mod p where p = 2^255-19
+ private static final long[] SQRTM1;
+
+ /**
+ * Base point for the Edwards twisted curve = (x, 4/5) and its exponentiations. B_TABLE[i][j] =
+ * (j+1)*256^i*B for i in [0, 32) and j in [0, 8). Base point B = B_TABLE[0][0]
+ */
+ private static final CachedXYT[][] B_TABLE;
+ private static final CachedXYT[] B2;
+
+ private static final BigInteger P_BI =
+ BigInteger.valueOf(2).pow(255).subtract(BigInteger.valueOf(19));
+ private static final BigInteger D_BI =
+ BigInteger.valueOf(-121665).multiply(BigInteger.valueOf(121666).modInverse(P_BI)).mod(P_BI);
+ private static final BigInteger D2_BI = BigInteger.valueOf(2).multiply(D_BI).mod(P_BI);
+ private static final BigInteger SQRTM1_BI =
+ BigInteger.valueOf(2).modPow(P_BI.subtract(BigInteger.ONE).divide(BigInteger.valueOf(4)), P_BI);
+
+ private Ed25519() {
+ }
+
+ private static class Point {
+ private BigInteger x;
+ private BigInteger y;
+ }
+
+ private static BigInteger recoverX(BigInteger y) {
+ // x^2 = (y^2 - 1) / (d * y^2 + 1) mod 2^255-19
+ BigInteger xx =
+ y.pow(2)
+ .subtract(BigInteger.ONE)
+ .multiply(D_BI.multiply(y.pow(2)).add(BigInteger.ONE).modInverse(P_BI));
+ BigInteger x = xx.modPow(P_BI.add(BigInteger.valueOf(3)).divide(BigInteger.valueOf(8)), P_BI);
+ if (!x.pow(2).subtract(xx).mod(P_BI).equals(BigInteger.ZERO)) {
+ x = x.multiply(SQRTM1_BI).mod(P_BI);
+ }
+ if (x.testBit(0)) {
+ x = P_BI.subtract(x);
+ }
+ return x;
+ }
+
+ private static Point edwards(Point a, Point b) {
+ Point o = new Point();
+ BigInteger xxyy = D_BI.multiply(a.x.multiply(b.x).multiply(a.y).multiply(b.y)).mod(P_BI);
+ o.x =
+ (a.x.multiply(b.y).add(b.x.multiply(a.y)))
+ .multiply(BigInteger.ONE.add(xxyy).modInverse(P_BI))
+ .mod(P_BI);
+ o.y =
+ (a.y.multiply(b.y).add(a.x.multiply(b.x)))
+ .multiply(BigInteger.ONE.subtract(xxyy).modInverse(P_BI))
+ .mod(P_BI);
+ return o;
+ }
+
+ private static byte[] toLittleEndian(BigInteger n) {
+ byte[] b = new byte[32];
+ byte[] nBytes = n.toByteArray();
+ System.arraycopy(nBytes, 0, b, 32 - nBytes.length, nBytes.length);
+ for (int i = 0; i < b.length / 2; i++) {
+ byte t = b[i];
+ b[i] = b[b.length - i - 1];
+ b[b.length - i - 1] = t;
+ }
+ return b;
+ }
+
+ private static CachedXYT getCachedXYT(Point p) {
+ return new CachedXYT(
+ Field25519.expand(toLittleEndian(p.y.add(p.x).mod(P_BI))),
+ Field25519.expand(toLittleEndian(p.y.subtract(p.x).mod(P_BI))),
+ Field25519.expand(toLittleEndian(D2_BI.multiply(p.x).multiply(p.y).mod(P_BI))));
+ }
+
+ static {
+ Point b = new Point();
+ b.y = BigInteger.valueOf(4).multiply(BigInteger.valueOf(5).modInverse(P_BI)).mod(P_BI);
+ b.x = recoverX(b.y);
+
+ D = Field25519.expand(toLittleEndian(D_BI));
+ D2 = Field25519.expand(toLittleEndian(D2_BI));
+ SQRTM1 = Field25519.expand(toLittleEndian(SQRTM1_BI));
+
+ Point bi = b;
+ B_TABLE = new CachedXYT[32][8];
+ for (int i = 0; i < 32; i++) {
+ Point bij = bi;
+ for (int j = 0; j < 8; j++) {
+ B_TABLE[i][j] = getCachedXYT(bij);
+ bij = edwards(bij, bi);
+ }
+ for (int j = 0; j < 8; j++) {
+ bi = edwards(bi, bi);
+ }
+ }
+ bi = b;
+ Point b2 = edwards(b, b);
+ B2 = new CachedXYT[8];
+ for (int i = 0; i < 8; i++) {
+ B2[i] = getCachedXYT(bi);
+ bi = edwards(bi, b2);
+ }
+ }
+
+ private static final int PUBLIC_KEY_LEN = Field25519.FIELD_LEN;
+ private static final int SIGNATURE_LEN = Field25519.FIELD_LEN * 2;
+
+ /**
+ * Defines field 25519 function based on <a
+ * href="https://github.com/agl/curve25519-donna/blob/master/curve25519-donna.c">curve25519-donna C
+ * implementation</a> (mostly identical).
+ *
+ * <p>Field elements are written as an array of signed, 64-bit limbs (an array of longs), least
+ * significant first. The value of the field element is:
+ *
+ * <pre>
+ * x[0] + 2^26·x[1] + 2^51·x[2] + 2^77·x[3] + 2^102·x[4] + 2^128·x[5] + 2^153·x[6] + 2^179·x[7] +
+ * 2^204·x[8] + 2^230·x[9],
+ * </pre>
+ *
+ * <p>i.e. the limbs are 26, 25, 26, 25, ... bits wide.
+ */
+ private static final class Field25519 {
+ /**
+ * During Field25519 computation, the mixed radix representation may be in different forms:
+ * <ul>
+ * <li> Reduced-size form: the array has size at most 10.
+ * <li> Non-reduced-size form: the array is not reduced modulo 2^255 - 19 and has size at most
+ * 19.
+ * </ul>
+ * <p>
+ * TODO(quannguyen):
+ * <ul>
+ * <li> Clarify ill-defined terminologies.
+ * <li> The reduction procedure is different from DJB's paper
+ * (http://cr.yp.to/ecdh/curve25519-20060209.pdf). The coefficients after reducing degree and
+ * reducing coefficients aren't guaranteed to be in range {-2^25, ..., 2^25}. We should check to
+ * see what's going on.
+ * <li> Consider using method mult() everywhere and making product() private.
+ * </ul>
+ */
+
+ static final int FIELD_LEN = 32;
+ static final int LIMB_CNT = 10;
+ private static final long TWO_TO_25 = 1 << 25;
+ private static final long TWO_TO_26 = TWO_TO_25 << 1;
+
+ private static final int[] EXPAND_START = {0, 3, 6, 9, 12, 16, 19, 22, 25, 28};
+ private static final int[] EXPAND_SHIFT = {0, 2, 3, 5, 6, 0, 1, 3, 4, 6};
+ private static final int[] MASK = {0x3ffffff, 0x1ffffff};
+ private static final int[] SHIFT = {26, 25};
+
+ /**
+ * Sums two numbers: output = in1 + in2
+ * <p>
+ * On entry: in1, in2 are in reduced-size form.
+ */
+ static void sum(long[] output, long[] in1, long[] in2) {
+ for (int i = 0; i < LIMB_CNT; i++) {
+ output[i] = in1[i] + in2[i];
+ }
+ }
+
+ /**
+ * Sums two numbers: output += in
+ * <p>
+ * On entry: in is in reduced-size form.
+ */
+ static void sum(long[] output, long[] in) {
+ sum(output, output, in);
+ }
+
+ /**
+ * Find the difference of two numbers: output = in1 - in2
+ * (note the order of the arguments!).
+ * <p>
+ * On entry: in1, in2 are in reduced-size form.
+ */
+ static void sub(long[] output, long[] in1, long[] in2) {
+ for (int i = 0; i < LIMB_CNT; i++) {
+ output[i] = in1[i] - in2[i];
+ }
+ }
+
+ /**
+ * Find the difference of two numbers: output = in - output
+ * (note the order of the arguments!).
+ * <p>
+ * On entry: in, output are in reduced-size form.
+ */
+ static void sub(long[] output, long[] in) {
+ sub(output, in, output);
+ }
+
+ /**
+ * Multiply a number by a scalar: output = in * scalar
+ */
+ static void scalarProduct(long[] output, long[] in, long scalar) {
+ for (int i = 0; i < LIMB_CNT; i++) {
+ output[i] = in[i] * scalar;
+ }
+ }
+
+ /**
+ * Multiply two numbers: out = in2 * in
+ * <p>
+ * output must be distinct to both inputs. The inputs are reduced coefficient form,
+ * the output is not.
+ * <p>
+ * out[x] <= 14 * the largest product of the input limbs.
+ */
+ static void product(long[] out, long[] in2, long[] in) {
+ out[0] = in2[0] * in[0];
+ out[1] = in2[0] * in[1]
+ + in2[1] * in[0];
+ out[2] = 2 * in2[1] * in[1]
+ + in2[0] * in[2]
+ + in2[2] * in[0];
+ out[3] = in2[1] * in[2]
+ + in2[2] * in[1]
+ + in2[0] * in[3]
+ + in2[3] * in[0];
+ out[4] = in2[2] * in[2]
+ + 2 * (in2[1] * in[3] + in2[3] * in[1])
+ + in2[0] * in[4]
+ + in2[4] * in[0];
+ out[5] = in2[2] * in[3]
+ + in2[3] * in[2]
+ + in2[1] * in[4]
+ + in2[4] * in[1]
+ + in2[0] * in[5]
+ + in2[5] * in[0];
+ out[6] = 2 * (in2[3] * in[3] + in2[1] * in[5] + in2[5] * in[1])
+ + in2[2] * in[4]
+ + in2[4] * in[2]
+ + in2[0] * in[6]
+ + in2[6] * in[0];
+ out[7] = in2[3] * in[4]
+ + in2[4] * in[3]
+ + in2[2] * in[5]
+ + in2[5] * in[2]
+ + in2[1] * in[6]
+ + in2[6] * in[1]
+ + in2[0] * in[7]
+ + in2[7] * in[0];
+ out[8] = in2[4] * in[4]
+ + 2 * (in2[3] * in[5] + in2[5] * in[3] + in2[1] * in[7] + in2[7] * in[1])
+ + in2[2] * in[6]
+ + in2[6] * in[2]
+ + in2[0] * in[8]
+ + in2[8] * in[0];
+ out[9] = in2[4] * in[5]
+ + in2[5] * in[4]
+ + in2[3] * in[6]
+ + in2[6] * in[3]
+ + in2[2] * in[7]
+ + in2[7] * in[2]
+ + in2[1] * in[8]
+ + in2[8] * in[1]
+ + in2[0] * in[9]
+ + in2[9] * in[0];
+ out[10] =
+ 2 * (in2[5] * in[5] + in2[3] * in[7] + in2[7] * in[3] + in2[1] * in[9] + in2[9] * in[1])
+ + in2[4] * in[6]
+ + in2[6] * in[4]
+ + in2[2] * in[8]
+ + in2[8] * in[2];
+ out[11] = in2[5] * in[6]
+ + in2[6] * in[5]
+ + in2[4] * in[7]
+ + in2[7] * in[4]
+ + in2[3] * in[8]
+ + in2[8] * in[3]
+ + in2[2] * in[9]
+ + in2[9] * in[2];
+ out[12] = in2[6] * in[6]
+ + 2 * (in2[5] * in[7] + in2[7] * in[5] + in2[3] * in[9] + in2[9] * in[3])
+ + in2[4] * in[8]
+ + in2[8] * in[4];
+ out[13] = in2[6] * in[7]
+ + in2[7] * in[6]
+ + in2[5] * in[8]
+ + in2[8] * in[5]
+ + in2[4] * in[9]
+ + in2[9] * in[4];
+ out[14] = 2 * (in2[7] * in[7] + in2[5] * in[9] + in2[9] * in[5])
+ + in2[6] * in[8]
+ + in2[8] * in[6];
+ out[15] = in2[7] * in[8]
+ + in2[8] * in[7]
+ + in2[6] * in[9]
+ + in2[9] * in[6];
+ out[16] = in2[8] * in[8]
+ + 2 * (in2[7] * in[9] + in2[9] * in[7]);
+ out[17] = in2[8] * in[9]
+ + in2[9] * in[8];
+ out[18] = 2 * in2[9] * in[9];
+ }
+
+ /**
+ * Reduce a field element by calling reduceSizeByModularReduction and reduceCoefficients.
+ *
+ * @param input An input array of any length. If the array has 19 elements, it will be used as
+ * temporary buffer and its contents changed.
+ * @param output An output array of size LIMB_CNT. After the call |output[i]| < 2^26 will hold.
+ */
+ static void reduce(long[] input, long[] output) {
+ long[] tmp;
+ if (input.length == 19) {
+ tmp = input;
+ } else {
+ tmp = new long[19];
+ System.arraycopy(input, 0, tmp, 0, input.length);
+ }
+ reduceSizeByModularReduction(tmp);
+ reduceCoefficients(tmp);
+ System.arraycopy(tmp, 0, output, 0, LIMB_CNT);
+ }
+
+ /**
+ * Reduce a long form to a reduced-size form by taking the input mod 2^255 - 19.
+ * <p>
+ * On entry: |output[i]| < 14*2^54
+ * On exit: |output[0..8]| < 280*2^54
+ */
+ static void reduceSizeByModularReduction(long[] output) {
+ // The coefficients x[10], x[11],..., x[18] are eliminated by reduction modulo 2^255 - 19.
+ // For example, the coefficient x[18] is multiplied by 19 and added to the coefficient x[8].
+ //
+ // Each of these shifts and adds ends up multiplying the value by 19.
+ //
+ // For output[0..8], the absolute entry value is < 14*2^54 and we add, at most, 19*14*2^54 thus,
+ // on exit, |output[0..8]| < 280*2^54.
+ output[8] += output[18] << 4;
+ output[8] += output[18] << 1;
+ output[8] += output[18];
+ output[7] += output[17] << 4;
+ output[7] += output[17] << 1;
+ output[7] += output[17];
+ output[6] += output[16] << 4;
+ output[6] += output[16] << 1;
+ output[6] += output[16];
+ output[5] += output[15] << 4;
+ output[5] += output[15] << 1;
+ output[5] += output[15];
+ output[4] += output[14] << 4;
+ output[4] += output[14] << 1;
+ output[4] += output[14];
+ output[3] += output[13] << 4;
+ output[3] += output[13] << 1;
+ output[3] += output[13];
+ output[2] += output[12] << 4;
+ output[2] += output[12] << 1;
+ output[2] += output[12];
+ output[1] += output[11] << 4;
+ output[1] += output[11] << 1;
+ output[1] += output[11];
+ output[0] += output[10] << 4;
+ output[0] += output[10] << 1;
+ output[0] += output[10];
+ }
+
+ /**
+ * Reduce all coefficients of the short form input so that |x| < 2^26.
+ * <p>
+ * On entry: |output[i]| < 280*2^54
+ */
+ static void reduceCoefficients(long[] output) {
+ output[10] = 0;
+
+ for (int i = 0; i < LIMB_CNT; i += 2) {
+ long over = output[i] / TWO_TO_26;
+ // The entry condition (that |output[i]| < 280*2^54) means that over is, at most, 280*2^28 in
+ // the first iteration of this loop. This is added to the next limb and we can approximate the
+ // resulting bound of that limb by 281*2^54.
+ output[i] -= over << 26;
+ output[i + 1] += over;
+
+ // For the first iteration, |output[i+1]| < 281*2^54, thus |over| < 281*2^29. When this is
+ // added to the next limb, the resulting bound can be approximated as 281*2^54.
+ //
+ // For subsequent iterations of the loop, 281*2^54 remains a conservative bound and no
+ // overflow occurs.
+ over = output[i + 1] / TWO_TO_25;
+ output[i + 1] -= over << 25;
+ output[i + 2] += over;
+ }
+ // Now |output[10]| < 281*2^29 and all other coefficients are reduced.
+ output[0] += output[10] << 4;
+ output[0] += output[10] << 1;
+ output[0] += output[10];
+
+ output[10] = 0;
+ // Now output[1..9] are reduced, and |output[0]| < 2^26 + 19*281*2^29 so |over| will be no more
+ // than 2^16.
+ long over = output[0] / TWO_TO_26;
+ output[0] -= over << 26;
+ output[1] += over;
+ // Now output[0,2..9] are reduced, and |output[1]| < 2^25 + 2^16 < 2^26. The bound on
+ // |output[1]| is sufficient to meet our needs.
+ }
+
+ /**
+ * A helpful wrapper around {@ref Field25519#product}: output = in * in2.
+ * <p>
+ * On entry: |in[i]| < 2^27 and |in2[i]| < 2^27.
+ * <p>
+ * The output is reduced degree (indeed, one need only provide storage for 10 limbs) and
+ * |output[i]| < 2^26.
+ */
+ static void mult(long[] output, long[] in, long[] in2) {
+ long[] t = new long[19];
+ product(t, in, in2);
+ // |t[i]| < 2^26
+ reduce(t, output);
+ }
+
+ /**
+ * Square a number: out = in**2
+ * <p>
+ * output must be distinct from the input. The inputs are reduced coefficient form, the output is
+ * not.
+ * <p>
+ * out[x] <= 14 * the largest product of the input limbs.
+ */
+ private static void squareInner(long[] out, long[] in) {
+ out[0] = in[0] * in[0];
+ out[1] = 2 * in[0] * in[1];
+ out[2] = 2 * (in[1] * in[1] + in[0] * in[2]);
+ out[3] = 2 * (in[1] * in[2] + in[0] * in[3]);
+ out[4] = in[2] * in[2]
+ + 4 * in[1] * in[3]
+ + 2 * in[0] * in[4];
+ out[5] = 2 * (in[2] * in[3] + in[1] * in[4] + in[0] * in[5]);
+ out[6] = 2 * (in[3] * in[3] + in[2] * in[4] + in[0] * in[6] + 2 * in[1] * in[5]);
+ out[7] = 2 * (in[3] * in[4] + in[2] * in[5] + in[1] * in[6] + in[0] * in[7]);
+ out[8] = in[4] * in[4]
+ + 2 * (in[2] * in[6] + in[0] * in[8] + 2 * (in[1] * in[7] + in[3] * in[5]));
+ out[9] = 2 * (in[4] * in[5] + in[3] * in[6] + in[2] * in[7] + in[1] * in[8] + in[0] * in[9]);
+ out[10] = 2 * (in[5] * in[5]
+ + in[4] * in[6]
+ + in[2] * in[8]
+ + 2 * (in[3] * in[7] + in[1] * in[9]));
+ out[11] = 2 * (in[5] * in[6] + in[4] * in[7] + in[3] * in[8] + in[2] * in[9]);
+ out[12] = in[6] * in[6]
+ + 2 * (in[4] * in[8] + 2 * (in[5] * in[7] + in[3] * in[9]));
+ out[13] = 2 * (in[6] * in[7] + in[5] * in[8] + in[4] * in[9]);
+ out[14] = 2 * (in[7] * in[7] + in[6] * in[8] + 2 * in[5] * in[9]);
+ out[15] = 2 * (in[7] * in[8] + in[6] * in[9]);
+ out[16] = in[8] * in[8] + 4 * in[7] * in[9];
+ out[17] = 2 * in[8] * in[9];
+ out[18] = 2 * in[9] * in[9];
+ }
+
+ /**
+ * Returns in^2.
+ * <p>
+ * On entry: The |in| argument is in reduced coefficients form and |in[i]| < 2^27.
+ * <p>
+ * On exit: The |output| argument is in reduced coefficients form (indeed, one need only provide
+ * storage for 10 limbs) and |out[i]| < 2^26.
+ */
+ static void square(long[] output, long[] in) {
+ long[] t = new long[19];
+ squareInner(t, in);
+ // |t[i]| < 14*2^54 because the largest product of two limbs will be < 2^(27+27) and SquareInner
+ // adds together, at most, 14 of those products.
+ reduce(t, output);
+ }
+
+ /**
+ * Takes a little-endian, 32-byte number and expands it into mixed radix form.
+ */
+ static long[] expand(byte[] input) {
+ long[] output = new long[LIMB_CNT];
+ for (int i = 0; i < LIMB_CNT; i++) {
+ output[i] = ((((long) (input[EXPAND_START[i]] & 0xff))
+ | ((long) (input[EXPAND_START[i] + 1] & 0xff)) << 8
+ | ((long) (input[EXPAND_START[i] + 2] & 0xff)) << 16
+ | ((long) (input[EXPAND_START[i] + 3] & 0xff)) << 24) >> EXPAND_SHIFT[i]) & MASK[i & 1];
+ }
+ return output;
+ }
+
+ /**
+ * Takes a fully reduced mixed radix form number and contract it into a little-endian, 32-byte
+ * array.
+ * <p>
+ * On entry: |input_limbs[i]| < 2^26
+ */
+ @SuppressWarnings("NarrowingCompoundAssignment")
+ static byte[] contract(long[] inputLimbs) {
+ long[] input = Arrays.copyOf(inputLimbs, LIMB_CNT);
+ for (int j = 0; j < 2; j++) {
+ for (int i = 0; i < 9; i++) {
+ // This calculation is a time-invariant way to make input[i] non-negative by borrowing
+ // from the next-larger limb.
+ int carry = -(int) ((input[i] & (input[i] >> 31)) >> SHIFT[i & 1]);
+ input[i] = input[i] + (carry << SHIFT[i & 1]);
+ input[i + 1] -= carry;
+ }
+
+ // There's no greater limb for input[9] to borrow from, but we can multiply by 19 and borrow
+ // from input[0], which is valid mod 2^255-19.
+ {
+ int carry = -(int) ((input[9] & (input[9] >> 31)) >> 25);
+ input[9] += (carry << 25);
+ input[0] -= (carry * 19);
+ }
+
+ // After the first iteration, input[1..9] are non-negative and fit within 25 or 26 bits,
+ // depending on position. However, input[0] may be negative.
+ }
+
+ // The first borrow-propagation pass above ended with every limb except (possibly) input[0]
+ // non-negative.
+ //
+ // If input[0] was negative after the first pass, then it was because of a carry from input[9].
+ // On entry, input[9] < 2^26 so the carry was, at most, one, since (2**26-1) >> 25 = 1. Thus
+ // input[0] >= -19.
+ //
+ // In the second pass, each limb is decreased by at most one. Thus the second borrow-propagation
+ // pass could only have wrapped around to decrease input[0] again if the first pass left
+ // input[0] negative *and* input[1] through input[9] were all zero. In that case, input[1] is
+ // now 2^25 - 1, and this last borrow-propagation step will leave input[1] non-negative.
+ {
+ int carry = -(int) ((input[0] & (input[0] >> 31)) >> 26);
+ input[0] += (carry << 26);
+ input[1] -= carry;
+ }
+
+ // All input[i] are now non-negative. However, there might be values between 2^25 and 2^26 in a
+ // limb which is, nominally, 25 bits wide.
+ for (int j = 0; j < 2; j++) {
+ for (int i = 0; i < 9; i++) {
+ int carry = (int) (input[i] >> SHIFT[i & 1]);
+ input[i] &= MASK[i & 1];
+ input[i + 1] += carry;
+ }
+ }
+
+ {
+ int carry = (int) (input[9] >> 25);
+ input[9] &= 0x1ffffff;
+ input[0] += 19 * carry;
+ }
+
+ // If the first carry-chain pass, just above, ended up with a carry from input[9], and that
+ // caused input[0] to be out-of-bounds, then input[0] was < 2^26 + 2*19, because the carry was,
+ // at most, two.
+ //
+ // If the second pass carried from input[9] again then input[0] is < 2*19 and the input[9] ->
+ // input[0] carry didn't push input[0] out of bounds.
+
+ // It still remains the case that input might be between 2^255-19 and 2^255. In this case,
+ // input[1..9] must take their maximum value and input[0] must be >= (2^255-19) & 0x3ffffff,
+ // which is 0x3ffffed.
+ int mask = gte((int) input[0], 0x3ffffed);
+ for (int i = 1; i < LIMB_CNT; i++) {
+ mask &= eq((int) input[i], MASK[i & 1]);
+ }
+
+ // mask is either 0xffffffff (if input >= 2^255-19) and zero otherwise. Thus this conditionally
+ // subtracts 2^255-19.
+ input[0] -= mask & 0x3ffffed;
+ input[1] -= mask & 0x1ffffff;
+ for (int i = 2; i < LIMB_CNT; i += 2) {
+ input[i] -= mask & 0x3ffffff;
+ input[i + 1] -= mask & 0x1ffffff;
+ }
+
+ for (int i = 0; i < LIMB_CNT; i++) {
+ input[i] <<= EXPAND_SHIFT[i];
+ }
+ byte[] output = new byte[FIELD_LEN];
+ for (int i = 0; i < LIMB_CNT; i++) {
+ output[EXPAND_START[i]] |= input[i] & 0xff;
+ output[EXPAND_START[i] + 1] |= (input[i] >> 8) & 0xff;
+ output[EXPAND_START[i] + 2] |= (input[i] >> 16) & 0xff;
+ output[EXPAND_START[i] + 3] |= (input[i] >> 24) & 0xff;
+ }
+ return output;
+ }
+
+ /**
+ * Computes inverse of z = z(2^255 - 21)
+ * <p>
+ * Shamelessly copied from agl's code which was shamelessly copied from djb's code. Only the
+ * comment format and the variable namings are different from those.
+ */
+ static void inverse(long[] out, long[] z) {
+ long[] z2 = new long[Field25519.LIMB_CNT];
+ long[] z9 = new long[Field25519.LIMB_CNT];
+ long[] z11 = new long[Field25519.LIMB_CNT];
+ long[] z2To5Minus1 = new long[Field25519.LIMB_CNT];
+ long[] z2To10Minus1 = new long[Field25519.LIMB_CNT];
+ long[] z2To20Minus1 = new long[Field25519.LIMB_CNT];
+ long[] z2To50Minus1 = new long[Field25519.LIMB_CNT];
+ long[] z2To100Minus1 = new long[Field25519.LIMB_CNT];
+ long[] t0 = new long[Field25519.LIMB_CNT];
+ long[] t1 = new long[Field25519.LIMB_CNT];
+
+ square(z2, z); // 2
+ square(t1, z2); // 4
+ square(t0, t1); // 8
+ mult(z9, t0, z); // 9
+ mult(z11, z9, z2); // 11
+ square(t0, z11); // 22
+ mult(z2To5Minus1, t0, z9); // 2^5 - 2^0 = 31
+
+ square(t0, z2To5Minus1); // 2^6 - 2^1
+ square(t1, t0); // 2^7 - 2^2
+ square(t0, t1); // 2^8 - 2^3
+ square(t1, t0); // 2^9 - 2^4
+ square(t0, t1); // 2^10 - 2^5
+ mult(z2To10Minus1, t0, z2To5Minus1); // 2^10 - 2^0
+
+ square(t0, z2To10Minus1); // 2^11 - 2^1
+ square(t1, t0); // 2^12 - 2^2
+ for (int i = 2; i < 10; i += 2) { // 2^20 - 2^10
+ square(t0, t1);
+ square(t1, t0);
+ }
+ mult(z2To20Minus1, t1, z2To10Minus1); // 2^20 - 2^0
+
+ square(t0, z2To20Minus1); // 2^21 - 2^1
+ square(t1, t0); // 2^22 - 2^2
+ for (int i = 2; i < 20; i += 2) { // 2^40 - 2^20
+ square(t0, t1);
+ square(t1, t0);
+ }
+ mult(t0, t1, z2To20Minus1); // 2^40 - 2^0
+
+ square(t1, t0); // 2^41 - 2^1
+ square(t0, t1); // 2^42 - 2^2
+ for (int i = 2; i < 10; i += 2) { // 2^50 - 2^10
+ square(t1, t0);
+ square(t0, t1);
+ }
+ mult(z2To50Minus1, t0, z2To10Minus1); // 2^50 - 2^0
+
+ square(t0, z2To50Minus1); // 2^51 - 2^1
+ square(t1, t0); // 2^52 - 2^2
+ for (int i = 2; i < 50; i += 2) { // 2^100 - 2^50
+ square(t0, t1);
+ square(t1, t0);
+ }
+ mult(z2To100Minus1, t1, z2To50Minus1); // 2^100 - 2^0
+
+ square(t1, z2To100Minus1); // 2^101 - 2^1
+ square(t0, t1); // 2^102 - 2^2
+ for (int i = 2; i < 100; i += 2) { // 2^200 - 2^100
+ square(t1, t0);
+ square(t0, t1);
+ }
+ mult(t1, t0, z2To100Minus1); // 2^200 - 2^0
+
+ square(t0, t1); // 2^201 - 2^1
+ square(t1, t0); // 2^202 - 2^2
+ for (int i = 2; i < 50; i += 2) { // 2^250 - 2^50
+ square(t0, t1);
+ square(t1, t0);
+ }
+ mult(t0, t1, z2To50Minus1); // 2^250 - 2^0
+
+ square(t1, t0); // 2^251 - 2^1
+ square(t0, t1); // 2^252 - 2^2
+ square(t1, t0); // 2^253 - 2^3
+ square(t0, t1); // 2^254 - 2^4
+ square(t1, t0); // 2^255 - 2^5
+ mult(out, t1, z11); // 2^255 - 21
+ }
+
+
+ /**
+ * Returns 0xffffffff iff a == b and zero otherwise.
+ */
+ private static int eq(int a, int b) {
+ a = ~(a ^ b);
+ a &= a << 16;
+ a &= a << 8;
+ a &= a << 4;
+ a &= a << 2;
+ a &= a << 1;
+ return a >> 31;
+ }
+
+ /**
+ * returns 0xffffffff if a >= b and zero otherwise, where a and b are both non-negative.
+ */
+ private static int gte(int a, int b) {
+ a -= b;
+ // a >= 0 iff a >= b.
+ return ~(a >> 31);
+ }
+ }
+
+ // (x = 0, y = 1) point
+ private static final CachedXYT CACHED_NEUTRAL = new CachedXYT(
+ new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ new long[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0});
+ private static final PartialXYZT NEUTRAL = new PartialXYZT(
+ new XYZ(new long[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0}),
+ new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0});
+
+ /**
+ * Projective point representation (X:Y:Z) satisfying x = X/Z, y = Y/Z
+ * <p>
+ * Note that this is referred as ge_p2 in ref10 impl.
+ * Also note that x = X, y = Y and z = Z below following Java coding style.
+ * <p>
+ * See
+ * Koyama K., Tsuruoka Y. (1993) Speeding up Elliptic Cryptosystems by Using a Signed Binary
+ * Window Method.
+ * <p>
+ * https://hyperelliptic.org/EFD/g1p/auto-twisted-projective.html
+ */
+ private static class XYZ {
+
+ final long[] x;
+ final long[] y;
+ final long[] z;
+
+ XYZ() {
+ this(new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT]);
+ }
+
+ XYZ(long[] x, long[] y, long[] z) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
+
+ XYZ(XYZ xyz) {
+ x = Arrays.copyOf(xyz.x, Field25519.LIMB_CNT);
+ y = Arrays.copyOf(xyz.y, Field25519.LIMB_CNT);
+ z = Arrays.copyOf(xyz.z, Field25519.LIMB_CNT);
+ }
+
+ XYZ(PartialXYZT partialXYZT) {
+ this();
+ fromPartialXYZT(this, partialXYZT);
+ }
+
+ /**
+ * ge_p1p1_to_p2.c
+ */
+ static XYZ fromPartialXYZT(XYZ out, PartialXYZT in) {
+ Field25519.mult(out.x, in.xyz.x, in.t);
+ Field25519.mult(out.y, in.xyz.y, in.xyz.z);
+ Field25519.mult(out.z, in.xyz.z, in.t);
+ return out;
+ }
+
+ /**
+ * Encodes this point to bytes.
+ */
+ byte[] toBytes() {
+ long[] recip = new long[Field25519.LIMB_CNT];
+ long[] x = new long[Field25519.LIMB_CNT];
+ long[] y = new long[Field25519.LIMB_CNT];
+ Field25519.inverse(recip, z);
+ Field25519.mult(x, this.x, recip);
+ Field25519.mult(y, this.y, recip);
+ byte[] s = Field25519.contract(y);
+ s[31] = (byte) (s[31] ^ (getLsb(x) << 7));
+ return s;
+ }
+
+
+ /**
+ * Best effort fix-timing array comparison.
+ *
+ * @return true if two arrays are equal.
+ */
+ private static boolean bytesEqual(final byte[] x, final byte[] y) {
+ if (x == null || y == null) {
+ return false;
+ }
+ if (x.length != y.length) {
+ return false;
+ }
+ int res = 0;
+ for (int i = 0; i < x.length; i++) {
+ res |= x[i] ^ y[i];
+ }
+ return res == 0;
+ }
+
+ /**
+ * Checks that the point is on curve
+ */
+ boolean isOnCurve() {
+ long[] x2 = new long[Field25519.LIMB_CNT];
+ Field25519.square(x2, x);
+ long[] y2 = new long[Field25519.LIMB_CNT];
+ Field25519.square(y2, y);
+ long[] z2 = new long[Field25519.LIMB_CNT];
+ Field25519.square(z2, z);
+ long[] z4 = new long[Field25519.LIMB_CNT];
+ Field25519.square(z4, z2);
+ long[] lhs = new long[Field25519.LIMB_CNT];
+ // lhs = y^2 - x^2
+ Field25519.sub(lhs, y2, x2);
+ // lhs = z^2 * (y2 - x2)
+ Field25519.mult(lhs, lhs, z2);
+ long[] rhs = new long[Field25519.LIMB_CNT];
+ // rhs = x^2 * y^2
+ Field25519.mult(rhs, x2, y2);
+ // rhs = D * x^2 * y^2
+ Field25519.mult(rhs, rhs, D);
+ // rhs = z^4 + D * x^2 * y^2
+ Field25519.sum(rhs, z4);
+ // Field25519.mult reduces its output, but Field25519.sum does not, so we have to manually
+ // reduce it here.
+ Field25519.reduce(rhs, rhs);
+ // z^2 (y^2 - x^2) == z^4 + D * x^2 * y^2
+ return bytesEqual(Field25519.contract(lhs), Field25519.contract(rhs));
+ }
+ }
+
+ /**
+ * Represents extended projective point representation (X:Y:Z:T) satisfying x = X/Z, y = Y/Z,
+ * XY = ZT
+ * <p>
+ * Note that this is referred as ge_p3 in ref10 impl.
+ * Also note that t = T below following Java coding style.
+ * <p>
+ * See
+ * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.
+ * <p>
+ * https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html
+ */
+ private static class XYZT {
+
+ final XYZ xyz;
+ final long[] t;
+
+ XYZT() {
+ this(new XYZ(), new long[Field25519.LIMB_CNT]);
+ }
+
+ XYZT(XYZ xyz, long[] t) {
+ this.xyz = xyz;
+ this.t = t;
+ }
+
+ XYZT(PartialXYZT partialXYZT) {
+ this();
+ fromPartialXYZT(this, partialXYZT);
+ }
+
+ /**
+ * ge_p1p1_to_p2.c
+ */
+ private static XYZT fromPartialXYZT(XYZT out, PartialXYZT in) {
+ Field25519.mult(out.xyz.x, in.xyz.x, in.t);
+ Field25519.mult(out.xyz.y, in.xyz.y, in.xyz.z);
+ Field25519.mult(out.xyz.z, in.xyz.z, in.t);
+ Field25519.mult(out.t, in.xyz.x, in.xyz.y);
+ return out;
+ }
+
+ /**
+ * Decodes {@code s} into an extented projective point.
+ * See Section 5.1.3 Decoding in https://tools.ietf.org/html/rfc8032#section-5.1.3
+ */
+ private static XYZT fromBytesNegateVarTime(byte[] s) throws GeneralSecurityException {
+ long[] x = new long[Field25519.LIMB_CNT];
+ long[] y = Field25519.expand(s);
+ long[] z = new long[Field25519.LIMB_CNT];
+ z[0] = 1;
+ long[] t = new long[Field25519.LIMB_CNT];
+ long[] u = new long[Field25519.LIMB_CNT];
+ long[] v = new long[Field25519.LIMB_CNT];
+ long[] vxx = new long[Field25519.LIMB_CNT];
+ long[] check = new long[Field25519.LIMB_CNT];
+ Field25519.square(u, y);
+ Field25519.mult(v, u, D);
+ Field25519.sub(u, u, z); // u = y^2 - 1
+ Field25519.sum(v, v, z); // v = dy^2 + 1
+
+ long[] v3 = new long[Field25519.LIMB_CNT];
+ Field25519.square(v3, v);
+ Field25519.mult(v3, v3, v); // v3 = v^3
+ Field25519.square(x, v3);
+ Field25519.mult(x, x, v);
+ Field25519.mult(x, x, u); // x = uv^7
+
+ pow2252m3(x, x); // x = (uv^7)^((q-5)/8)
+ Field25519.mult(x, x, v3);
+ Field25519.mult(x, x, u); // x = uv^3(uv^7)^((q-5)/8)
+
+ Field25519.square(vxx, x);
+ Field25519.mult(vxx, vxx, v);
+ Field25519.sub(check, vxx, u); // vx^2-u
+ if (isNonZeroVarTime(check)) {
+ Field25519.sum(check, vxx, u); // vx^2+u
+ if (isNonZeroVarTime(check)) {
+ throw new GeneralSecurityException("Cannot convert given bytes to extended projective "
+ + "coordinates. No square root exists for modulo 2^255-19");
+ }
+ Field25519.mult(x, x, SQRTM1);
+ }
+
+ if (!isNonZeroVarTime(x) && (s[31] & 0xff) >> 7 != 0) {
+ throw new GeneralSecurityException("Cannot convert given bytes to extended projective "
+ + "coordinates. Computed x is zero and encoded x's least significant bit is not zero");
+ }
+ if (getLsb(x) == ((s[31] & 0xff) >> 7)) {
+ neg(x, x);
+ }
+
+ Field25519.mult(t, x, y);
+ return new XYZT(new XYZ(x, y, z), t);
+ }
+ }
+
+ /**
+ * Partial projective point representation ((X:Z),(Y:T)) satisfying x=X/Z, y=Y/T
+ * <p>
+ * Note that this is referred as complete form in the original ref10 impl (ge_p1p1).
+ * Also note that t = T below following Java coding style.
+ * <p>
+ * Although this has the same types as XYZT, it is redefined to have its own type so that it is
+ * readable and 1:1 corresponds to ref10 impl.
+ * <p>
+ * Can be converted to XYZT as follows:
+ * X1 = X * T = x * Z * T = x * Z1
+ * Y1 = Y * Z = y * T * Z = y * Z1
+ * Z1 = Z * T = Z * T
+ * T1 = X * Y = x * Z * y * T = x * y * Z1 = X1Y1 / Z1
+ */
+ private static class PartialXYZT {
+
+ final XYZ xyz;
+ final long[] t;
+
+ PartialXYZT() {
+ this(new XYZ(), new long[Field25519.LIMB_CNT]);
+ }
+
+ PartialXYZT(XYZ xyz, long[] t) {
+ this.xyz = xyz;
+ this.t = t;
+ }
+
+ PartialXYZT(PartialXYZT other) {
+ xyz = new XYZ(other.xyz);
+ t = Arrays.copyOf(other.t, Field25519.LIMB_CNT);
+ }
+ }
+
+ /**
+ * Corresponds to the caching mentioned in the last paragraph of Section 3.1 of
+ * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.
+ * with Z = 1.
+ */
+ private static class CachedXYT {
+
+ final long[] yPlusX;
+ final long[] yMinusX;
+ final long[] t2d;
+
+ /**
+ * Creates a cached XYZT with Z = 1
+ *
+ * @param yPlusX y + x
+ * @param yMinusX y - x
+ * @param t2d 2d * xy
+ */
+ CachedXYT(long[] yPlusX, long[] yMinusX, long[] t2d) {
+ this.yPlusX = yPlusX;
+ this.yMinusX = yMinusX;
+ this.t2d = t2d;
+ }
+
+ CachedXYT(CachedXYT other) {
+ yPlusX = Arrays.copyOf(other.yPlusX, Field25519.LIMB_CNT);
+ yMinusX = Arrays.copyOf(other.yMinusX, Field25519.LIMB_CNT);
+ t2d = Arrays.copyOf(other.t2d, Field25519.LIMB_CNT);
+ }
+
+ // z is one implicitly, so this just copies {@code in} to {@code output}.
+ void multByZ(long[] output, long[] in) {
+ System.arraycopy(in, 0, output, 0, Field25519.LIMB_CNT);
+ }
+
+ /**
+ * If icopy is 1, copies {@code other} into this point. Time invariant wrt to icopy value.
+ */
+ void copyConditional(CachedXYT other, int icopy) {
+ copyConditional(yPlusX, other.yPlusX, icopy);
+ copyConditional(yMinusX, other.yMinusX, icopy);
+ copyConditional(t2d, other.t2d, icopy);
+ }
+
+ /**
+ * Conditionally copies a reduced-form limb arrays {@code b} into {@code a} if {@code icopy} is 1,
+ * but leave {@code a} unchanged if 'iswap' is 0. Runs in data-invariant time to avoid
+ * side-channel attacks.
+ *
+ * <p>NOTE that this function requires that {@code icopy} be 1 or 0; other values give wrong
+ * results. Also, the two limb arrays must be in reduced-coefficient, reduced-degree form: the
+ * values in a[10..19] or b[10..19] aren't swapped, and all all values in a[0..9],b[0..9] must
+ * have magnitude less than Integer.MAX_VALUE.
+ */
+ static void copyConditional(long[] a, long[] b, int icopy) {
+ int copy = -icopy;
+ for (int i = 0; i < Field25519.LIMB_CNT; i++) {
+ int x = copy & (((int) a[i]) ^ ((int) b[i]));
+ a[i] = ((int) a[i]) ^ x;
+ }
+ }
+ }
+
+ private static class CachedXYZT extends CachedXYT {
+
+ private final long[] z;
+
+ CachedXYZT() {
+ this(new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT]);
+ }
+
+ /**
+ * ge_p3_to_cached.c
+ */
+ CachedXYZT(XYZT xyzt) {
+ this();
+ Field25519.sum(yPlusX, xyzt.xyz.y, xyzt.xyz.x);
+ Field25519.sub(yMinusX, xyzt.xyz.y, xyzt.xyz.x);
+ System.arraycopy(xyzt.xyz.z, 0, z, 0, Field25519.LIMB_CNT);
+ Field25519.mult(t2d, xyzt.t, D2);
+ }
+
+ /**
+ * Creates a cached XYZT
+ *
+ * @param yPlusX Y + X
+ * @param yMinusX Y - X
+ * @param z Z
+ * @param t2d 2d * (XY/Z)
+ */
+ CachedXYZT(long[] yPlusX, long[] yMinusX, long[] z, long[] t2d) {
+ super(yPlusX, yMinusX, t2d);
+ this.z = z;
+ }
+
+ @Override
+ public void multByZ(long[] output, long[] in) {
+ Field25519.mult(output, in, z);
+ }
+ }
+
+ /**
+ * Addition defined in Section 3.1 of
+ * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.
+ * <p>
+ * Please note that this is a partial of the operation listed there leaving out the final
+ * conversion from PartialXYZT to XYZT.
+ *
+ * @param extended extended projective point input
+ * @param cached cached projective point input
+ */
+ private static void add(PartialXYZT partialXYZT, XYZT extended, CachedXYT cached) {
+ long[] t = new long[Field25519.LIMB_CNT];
+
+ // Y1 + X1
+ Field25519.sum(partialXYZT.xyz.x, extended.xyz.y, extended.xyz.x);
+
+ // Y1 - X1
+ Field25519.sub(partialXYZT.xyz.y, extended.xyz.y, extended.xyz.x);
+
+ // A = (Y1 - X1) * (Y2 - X2)
+ Field25519.mult(partialXYZT.xyz.y, partialXYZT.xyz.y, cached.yMinusX);
+
+ // B = (Y1 + X1) * (Y2 + X2)
+ Field25519.mult(partialXYZT.xyz.z, partialXYZT.xyz.x, cached.yPlusX);
+
+ // C = T1 * 2d * T2 = 2d * T1 * T2 (2d is written as k in the paper)
+ Field25519.mult(partialXYZT.t, extended.t, cached.t2d);
+
+ // Z1 * Z2
+ cached.multByZ(partialXYZT.xyz.x, extended.xyz.z);
+
+ // D = 2 * Z1 * Z2
+ Field25519.sum(t, partialXYZT.xyz.x, partialXYZT.xyz.x);
+
+ // X3 = B - A
+ Field25519.sub(partialXYZT.xyz.x, partialXYZT.xyz.z, partialXYZT.xyz.y);
+
+ // Y3 = B + A
+ Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.y);
+
+ // Z3 = D + C
+ Field25519.sum(partialXYZT.xyz.z, t, partialXYZT.t);
+
+ // T3 = D - C
+ Field25519.sub(partialXYZT.t, t, partialXYZT.t);
+ }
+
+ /**
+ * Based on the addition defined in Section 3.1 of
+ * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.
+ * <p>
+ * Please note that this is a partial of the operation listed there leaving out the final
+ * conversion from PartialXYZT to XYZT.
+ *
+ * @param extended extended projective point input
+ * @param cached cached projective point input
+ */
+ private static void sub(PartialXYZT partialXYZT, XYZT extended, CachedXYT cached) {
+ long[] t = new long[Field25519.LIMB_CNT];
+
+ // Y1 + X1
+ Field25519.sum(partialXYZT.xyz.x, extended.xyz.y, extended.xyz.x);
+
+ // Y1 - X1
+ Field25519.sub(partialXYZT.xyz.y, extended.xyz.y, extended.xyz.x);
+
+ // A = (Y1 - X1) * (Y2 + X2)
+ Field25519.mult(partialXYZT.xyz.y, partialXYZT.xyz.y, cached.yPlusX);
+
+ // B = (Y1 + X1) * (Y2 - X2)
+ Field25519.mult(partialXYZT.xyz.z, partialXYZT.xyz.x, cached.yMinusX);
+
+ // C = T1 * 2d * T2 = 2d * T1 * T2 (2d is written as k in the paper)
+ Field25519.mult(partialXYZT.t, extended.t, cached.t2d);
+
+ // Z1 * Z2
+ cached.multByZ(partialXYZT.xyz.x, extended.xyz.z);
+
+ // D = 2 * Z1 * Z2
+ Field25519.sum(t, partialXYZT.xyz.x, partialXYZT.xyz.x);
+
+ // X3 = B - A
+ Field25519.sub(partialXYZT.xyz.x, partialXYZT.xyz.z, partialXYZT.xyz.y);
+
+ // Y3 = B + A
+ Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.y);
+
+ // Z3 = D - C
+ Field25519.sub(partialXYZT.xyz.z, t, partialXYZT.t);
+
+ // T3 = D + C
+ Field25519.sum(partialXYZT.t, t, partialXYZT.t);
+ }
+
+ /**
+ * Doubles {@code p} and puts the result into this PartialXYZT.
+ * <p>
+ * This is based on the addition defined in formula 7 in Section 3.3 of
+ * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.
+ * <p>
+ * Please note that this is a partial of the operation listed there leaving out the final
+ * conversion from PartialXYZT to XYZT and also this fixes a typo in calculation of Y3 and T3 in
+ * the paper, H should be replaced with A+B.
+ */
+ private static void doubleXYZ(PartialXYZT partialXYZT, XYZ p) {
+ long[] t0 = new long[Field25519.LIMB_CNT];
+
+ // XX = X1^2
+ Field25519.square(partialXYZT.xyz.x, p.x);
+
+ // YY = Y1^2
+ Field25519.square(partialXYZT.xyz.z, p.y);
+
+ // B' = Z1^2
+ Field25519.square(partialXYZT.t, p.z);
+
+ // B = 2 * B'
+ Field25519.sum(partialXYZT.t, partialXYZT.t, partialXYZT.t);
+
+ // A = X1 + Y1
+ Field25519.sum(partialXYZT.xyz.y, p.x, p.y);
+
+ // AA = A^2
+ Field25519.square(t0, partialXYZT.xyz.y);
+
+ // Y3 = YY + XX
+ Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.x);
+
+ // Z3 = YY - XX
+ Field25519.sub(partialXYZT.xyz.z, partialXYZT.xyz.z, partialXYZT.xyz.x);
+
+ // X3 = AA - Y3
+ Field25519.sub(partialXYZT.xyz.x, t0, partialXYZT.xyz.y);
+
+ // T3 = B - Z3
+ Field25519.sub(partialXYZT.t, partialXYZT.t, partialXYZT.xyz.z);
+ }
+
+ /**
+ * Doubles {@code p} and puts the result into this PartialXYZT.
+ */
+ private static void doubleXYZT(PartialXYZT partialXYZT, XYZT p) {
+ doubleXYZ(partialXYZT, p.xyz);
+ }
+
+ /**
+ * Compares two byte values in constant time.
+ */
+ private static int eq(int a, int b) {
+ int r = ~(a ^ b) & 0xff;
+ r &= r << 4;
+ r &= r << 2;
+ r &= r << 1;
+ return (r >> 7) & 1;
+ }
+
+ /**
+ * This is a constant time operation where point b*B*256^pos is stored in {@code t}.
+ * When b is 0, t remains the same (i.e., neutral point).
+ * <p>
+ * Although B_TABLE[32][8] (B_TABLE[i][j] = (j+1)*B*256^i) has j values in [0, 7], the select
+ * method negates the corresponding point if b is negative (which is straight forward in elliptic
+ * curves by just negating y coordinate). Therefore we can get multiples of B with the half of
+ * memory requirements.
+ *
+ * @param t neutral element (i.e., point 0), also serves as output.
+ * @param pos in B[pos][j] = (j+1)*B*256^pos
+ * @param b value in [-8, 8] range.
+ */
+ private static void select(CachedXYT t, int pos, byte b) {
+ int bnegative = (b & 0xff) >> 7;
+ int babs = b - (((-bnegative) & b) << 1);
+
+ t.copyConditional(B_TABLE[pos][0], eq(babs, 1));
+ t.copyConditional(B_TABLE[pos][1], eq(babs, 2));
+ t.copyConditional(B_TABLE[pos][2], eq(babs, 3));
+ t.copyConditional(B_TABLE[pos][3], eq(babs, 4));
+ t.copyConditional(B_TABLE[pos][4], eq(babs, 5));
+ t.copyConditional(B_TABLE[pos][5], eq(babs, 6));
+ t.copyConditional(B_TABLE[pos][6], eq(babs, 7));
+ t.copyConditional(B_TABLE[pos][7], eq(babs, 8));
+
+ long[] yPlusX = Arrays.copyOf(t.yMinusX, Field25519.LIMB_CNT);
+ long[] yMinusX = Arrays.copyOf(t.yPlusX, Field25519.LIMB_CNT);
+ long[] t2d = Arrays.copyOf(t.t2d, Field25519.LIMB_CNT);
+ neg(t2d, t2d);
+ CachedXYT minust = new CachedXYT(yPlusX, yMinusX, t2d);
+ t.copyConditional(minust, bnegative);
+ }
+
+ /**
+ * Computes {@code a}*B
+ * where a = a[0]+256*a[1]+...+256^31 a[31] and
+ * B is the Ed25519 base point (x,4/5) with x positive.
+ * <p>
+ * Preconditions:
+ * a[31] <= 127
+ *
+ * @throws IllegalStateException iff there is arithmetic error.
+ */
+ @SuppressWarnings("NarrowingCompoundAssignment")
+ private static XYZ scalarMultWithBase(byte[] a) {
+ byte[] e = new byte[2 * Field25519.FIELD_LEN];
+ for (int i = 0; i < Field25519.FIELD_LEN; i++) {
+ e[2 * i + 0] = (byte) (((a[i] & 0xff) >> 0) & 0xf);
+ e[2 * i + 1] = (byte) (((a[i] & 0xff) >> 4) & 0xf);
+ }
+ // each e[i] is between 0 and 15
+ // e[63] is between 0 and 7
+
+ // Rewrite e in a way that each e[i] is in [-8, 8].
+ // This can be done since a[63] is in [0, 7], the carry-over onto the most significant byte
+ // a[63] can be at most 1.
+ int carry = 0;
+ for (int i = 0; i < e.length - 1; i++) {
+ e[i] += carry;
+ carry = e[i] + 8;
+ carry >>= 4;
+ e[i] -= carry << 4;
+ }
+ e[e.length - 1] += carry;
+
+ PartialXYZT ret = new PartialXYZT(NEUTRAL);
+ XYZT xyzt = new XYZT();
+ // Although B_TABLE's i can be at most 31 (stores only 32 4bit multiples of B) and we have 64
+ // 4bit values in e array, the below for loop adds cached values by iterating e by two in odd
+ // indices. After the result, we can double the result point 4 times to shift the multiplication
+ // scalar by 4 bits.
+ for (int i = 1; i < e.length; i += 2) {
+ CachedXYT t = new CachedXYT(CACHED_NEUTRAL);
+ select(t, i / 2, e[i]);
+ add(ret, XYZT.fromPartialXYZT(xyzt, ret), t);
+ }
+
+ // Doubles the result 4 times to shift the multiplication scalar 4 bits to get the actual result
+ // for the odd indices in e.
+ XYZ xyz = new XYZ();
+ doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret));
+ doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret));
+ doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret));
+ doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret));
+
+ // Add multiples of B for even indices of e.
+ for (int i = 0; i < e.length; i += 2) {
+ CachedXYT t = new CachedXYT(CACHED_NEUTRAL);
+ select(t, i / 2, e[i]);
+ add(ret, XYZT.fromPartialXYZT(xyzt, ret), t);
+ }
+
+ // This check is to protect against flaws, i.e. if there is a computation error through a
+ // faulty CPU or if the implementation contains a bug.
+ XYZ result = new XYZ(ret);
+ if (!result.isOnCurve()) {
+ throw new IllegalStateException("arithmetic error in scalar multiplication");
+ }
+ return result;
+ }
+
+ @SuppressWarnings("NarrowingCompoundAssignment")
+ private static byte[] slide(byte[] a) {
+ byte[] r = new byte[256];
+ // Writes each bit in a[0..31] into r[0..255]:
+ // a = a[0]+256*a[1]+...+256^31*a[31] is equal to
+ // r = r[0]+2*r[1]+...+2^255*r[255]
+ for (int i = 0; i < 256; i++) {
+ r[i] = (byte) (1 & ((a[i >> 3] & 0xff) >> (i & 7)));
+ }
+
+ // Transforms r[i] as odd values in [-15, 15]
+ for (int i = 0; i < 256; i++) {
+ if (r[i] != 0) {
+ for (int b = 1; b <= 6 && i + b < 256; b++) {
+ if (r[i + b] != 0) {
+ if (r[i] + (r[i + b] << b) <= 15) {
+ r[i] += r[i + b] << b;
+ r[i + b] = 0;
+ } else if (r[i] - (r[i + b] << b) >= -15) {
+ r[i] -= r[i + b] << b;
+ for (int k = i + b; k < 256; k++) {
+ if (r[k] == 0) {
+ r[k] = 1;
+ break;
+ }
+ r[k] = 0;
+ }
+ } else {
+ break;
+ }
+ }
+ }
+ }
+ }
+ return r;
+ }
+
+ /**
+ * Computes {@code a}*{@code pointA}+{@code b}*B
+ * where a = a[0]+256*a[1]+...+256^31*a[31].
+ * and b = b[0]+256*b[1]+...+256^31*b[31].
+ * B is the Ed25519 base point (x,4/5) with x positive.
+ * <p>
+ * Note that execution time varies based on the input since this will only be used in verification
+ * of signatures.
+ */
+ private static XYZ doubleScalarMultVarTime(byte[] a, XYZT pointA, byte[] b) {
+ // pointA, 3*pointA, 5*pointA, 7*pointA, 9*pointA, 11*pointA, 13*pointA, 15*pointA
+ CachedXYZT[] pointAArray = new CachedXYZT[8];
+ pointAArray[0] = new CachedXYZT(pointA);
+ PartialXYZT t = new PartialXYZT();
+ doubleXYZT(t, pointA);
+ XYZT doubleA = new XYZT(t);
+ for (int i = 1; i < pointAArray.length; i++) {
+ add(t, doubleA, pointAArray[i - 1]);
+ pointAArray[i] = new CachedXYZT(new XYZT(t));
+ }
+
+ byte[] aSlide = slide(a);
+ byte[] bSlide = slide(b);
+ t = new PartialXYZT(NEUTRAL);
+ XYZT u = new XYZT();
+ int i = 255;
+ for (; i >= 0; i--) {
+ if (aSlide[i] != 0 || bSlide[i] != 0) {
+ break;
+ }
+ }
+ for (; i >= 0; i--) {
+ doubleXYZ(t, new XYZ(t));
+ if (aSlide[i] > 0) {
+ add(t, XYZT.fromPartialXYZT(u, t), pointAArray[aSlide[i] / 2]);
+ } else if (aSlide[i] < 0) {
+ sub(t, XYZT.fromPartialXYZT(u, t), pointAArray[-aSlide[i] / 2]);
+ }
+ if (bSlide[i] > 0) {
+ add(t, XYZT.fromPartialXYZT(u, t), B2[bSlide[i] / 2]);
+ } else if (bSlide[i] < 0) {
+ sub(t, XYZT.fromPartialXYZT(u, t), B2[-bSlide[i] / 2]);
+ }
+ }
+
+ return new XYZ(t);
+ }
+
+ /**
+ * Returns true if {@code in} is nonzero.
+ * <p>
+ * Note that execution time might depend on the input {@code in}.
+ */
+ private static boolean isNonZeroVarTime(long[] in) {
+ long[] inCopy = new long[in.length + 1];
+ System.arraycopy(in, 0, inCopy, 0, in.length);
+ Field25519.reduceCoefficients(inCopy);
+ byte[] bytes = Field25519.contract(inCopy);
+ for (byte b : bytes) {
+ if (b != 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the least significant bit of {@code in}.
+ */
+ private static int getLsb(long[] in) {
+ return Field25519.contract(in)[0] & 1;
+ }
+
+ /**
+ * Negates all values in {@code in} and store it in {@code out}.
+ */
+ private static void neg(long[] out, long[] in) {
+ for (int i = 0; i < in.length; i++) {
+ out[i] = -in[i];
+ }
+ }
+
+ /**
+ * Computes {@code in}^(2^252-3) mod 2^255-19 and puts the result in {@code out}.
+ */
+ private static void pow2252m3(long[] out, long[] in) {
+ long[] t0 = new long[Field25519.LIMB_CNT];
+ long[] t1 = new long[Field25519.LIMB_CNT];
+ long[] t2 = new long[Field25519.LIMB_CNT];
+
+ // z2 = z1^2^1
+ Field25519.square(t0, in);
+
+ // z8 = z2^2^2
+ Field25519.square(t1, t0);
+ for (int i = 1; i < 2; i++) {
+ Field25519.square(t1, t1);
+ }
+
+ // z9 = z1*z8
+ Field25519.mult(t1, in, t1);
+
+ // z11 = z2*z9
+ Field25519.mult(t0, t0, t1);
+
+ // z22 = z11^2^1
+ Field25519.square(t0, t0);
+
+ // z_5_0 = z9*z22
+ Field25519.mult(t0, t1, t0);
+
+ // z_10_5 = z_5_0^2^5
+ Field25519.square(t1, t0);
+ for (int i = 1; i < 5; i++) {
+ Field25519.square(t1, t1);
+ }
+
+ // z_10_0 = z_10_5*z_5_0
+ Field25519.mult(t0, t1, t0);
+
+ // z_20_10 = z_10_0^2^10
+ Field25519.square(t1, t0);
+ for (int i = 1; i < 10; i++) {
+ Field25519.square(t1, t1);
+ }
+
+ // z_20_0 = z_20_10*z_10_0
+ Field25519.mult(t1, t1, t0);
+
+ // z_40_20 = z_20_0^2^20
+ Field25519.square(t2, t1);
+ for (int i = 1; i < 20; i++) {
+ Field25519.square(t2, t2);
+ }
+
+ // z_40_0 = z_40_20*z_20_0
+ Field25519.mult(t1, t2, t1);
+
+ // z_50_10 = z_40_0^2^10
+ Field25519.square(t1, t1);
+ for (int i = 1; i < 10; i++) {
+ Field25519.square(t1, t1);
+ }
+
+ // z_50_0 = z_50_10*z_10_0
+ Field25519.mult(t0, t1, t0);
+
+ // z_100_50 = z_50_0^2^50
+ Field25519.square(t1, t0);
+ for (int i = 1; i < 50; i++) {
+ Field25519.square(t1, t1);
+ }
+
+ // z_100_0 = z_100_50*z_50_0
+ Field25519.mult(t1, t1, t0);
+
+ // z_200_100 = z_100_0^2^100
+ Field25519.square(t2, t1);
+ for (int i = 1; i < 100; i++) {
+ Field25519.square(t2, t2);
+ }
+
+ // z_200_0 = z_200_100*z_100_0
+ Field25519.mult(t1, t2, t1);
+
+ // z_250_50 = z_200_0^2^50
+ Field25519.square(t1, t1);
+ for (int i = 1; i < 50; i++) {
+ Field25519.square(t1, t1);
+ }
+
+ // z_250_0 = z_250_50*z_50_0
+ Field25519.mult(t0, t1, t0);
+
+ // z_252_2 = z_250_0^2^2
+ Field25519.square(t0, t0);
+ for (int i = 1; i < 2; i++) {
+ Field25519.square(t0, t0);
+ }
+
+ // z_252_3 = z_252_2*z1
+ Field25519.mult(out, t0, in);
+ }
+
+ /**
+ * Returns 3 bytes of {@code in} starting from {@code idx} in Little-Endian format.
+ */
+ private static long load3(byte[] in, int idx) {
+ long result;
+ result = (long) in[idx] & 0xff;
+ result |= (long) (in[idx + 1] & 0xff) << 8;
+ result |= (long) (in[idx + 2] & 0xff) << 16;
+ return result;
+ }
+
+ /**
+ * Returns 4 bytes of {@code in} starting from {@code idx} in Little-Endian format.
+ */
+ private static long load4(byte[] in, int idx) {
+ long result = load3(in, idx);
+ result |= (long) (in[idx + 3] & 0xff) << 24;
+ return result;
+ }
+
+ /**
+ * Input:
+ * s[0]+256*s[1]+...+256^63*s[63] = s
+ * <p>
+ * Output:
+ * s[0]+256*s[1]+...+256^31*s[31] = s mod l
+ * where l = 2^252 + 27742317777372353535851937790883648493.
+ * Overwrites s in place.
+ */
+ private static void reduce(byte[] s) {
+ // Observation:
+ // 2^252 mod l is equivalent to -27742317777372353535851937790883648493 mod l
+ // Let m = -27742317777372353535851937790883648493
+ // Thus a*2^252+b mod l is equivalent to a*m+b mod l
+ //
+ // First s is divided into chunks of 21 bits as follows:
+ // s0+2^21*s1+2^42*s3+...+2^462*s23 = s[0]+256*s[1]+...+256^63*s[63]
+ long s0 = 2097151 & load3(s, 0);
+ long s1 = 2097151 & (load4(s, 2) >> 5);
+ long s2 = 2097151 & (load3(s, 5) >> 2);
+ long s3 = 2097151 & (load4(s, 7) >> 7);
+ long s4 = 2097151 & (load4(s, 10) >> 4);
+ long s5 = 2097151 & (load3(s, 13) >> 1);
+ long s6 = 2097151 & (load4(s, 15) >> 6);
+ long s7 = 2097151 & (load3(s, 18) >> 3);
+ long s8 = 2097151 & load3(s, 21);
+ long s9 = 2097151 & (load4(s, 23) >> 5);
+ long s10 = 2097151 & (load3(s, 26) >> 2);
+ long s11 = 2097151 & (load4(s, 28) >> 7);
+ long s12 = 2097151 & (load4(s, 31) >> 4);
+ long s13 = 2097151 & (load3(s, 34) >> 1);
+ long s14 = 2097151 & (load4(s, 36) >> 6);
+ long s15 = 2097151 & (load3(s, 39) >> 3);
+ long s16 = 2097151 & load3(s, 42);
+ long s17 = 2097151 & (load4(s, 44) >> 5);
+ long s18 = 2097151 & (load3(s, 47) >> 2);
+ long s19 = 2097151 & (load4(s, 49) >> 7);
+ long s20 = 2097151 & (load4(s, 52) >> 4);
+ long s21 = 2097151 & (load3(s, 55) >> 1);
+ long s22 = 2097151 & (load4(s, 57) >> 6);
+ long s23 = (load4(s, 60) >> 3);
+ long carry0;
+ long carry1;
+ long carry2;
+ long carry3;
+ long carry4;
+ long carry5;
+ long carry6;
+ long carry7;
+ long carry8;
+ long carry9;
+ long carry10;
+ long carry11;
+ long carry12;
+ long carry13;
+ long carry14;
+ long carry15;
+ long carry16;
+
+ // s23*2^462 = s23*2^210*2^252 is equivalent to s23*2^210*m in mod l
+ // As m is a 125 bit number, the result needs to scattered to 6 limbs (125/21 ceil is 6)
+ // starting from s11 (s11*2^210)
+ // m = [666643, 470296, 654183, -997805, 136657, -683901] in 21-bit limbs
+ s11 += s23 * 666643;
+ s12 += s23 * 470296;
+ s13 += s23 * 654183;
+ s14 -= s23 * 997805;
+ s15 += s23 * 136657;
+ s16 -= s23 * 683901;
+ // s23 = 0;
+
+ s10 += s22 * 666643;
+ s11 += s22 * 470296;
+ s12 += s22 * 654183;
+ s13 -= s22 * 997805;
+ s14 += s22 * 136657;
+ s15 -= s22 * 683901;
+ // s22 = 0;
+
+ s9 += s21 * 666643;
+ s10 += s21 * 470296;
+ s11 += s21 * 654183;
+ s12 -= s21 * 997805;
+ s13 += s21 * 136657;
+ s14 -= s21 * 683901;
+ // s21 = 0;
+
+ s8 += s20 * 666643;
+ s9 += s20 * 470296;
+ s10 += s20 * 654183;
+ s11 -= s20 * 997805;
+ s12 += s20 * 136657;
+ s13 -= s20 * 683901;
+ // s20 = 0;
+
+ s7 += s19 * 666643;
+ s8 += s19 * 470296;
+ s9 += s19 * 654183;
+ s10 -= s19 * 997805;
+ s11 += s19 * 136657;
+ s12 -= s19 * 683901;
+ // s19 = 0;
+
+ s6 += s18 * 666643;
+ s7 += s18 * 470296;
+ s8 += s18 * 654183;
+ s9 -= s18 * 997805;
+ s10 += s18 * 136657;
+ s11 -= s18 * 683901;
+ // s18 = 0;
+
+ // Reduce the bit length of limbs from s6 to s15 to 21-bits.
+ carry6 = (s6 + (1 << 20)) >> 21;
+ s7 += carry6;
+ s6 -= carry6 << 21;
+ carry8 = (s8 + (1 << 20)) >> 21;
+ s9 += carry8;
+ s8 -= carry8 << 21;
+ carry10 = (s10 + (1 << 20)) >> 21;
+ s11 += carry10;
+ s10 -= carry10 << 21;
+ carry12 = (s12 + (1 << 20)) >> 21;
+ s13 += carry12;
+ s12 -= carry12 << 21;
+ carry14 = (s14 + (1 << 20)) >> 21;
+ s15 += carry14;
+ s14 -= carry14 << 21;
+ carry16 = (s16 + (1 << 20)) >> 21;
+ s17 += carry16;
+ s16 -= carry16 << 21;
+
+ carry7 = (s7 + (1 << 20)) >> 21;
+ s8 += carry7;
+ s7 -= carry7 << 21;
+ carry9 = (s9 + (1 << 20)) >> 21;
+ s10 += carry9;
+ s9 -= carry9 << 21;
+ carry11 = (s11 + (1 << 20)) >> 21;
+ s12 += carry11;
+ s11 -= carry11 << 21;
+ carry13 = (s13 + (1 << 20)) >> 21;
+ s14 += carry13;
+ s13 -= carry13 << 21;
+ carry15 = (s15 + (1 << 20)) >> 21;
+ s16 += carry15;
+ s15 -= carry15 << 21;
+
+ // Resume reduction where we left off.
+ s5 += s17 * 666643;
+ s6 += s17 * 470296;
+ s7 += s17 * 654183;
+ s8 -= s17 * 997805;
+ s9 += s17 * 136657;
+ s10 -= s17 * 683901;
+ // s17 = 0;
+
+ s4 += s16 * 666643;
+ s5 += s16 * 470296;
+ s6 += s16 * 654183;
+ s7 -= s16 * 997805;
+ s8 += s16 * 136657;
+ s9 -= s16 * 683901;
+ // s16 = 0;
+
+ s3 += s15 * 666643;
+ s4 += s15 * 470296;
+ s5 += s15 * 654183;
+ s6 -= s15 * 997805;
+ s7 += s15 * 136657;
+ s8 -= s15 * 683901;
+ // s15 = 0;
+
+ s2 += s14 * 666643;
+ s3 += s14 * 470296;
+ s4 += s14 * 654183;
+ s5 -= s14 * 997805;
+ s6 += s14 * 136657;
+ s7 -= s14 * 683901;
+ // s14 = 0;
+
+ s1 += s13 * 666643;
+ s2 += s13 * 470296;
+ s3 += s13 * 654183;
+ s4 -= s13 * 997805;
+ s5 += s13 * 136657;
+ s6 -= s13 * 683901;
+ // s13 = 0;
+
+ s0 += s12 * 666643;
+ s1 += s12 * 470296;
+ s2 += s12 * 654183;
+ s3 -= s12 * 997805;
+ s4 += s12 * 136657;
+ s5 -= s12 * 683901;
+ s12 = 0;
+
+ // Reduce the range of limbs from s0 to s11 to 21-bits.
+ carry0 = (s0 + (1 << 20)) >> 21;
+ s1 += carry0;
+ s0 -= carry0 << 21;
+ carry2 = (s2 + (1 << 20)) >> 21;
+ s3 += carry2;
+ s2 -= carry2 << 21;
+ carry4 = (s4 + (1 << 20)) >> 21;
+ s5 += carry4;
+ s4 -= carry4 << 21;
+ carry6 = (s6 + (1 << 20)) >> 21;
+ s7 += carry6;
+ s6 -= carry6 << 21;
+ carry8 = (s8 + (1 << 20)) >> 21;
+ s9 += carry8;
+ s8 -= carry8 << 21;
+ carry10 = (s10 + (1 << 20)) >> 21;
+ s11 += carry10;
+ s10 -= carry10 << 21;
+
+ carry1 = (s1 + (1 << 20)) >> 21;
+ s2 += carry1;
+ s1 -= carry1 << 21;
+ carry3 = (s3 + (1 << 20)) >> 21;
+ s4 += carry3;
+ s3 -= carry3 << 21;
+ carry5 = (s5 + (1 << 20)) >> 21;
+ s6 += carry5;
+ s5 -= carry5 << 21;
+ carry7 = (s7 + (1 << 20)) >> 21;
+ s8 += carry7;
+ s7 -= carry7 << 21;
+ carry9 = (s9 + (1 << 20)) >> 21;
+ s10 += carry9;
+ s9 -= carry9 << 21;
+ carry11 = (s11 + (1 << 20)) >> 21;
+ s12 += carry11;
+ s11 -= carry11 << 21;
+
+ s0 += s12 * 666643;
+ s1 += s12 * 470296;
+ s2 += s12 * 654183;
+ s3 -= s12 * 997805;
+ s4 += s12 * 136657;
+ s5 -= s12 * 683901;
+ s12 = 0;
+
+ // Carry chain reduction to propagate excess bits from s0 to s5 to the most significant limbs.
+ carry0 = s0 >> 21;
+ s1 += carry0;
+ s0 -= carry0 << 21;
+ carry1 = s1 >> 21;
+ s2 += carry1;
+ s1 -= carry1 << 21;
+ carry2 = s2 >> 21;
+ s3 += carry2;
+ s2 -= carry2 << 21;
+ carry3 = s3 >> 21;
+ s4 += carry3;
+ s3 -= carry3 << 21;
+ carry4 = s4 >> 21;
+ s5 += carry4;
+ s4 -= carry4 << 21;
+ carry5 = s5 >> 21;
+ s6 += carry5;
+ s5 -= carry5 << 21;
+ carry6 = s6 >> 21;
+ s7 += carry6;
+ s6 -= carry6 << 21;
+ carry7 = s7 >> 21;
+ s8 += carry7;
+ s7 -= carry7 << 21;
+ carry8 = s8 >> 21;
+ s9 += carry8;
+ s8 -= carry8 << 21;
+ carry9 = s9 >> 21;
+ s10 += carry9;
+ s9 -= carry9 << 21;
+ carry10 = s10 >> 21;
+ s11 += carry10;
+ s10 -= carry10 << 21;
+ carry11 = s11 >> 21;
+ s12 += carry11;
+ s11 -= carry11 << 21;
+
+ // Do one last reduction as s12 might be 1.
+ s0 += s12 * 666643;
+ s1 += s12 * 470296;
+ s2 += s12 * 654183;
+ s3 -= s12 * 997805;
+ s4 += s12 * 136657;
+ s5 -= s12 * 683901;
+ // s12 = 0;
+
+ carry0 = s0 >> 21;
+ s1 += carry0;
+ s0 -= carry0 << 21;
+ carry1 = s1 >> 21;
+ s2 += carry1;
+ s1 -= carry1 << 21;
+ carry2 = s2 >> 21;
+ s3 += carry2;
+ s2 -= carry2 << 21;
+ carry3 = s3 >> 21;
+ s4 += carry3;
+ s3 -= carry3 << 21;
+ carry4 = s4 >> 21;
+ s5 += carry4;
+ s4 -= carry4 << 21;
+ carry5 = s5 >> 21;
+ s6 += carry5;
+ s5 -= carry5 << 21;
+ carry6 = s6 >> 21;
+ s7 += carry6;
+ s6 -= carry6 << 21;
+ carry7 = s7 >> 21;
+ s8 += carry7;
+ s7 -= carry7 << 21;
+ carry8 = s8 >> 21;
+ s9 += carry8;
+ s8 -= carry8 << 21;
+ carry9 = s9 >> 21;
+ s10 += carry9;
+ s9 -= carry9 << 21;
+ carry10 = s10 >> 21;
+ s11 += carry10;
+ s10 -= carry10 << 21;
+
+ // Serialize the result into the s.
+ s[0] = (byte) s0;
+ s[1] = (byte) (s0 >> 8);
+ s[2] = (byte) ((s0 >> 16) | (s1 << 5));
+ s[3] = (byte) (s1 >> 3);
+ s[4] = (byte) (s1 >> 11);
+ s[5] = (byte) ((s1 >> 19) | (s2 << 2));
+ s[6] = (byte) (s2 >> 6);
+ s[7] = (byte) ((s2 >> 14) | (s3 << 7));
+ s[8] = (byte) (s3 >> 1);
+ s[9] = (byte) (s3 >> 9);
+ s[10] = (byte) ((s3 >> 17) | (s4 << 4));
+ s[11] = (byte) (s4 >> 4);
+ s[12] = (byte) (s4 >> 12);
+ s[13] = (byte) ((s4 >> 20) | (s5 << 1));
+ s[14] = (byte) (s5 >> 7);
+ s[15] = (byte) ((s5 >> 15) | (s6 << 6));
+ s[16] = (byte) (s6 >> 2);
+ s[17] = (byte) (s6 >> 10);
+ s[18] = (byte) ((s6 >> 18) | (s7 << 3));
+ s[19] = (byte) (s7 >> 5);
+ s[20] = (byte) (s7 >> 13);
+ s[21] = (byte) s8;
+ s[22] = (byte) (s8 >> 8);
+ s[23] = (byte) ((s8 >> 16) | (s9 << 5));
+ s[24] = (byte) (s9 >> 3);
+ s[25] = (byte) (s9 >> 11);
+ s[26] = (byte) ((s9 >> 19) | (s10 << 2));
+ s[27] = (byte) (s10 >> 6);
+ s[28] = (byte) ((s10 >> 14) | (s11 << 7));
+ s[29] = (byte) (s11 >> 1);
+ s[30] = (byte) (s11 >> 9);
+ s[31] = (byte) (s11 >> 17);
+ }
+
+ /**
+ * Input:
+ * a[0]+256*a[1]+...+256^31*a[31] = a
+ * b[0]+256*b[1]+...+256^31*b[31] = b
+ * c[0]+256*c[1]+...+256^31*c[31] = c
+ * <p>
+ * Output:
+ * s[0]+256*s[1]+...+256^31*s[31] = (ab+c) mod l
+ * where l = 2^252 + 27742317777372353535851937790883648493.
+ */
+ private static void mulAdd(byte[] s, byte[] a, byte[] b, byte[] c) {
+ // This is very similar to Ed25519.reduce, the difference in here is that it computes ab+c
+ // See Ed25519.reduce for related comments.
+ long a0 = 2097151 & load3(a, 0);
+ long a1 = 2097151 & (load4(a, 2) >> 5);
+ long a2 = 2097151 & (load3(a, 5) >> 2);
+ long a3 = 2097151 & (load4(a, 7) >> 7);
+ long a4 = 2097151 & (load4(a, 10) >> 4);
+ long a5 = 2097151 & (load3(a, 13) >> 1);
+ long a6 = 2097151 & (load4(a, 15) >> 6);
+ long a7 = 2097151 & (load3(a, 18) >> 3);
+ long a8 = 2097151 & load3(a, 21);
+ long a9 = 2097151 & (load4(a, 23) >> 5);
+ long a10 = 2097151 & (load3(a, 26) >> 2);
+ long a11 = (load4(a, 28) >> 7);
+ long b0 = 2097151 & load3(b, 0);
+ long b1 = 2097151 & (load4(b, 2) >> 5);
+ long b2 = 2097151 & (load3(b, 5) >> 2);
+ long b3 = 2097151 & (load4(b, 7) >> 7);
+ long b4 = 2097151 & (load4(b, 10) >> 4);
+ long b5 = 2097151 & (load3(b, 13) >> 1);
+ long b6 = 2097151 & (load4(b, 15) >> 6);
+ long b7 = 2097151 & (load3(b, 18) >> 3);
+ long b8 = 2097151 & load3(b, 21);
+ long b9 = 2097151 & (load4(b, 23) >> 5);
+ long b10 = 2097151 & (load3(b, 26) >> 2);
+ long b11 = (load4(b, 28) >> 7);
+ long c0 = 2097151 & load3(c, 0);
+ long c1 = 2097151 & (load4(c, 2) >> 5);
+ long c2 = 2097151 & (load3(c, 5) >> 2);
+ long c3 = 2097151 & (load4(c, 7) >> 7);
+ long c4 = 2097151 & (load4(c, 10) >> 4);
+ long c5 = 2097151 & (load3(c, 13) >> 1);
+ long c6 = 2097151 & (load4(c, 15) >> 6);
+ long c7 = 2097151 & (load3(c, 18) >> 3);
+ long c8 = 2097151 & load3(c, 21);
+ long c9 = 2097151 & (load4(c, 23) >> 5);
+ long c10 = 2097151 & (load3(c, 26) >> 2);
+ long c11 = (load4(c, 28) >> 7);
+ long s0;
+ long s1;
+ long s2;
+ long s3;
+ long s4;
+ long s5;
+ long s6;
+ long s7;
+ long s8;
+ long s9;
+ long s10;
+ long s11;
+ long s12;
+ long s13;
+ long s14;
+ long s15;
+ long s16;
+ long s17;
+ long s18;
+ long s19;
+ long s20;
+ long s21;
+ long s22;
+ long s23;
+ long carry0;
+ long carry1;
+ long carry2;
+ long carry3;
+ long carry4;
+ long carry5;
+ long carry6;
+ long carry7;
+ long carry8;
+ long carry9;
+ long carry10;
+ long carry11;
+ long carry12;
+ long carry13;
+ long carry14;
+ long carry15;
+ long carry16;
+ long carry17;
+ long carry18;
+ long carry19;
+ long carry20;
+ long carry21;
+ long carry22;
+
+ s0 = c0 + a0 * b0;
+ s1 = c1 + a0 * b1 + a1 * b0;
+ s2 = c2 + a0 * b2 + a1 * b1 + a2 * b0;
+ s3 = c3 + a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0;
+ s4 = c4 + a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0;
+ s5 = c5 + a0 * b5 + a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 + a5 * b0;
+ s6 = c6 + a0 * b6 + a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 + a6 * b0;
+ s7 = c7 + a0 * b7 + a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 + a6 * b1 + a7 * b0;
+ s8 = c8 + a0 * b8 + a1 * b7 + a2 * b6 + a3 * b5 + a4 * b4 + a5 * b3 + a6 * b2 + a7 * b1
+ + a8 * b0;
+ s9 = c9 + a0 * b9 + a1 * b8 + a2 * b7 + a3 * b6 + a4 * b5 + a5 * b4 + a6 * b3 + a7 * b2
+ + a8 * b1 + a9 * b0;
+ s10 = c10 + a0 * b10 + a1 * b9 + a2 * b8 + a3 * b7 + a4 * b6 + a5 * b5 + a6 * b4 + a7 * b3
+ + a8 * b2 + a9 * b1 + a10 * b0;
+ s11 = c11 + a0 * b11 + a1 * b10 + a2 * b9 + a3 * b8 + a4 * b7 + a5 * b6 + a6 * b5 + a7 * b4
+ + a8 * b3 + a9 * b2 + a10 * b1 + a11 * b0;
+ s12 = a1 * b11 + a2 * b10 + a3 * b9 + a4 * b8 + a5 * b7 + a6 * b6 + a7 * b5 + a8 * b4 + a9 * b3
+ + a10 * b2 + a11 * b1;
+ s13 = a2 * b11 + a3 * b10 + a4 * b9 + a5 * b8 + a6 * b7 + a7 * b6 + a8 * b5 + a9 * b4 + a10 * b3
+ + a11 * b2;
+ s14 = a3 * b11 + a4 * b10 + a5 * b9 + a6 * b8 + a7 * b7 + a8 * b6 + a9 * b5 + a10 * b4
+ + a11 * b3;
+ s15 = a4 * b11 + a5 * b10 + a6 * b9 + a7 * b8 + a8 * b7 + a9 * b6 + a10 * b5 + a11 * b4;
+ s16 = a5 * b11 + a6 * b10 + a7 * b9 + a8 * b8 + a9 * b7 + a10 * b6 + a11 * b5;
+ s17 = a6 * b11 + a7 * b10 + a8 * b9 + a9 * b8 + a10 * b7 + a11 * b6;
+ s18 = a7 * b11 + a8 * b10 + a9 * b9 + a10 * b8 + a11 * b7;
+ s19 = a8 * b11 + a9 * b10 + a10 * b9 + a11 * b8;
+ s20 = a9 * b11 + a10 * b10 + a11 * b9;
+ s21 = a10 * b11 + a11 * b10;
+ s22 = a11 * b11;
+ s23 = 0;
+
+ carry0 = (s0 + (1 << 20)) >> 21;
+ s1 += carry0;
+ s0 -= carry0 << 21;
+ carry2 = (s2 + (1 << 20)) >> 21;
+ s3 += carry2;
+ s2 -= carry2 << 21;
+ carry4 = (s4 + (1 << 20)) >> 21;
+ s5 += carry4;
+ s4 -= carry4 << 21;
+ carry6 = (s6 + (1 << 20)) >> 21;
+ s7 += carry6;
+ s6 -= carry6 << 21;
+ carry8 = (s8 + (1 << 20)) >> 21;
+ s9 += carry8;
+ s8 -= carry8 << 21;
+ carry10 = (s10 + (1 << 20)) >> 21;
+ s11 += carry10;
+ s10 -= carry10 << 21;
+ carry12 = (s12 + (1 << 20)) >> 21;
+ s13 += carry12;
+ s12 -= carry12 << 21;
+ carry14 = (s14 + (1 << 20)) >> 21;
+ s15 += carry14;
+ s14 -= carry14 << 21;
+ carry16 = (s16 + (1 << 20)) >> 21;
+ s17 += carry16;
+ s16 -= carry16 << 21;
+ carry18 = (s18 + (1 << 20)) >> 21;
+ s19 += carry18;
+ s18 -= carry18 << 21;
+ carry20 = (s20 + (1 << 20)) >> 21;
+ s21 += carry20;
+ s20 -= carry20 << 21;
+ carry22 = (s22 + (1 << 20)) >> 21;
+ s23 += carry22;
+ s22 -= carry22 << 21;
+
+ carry1 = (s1 + (1 << 20)) >> 21;
+ s2 += carry1;
+ s1 -= carry1 << 21;
+ carry3 = (s3 + (1 << 20)) >> 21;
+ s4 += carry3;
+ s3 -= carry3 << 21;
+ carry5 = (s5 + (1 << 20)) >> 21;
+ s6 += carry5;
+ s5 -= carry5 << 21;
+ carry7 = (s7 + (1 << 20)) >> 21;
+ s8 += carry7;
+ s7 -= carry7 << 21;
+ carry9 = (s9 + (1 << 20)) >> 21;
+ s10 += carry9;
+ s9 -= carry9 << 21;
+ carry11 = (s11 + (1 << 20)) >> 21;
+ s12 += carry11;
+ s11 -= carry11 << 21;
+ carry13 = (s13 + (1 << 20)) >> 21;
+ s14 += carry13;
+ s13 -= carry13 << 21;
+ carry15 = (s15 + (1 << 20)) >> 21;
+ s16 += carry15;
+ s15 -= carry15 << 21;
+ carry17 = (s17 + (1 << 20)) >> 21;
+ s18 += carry17;
+ s17 -= carry17 << 21;
+ carry19 = (s19 + (1 << 20)) >> 21;
+ s20 += carry19;
+ s19 -= carry19 << 21;
+ carry21 = (s21 + (1 << 20)) >> 21;
+ s22 += carry21;
+ s21 -= carry21 << 21;
+
+ s11 += s23 * 666643;
+ s12 += s23 * 470296;
+ s13 += s23 * 654183;
+ s14 -= s23 * 997805;
+ s15 += s23 * 136657;
+ s16 -= s23 * 683901;
+ // s23 = 0;
+
+ s10 += s22 * 666643;
+ s11 += s22 * 470296;
+ s12 += s22 * 654183;
+ s13 -= s22 * 997805;
+ s14 += s22 * 136657;
+ s15 -= s22 * 683901;
+ // s22 = 0;
+
+ s9 += s21 * 666643;
+ s10 += s21 * 470296;
+ s11 += s21 * 654183;
+ s12 -= s21 * 997805;
+ s13 += s21 * 136657;
+ s14 -= s21 * 683901;
+ // s21 = 0;
+
+ s8 += s20 * 666643;
+ s9 += s20 * 470296;
+ s10 += s20 * 654183;
+ s11 -= s20 * 997805;
+ s12 += s20 * 136657;
+ s13 -= s20 * 683901;
+ // s20 = 0;
+
+ s7 += s19 * 666643;
+ s8 += s19 * 470296;
+ s9 += s19 * 654183;
+ s10 -= s19 * 997805;
+ s11 += s19 * 136657;
+ s12 -= s19 * 683901;
+ // s19 = 0;
+
+ s6 += s18 * 666643;
+ s7 += s18 * 470296;
+ s8 += s18 * 654183;
+ s9 -= s18 * 997805;
+ s10 += s18 * 136657;
+ s11 -= s18 * 683901;
+ // s18 = 0;
+
+ carry6 = (s6 + (1 << 20)) >> 21;
+ s7 += carry6;
+ s6 -= carry6 << 21;
+ carry8 = (s8 + (1 << 20)) >> 21;
+ s9 += carry8;
+ s8 -= carry8 << 21;
+ carry10 = (s10 + (1 << 20)) >> 21;
+ s11 += carry10;
+ s10 -= carry10 << 21;
+ carry12 = (s12 + (1 << 20)) >> 21;
+ s13 += carry12;
+ s12 -= carry12 << 21;
+ carry14 = (s14 + (1 << 20)) >> 21;
+ s15 += carry14;
+ s14 -= carry14 << 21;
+ carry16 = (s16 + (1 << 20)) >> 21;
+ s17 += carry16;
+ s16 -= carry16 << 21;
+
+ carry7 = (s7 + (1 << 20)) >> 21;
+ s8 += carry7;
+ s7 -= carry7 << 21;
+ carry9 = (s9 + (1 << 20)) >> 21;
+ s10 += carry9;
+ s9 -= carry9 << 21;
+ carry11 = (s11 + (1 << 20)) >> 21;
+ s12 += carry11;
+ s11 -= carry11 << 21;
+ carry13 = (s13 + (1 << 20)) >> 21;
+ s14 += carry13;
+ s13 -= carry13 << 21;
+ carry15 = (s15 + (1 << 20)) >> 21;
+ s16 += carry15;
+ s15 -= carry15 << 21;
+
+ s5 += s17 * 666643;
+ s6 += s17 * 470296;
+ s7 += s17 * 654183;
+ s8 -= s17 * 997805;
+ s9 += s17 * 136657;
+ s10 -= s17 * 683901;
+ // s17 = 0;
+
+ s4 += s16 * 666643;
+ s5 += s16 * 470296;
+ s6 += s16 * 654183;
+ s7 -= s16 * 997805;
+ s8 += s16 * 136657;
+ s9 -= s16 * 683901;
+ // s16 = 0;
+
+ s3 += s15 * 666643;
+ s4 += s15 * 470296;
+ s5 += s15 * 654183;
+ s6 -= s15 * 997805;
+ s7 += s15 * 136657;
+ s8 -= s15 * 683901;
+ // s15 = 0;
+
+ s2 += s14 * 666643;
+ s3 += s14 * 470296;
+ s4 += s14 * 654183;
+ s5 -= s14 * 997805;
+ s6 += s14 * 136657;
+ s7 -= s14 * 683901;
+ // s14 = 0;
+
+ s1 += s13 * 666643;
+ s2 += s13 * 470296;
+ s3 += s13 * 654183;
+ s4 -= s13 * 997805;
+ s5 += s13 * 136657;
+ s6 -= s13 * 683901;
+ // s13 = 0;
+
+ s0 += s12 * 666643;
+ s1 += s12 * 470296;
+ s2 += s12 * 654183;
+ s3 -= s12 * 997805;
+ s4 += s12 * 136657;
+ s5 -= s12 * 683901;
+ s12 = 0;
+
+ carry0 = (s0 + (1 << 20)) >> 21;
+ s1 += carry0;
+ s0 -= carry0 << 21;
+ carry2 = (s2 + (1 << 20)) >> 21;
+ s3 += carry2;
+ s2 -= carry2 << 21;
+ carry4 = (s4 + (1 << 20)) >> 21;
+ s5 += carry4;
+ s4 -= carry4 << 21;
+ carry6 = (s6 + (1 << 20)) >> 21;
+ s7 += carry6;
+ s6 -= carry6 << 21;
+ carry8 = (s8 + (1 << 20)) >> 21;
+ s9 += carry8;
+ s8 -= carry8 << 21;
+ carry10 = (s10 + (1 << 20)) >> 21;
+ s11 += carry10;
+ s10 -= carry10 << 21;
+
+ carry1 = (s1 + (1 << 20)) >> 21;
+ s2 += carry1;
+ s1 -= carry1 << 21;
+ carry3 = (s3 + (1 << 20)) >> 21;
+ s4 += carry3;
+ s3 -= carry3 << 21;
+ carry5 = (s5 + (1 << 20)) >> 21;
+ s6 += carry5;
+ s5 -= carry5 << 21;
+ carry7 = (s7 + (1 << 20)) >> 21;
+ s8 += carry7;
+ s7 -= carry7 << 21;
+ carry9 = (s9 + (1 << 20)) >> 21;
+ s10 += carry9;
+ s9 -= carry9 << 21;
+ carry11 = (s11 + (1 << 20)) >> 21;
+ s12 += carry11;
+ s11 -= carry11 << 21;
+
+ s0 += s12 * 666643;
+ s1 += s12 * 470296;
+ s2 += s12 * 654183;
+ s3 -= s12 * 997805;
+ s4 += s12 * 136657;
+ s5 -= s12 * 683901;
+ s12 = 0;
+
+ carry0 = s0 >> 21;
+ s1 += carry0;
+ s0 -= carry0 << 21;
+ carry1 = s1 >> 21;
+ s2 += carry1;
+ s1 -= carry1 << 21;
+ carry2 = s2 >> 21;
+ s3 += carry2;
+ s2 -= carry2 << 21;
+ carry3 = s3 >> 21;
+ s4 += carry3;
+ s3 -= carry3 << 21;
+ carry4 = s4 >> 21;
+ s5 += carry4;
+ s4 -= carry4 << 21;
+ carry5 = s5 >> 21;
+ s6 += carry5;
+ s5 -= carry5 << 21;
+ carry6 = s6 >> 21;
+ s7 += carry6;
+ s6 -= carry6 << 21;
+ carry7 = s7 >> 21;
+ s8 += carry7;
+ s7 -= carry7 << 21;
+ carry8 = s8 >> 21;
+ s9 += carry8;
+ s8 -= carry8 << 21;
+ carry9 = s9 >> 21;
+ s10 += carry9;
+ s9 -= carry9 << 21;
+ carry10 = s10 >> 21;
+ s11 += carry10;
+ s10 -= carry10 << 21;
+ carry11 = s11 >> 21;
+ s12 += carry11;
+ s11 -= carry11 << 21;
+
+ s0 += s12 * 666643;
+ s1 += s12 * 470296;
+ s2 += s12 * 654183;
+ s3 -= s12 * 997805;
+ s4 += s12 * 136657;
+ s5 -= s12 * 683901;
+ // s12 = 0;
+
+ carry0 = s0 >> 21;
+ s1 += carry0;
+ s0 -= carry0 << 21;
+ carry1 = s1 >> 21;
+ s2 += carry1;
+ s1 -= carry1 << 21;
+ carry2 = s2 >> 21;
+ s3 += carry2;
+ s2 -= carry2 << 21;
+ carry3 = s3 >> 21;
+ s4 += carry3;
+ s3 -= carry3 << 21;
+ carry4 = s4 >> 21;
+ s5 += carry4;
+ s4 -= carry4 << 21;
+ carry5 = s5 >> 21;
+ s6 += carry5;
+ s5 -= carry5 << 21;
+ carry6 = s6 >> 21;
+ s7 += carry6;
+ s6 -= carry6 << 21;
+ carry7 = s7 >> 21;
+ s8 += carry7;
+ s7 -= carry7 << 21;
+ carry8 = s8 >> 21;
+ s9 += carry8;
+ s8 -= carry8 << 21;
+ carry9 = s9 >> 21;
+ s10 += carry9;
+ s9 -= carry9 << 21;
+ carry10 = s10 >> 21;
+ s11 += carry10;
+ s10 -= carry10 << 21;
+
+ s[0] = (byte) s0;
+ s[1] = (byte) (s0 >> 8);
+ s[2] = (byte) ((s0 >> 16) | (s1 << 5));
+ s[3] = (byte) (s1 >> 3);
+ s[4] = (byte) (s1 >> 11);
+ s[5] = (byte) ((s1 >> 19) | (s2 << 2));
+ s[6] = (byte) (s2 >> 6);
+ s[7] = (byte) ((s2 >> 14) | (s3 << 7));
+ s[8] = (byte) (s3 >> 1);
+ s[9] = (byte) (s3 >> 9);
+ s[10] = (byte) ((s3 >> 17) | (s4 << 4));
+ s[11] = (byte) (s4 >> 4);
+ s[12] = (byte) (s4 >> 12);
+ s[13] = (byte) ((s4 >> 20) | (s5 << 1));
+ s[14] = (byte) (s5 >> 7);
+ s[15] = (byte) ((s5 >> 15) | (s6 << 6));
+ s[16] = (byte) (s6 >> 2);
+ s[17] = (byte) (s6 >> 10);
+ s[18] = (byte) ((s6 >> 18) | (s7 << 3));
+ s[19] = (byte) (s7 >> 5);
+ s[20] = (byte) (s7 >> 13);
+ s[21] = (byte) s8;
+ s[22] = (byte) (s8 >> 8);
+ s[23] = (byte) ((s8 >> 16) | (s9 << 5));
+ s[24] = (byte) (s9 >> 3);
+ s[25] = (byte) (s9 >> 11);
+ s[26] = (byte) ((s9 >> 19) | (s10 << 2));
+ s[27] = (byte) (s10 >> 6);
+ s[28] = (byte) ((s10 >> 14) | (s11 << 7));
+ s[29] = (byte) (s11 >> 1);
+ s[30] = (byte) (s11 >> 9);
+ s[31] = (byte) (s11 >> 17);
+ }
+
+ // The order of the generator as unsigned bytes in little endian order.
+ // (2^252 + 0x14def9dea2f79cd65812631a5cf5d3ed, cf. RFC 7748)
+ private static final byte[] GROUP_ORDER = {
+ (byte) 0xed, (byte) 0xd3, (byte) 0xf5, (byte) 0x5c,
+ (byte) 0x1a, (byte) 0x63, (byte) 0x12, (byte) 0x58,
+ (byte) 0xd6, (byte) 0x9c, (byte) 0xf7, (byte) 0xa2,
+ (byte) 0xde, (byte) 0xf9, (byte) 0xde, (byte) 0x14,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x10};
+
+ // Checks whether s represents an integer smaller than the order of the group.
+ // This is needed to ensure that EdDSA signatures are non-malleable, as failing to check
+ // the range of S allows to modify signatures (cf. RFC 8032, Section 5.2.7 and Section 8.4.)
+ // @param s an integer in little-endian order.
+ private static boolean isSmallerThanGroupOrder(byte[] s) {
+ for (int j = Field25519.FIELD_LEN - 1; j >= 0; j--) {
+ // compare unsigned bytes
+ int a = s[j] & 0xff;
+ int b = GROUP_ORDER[j] & 0xff;
+ if (a != b) {
+ return a < b;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the EdDSA {@code signature} with {@code message}, can be verified with
+ * {@code publicKey}.
+ */
+ public static boolean verify(final byte[] message, final byte[] signature,
+ final byte[] publicKey) {
+ try {
+ if (signature.length != SIGNATURE_LEN) {
+ return false;
+ }
+ if (publicKey.length != PUBLIC_KEY_LEN) {
+ return false;
+ }
+ byte[] s = Arrays.copyOfRange(signature, Field25519.FIELD_LEN, SIGNATURE_LEN);
+ if (!isSmallerThanGroupOrder(s)) {
+ return false;
+ }
+ MessageDigest digest = MessageDigest.getInstance("SHA-512");
+ digest.update(signature, 0, Field25519.FIELD_LEN);
+ digest.update(publicKey);
+ digest.update(message);
+ byte[] h = digest.digest();
+ reduce(h);
+
+ XYZT negPublicKey = XYZT.fromBytesNegateVarTime(publicKey);
+ XYZ xyz = doubleScalarMultVarTime(h, negPublicKey, s);
+ byte[] expectedR = xyz.toBytes();
+ for (int i = 0; i < Field25519.FIELD_LEN; i++) {
+ if (expectedR[i] != signature[i]) {
+ return false;
+ }
+ }
+ return true;
+ } catch (final GeneralSecurityException ignored) {
+ return false;
+ }
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/updater/SnackbarUpdateShower.kt b/ui/src/main/java/com/wireguard/android/updater/SnackbarUpdateShower.kt
new file mode 100644
index 00000000..30da3b07
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/updater/SnackbarUpdateShower.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.updater
+
+import android.content.Intent
+import android.net.Uri
+import android.view.View
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.snackbar.BaseTransientBottomBar
+import com.google.android.material.snackbar.Snackbar
+import com.wireguard.android.R
+import com.wireguard.android.util.ErrorMessages
+import com.wireguard.android.util.QuantityFormatter
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import kotlin.time.Duration.Companion.seconds
+
+class SnackbarUpdateShower(private val fragment: Fragment) {
+ private var lastUserIntervention: Updater.Progress.NeedsUserIntervention? = null
+ private val intentLauncher = fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ lastUserIntervention?.markAsDone()
+ }
+
+ private class SwapableSnackbar(fragment: Fragment, view: View, anchor: View?) {
+ private val actionSnackbar = makeSnackbar(fragment, view, anchor)
+ private val statusSnackbar = makeSnackbar(fragment, view, anchor)
+ private var showingAction: Boolean = false
+ private var showingStatus: Boolean = false
+
+ private fun makeSnackbar(fragment: Fragment, view: View, anchor: View?): Snackbar {
+ val snackbar = Snackbar.make(fragment.requireContext(), view, "", Snackbar.LENGTH_INDEFINITE)
+ if (anchor != null)
+ snackbar.anchorView = anchor
+ snackbar.setTextMaxLines(6)
+ snackbar.behavior = object : BaseTransientBottomBar.Behavior() {
+ override fun canSwipeDismissView(child: View): Boolean {
+ return false
+ }
+ }
+ snackbar.addCallback(object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
+ override fun onDismissed(snackbar: Snackbar?, @DismissEvent event: Int) {
+ super.onDismissed(snackbar, event)
+ if (event == DISMISS_EVENT_MANUAL || event == DISMISS_EVENT_ACTION ||
+ (snackbar == actionSnackbar && !showingAction) || (snackbar == statusSnackbar && !showingStatus)
+ )
+ return
+ fragment.lifecycleScope.launch {
+ delay(5.seconds)
+ snackbar?.show()
+ }
+ }
+ })
+ return snackbar
+ }
+
+ fun showAction(text: String, action: String, listener: View.OnClickListener) {
+ if (showingStatus) {
+ showingStatus = false
+ statusSnackbar.dismiss()
+ }
+ actionSnackbar.setText(text)
+ actionSnackbar.setAction(action, listener)
+ if (!showingAction) {
+ actionSnackbar.show()
+ showingAction = true
+ }
+ }
+
+ fun showText(text: String) {
+ if (showingAction) {
+ showingAction = false
+ actionSnackbar.dismiss()
+ }
+ statusSnackbar.setText(text)
+ if (!showingStatus) {
+ statusSnackbar.show()
+ showingStatus = true
+ }
+ }
+
+ fun dismiss() {
+ actionSnackbar.dismiss()
+ statusSnackbar.dismiss()
+ showingAction = false
+ showingStatus = false
+ }
+ }
+
+ fun attach(view: View, anchor: View?) {
+ val snackbar = SwapableSnackbar(fragment, view, anchor)
+ val context = fragment.requireContext()
+
+ Updater.state.onEach { progress ->
+ when (progress) {
+ is Updater.Progress.Complete ->
+ snackbar.dismiss()
+
+ is Updater.Progress.Available ->
+ snackbar.showAction(context.getString(R.string.updater_avalable), context.getString(R.string.updater_action)) {
+ progress.update()
+ }
+
+ is Updater.Progress.NeedsUserIntervention -> {
+ lastUserIntervention = progress
+ intentLauncher.launch(progress.intent)
+ }
+
+ is Updater.Progress.Installing ->
+ snackbar.showText(context.getString(R.string.updater_installing))
+
+ is Updater.Progress.Rechecking ->
+ snackbar.showText(context.getString(R.string.updater_rechecking))
+
+ is Updater.Progress.Downloading -> {
+ if (progress.bytesTotal != 0UL) {
+ snackbar.showText(
+ context.getString(
+ R.string.updater_download_progress,
+ QuantityFormatter.formatBytes(progress.bytesDownloaded.toLong()),
+ QuantityFormatter.formatBytes(progress.bytesTotal.toLong()),
+ progress.bytesDownloaded.toFloat() * 100.0 / progress.bytesTotal.toFloat()
+ )
+ )
+ } else {
+ snackbar.showText(
+ context.getString(
+ R.string.updater_download_progress_nototal,
+ QuantityFormatter.formatBytes(progress.bytesDownloaded.toLong())
+ )
+ )
+ }
+ }
+
+ is Updater.Progress.Failure -> {
+ snackbar.showText(context.getString(R.string.updater_failure, ErrorMessages[progress.error]))
+ delay(5.seconds)
+ progress.retry()
+ }
+
+ is Updater.Progress.Corrupt -> {
+ MaterialAlertDialogBuilder(context)
+ .setTitle(R.string.updater_corrupt_title)
+ .setMessage(R.string.updater_corrupt_message)
+ .setPositiveButton(R.string.updater_corrupt_navigate) { _, _ ->
+ val intent = Intent(Intent.ACTION_VIEW)
+ intent.data = Uri.parse(progress.downloadUrl)
+ try {
+ context.startActivity(intent)
+ } catch (e: Throwable) {
+ Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_SHORT).show()
+ }
+ }.setCancelable(false).setOnDismissListener {
+ val intent = Intent(Intent.ACTION_MAIN)
+ intent.addCategory(Intent.CATEGORY_HOME)
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ context.startActivity(intent)
+ System.exit(0)
+ }.show()
+ }
+ }
+ }.launchIn(fragment.lifecycleScope)
+ }
+} \ No newline at end of file
diff --git a/ui/src/main/java/com/wireguard/android/updater/Updater.kt b/ui/src/main/java/com/wireguard/android/updater/Updater.kt
new file mode 100644
index 00000000..651e3cd7
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/updater/Updater.kt
@@ -0,0 +1,451 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.updater
+
+import android.Manifest
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.PackageInstaller
+import android.content.pm.PackageManager
+import android.os.Build
+import android.util.Base64
+import android.util.Log
+import androidx.core.content.ContextCompat
+import androidx.core.content.IntentCompat
+import com.wireguard.android.Application
+import com.wireguard.android.BuildConfig
+import com.wireguard.android.activity.MainActivity
+import com.wireguard.android.util.UserKnobs
+import com.wireguard.android.util.applicationScope
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.io.IOException
+import java.net.HttpURLConnection
+import java.net.URL
+import java.nio.charset.StandardCharsets
+import java.security.InvalidKeyException
+import java.security.InvalidParameterException
+import java.security.MessageDigest
+import java.util.UUID
+import kotlin.math.max
+import kotlin.time.Duration.Companion.minutes
+import kotlin.time.Duration.Companion.seconds
+
+object Updater {
+ private const val TAG = "WireGuard/Updater"
+ private const val UPDATE_URL_FMT = "https://download.wireguard.com/android-client/%s"
+ private const val APK_NAME_PREFIX = BuildConfig.APPLICATION_ID + "-"
+ private const val APK_NAME_SUFFIX = ".apk"
+ private const val LATEST_FILE = "latest.sig"
+ private const val RELEASE_PUBLIC_KEY_BASE64 = "RWTAzwGRYr3EC9px0Ia3fbttz8WcVN6wrOwWp2delz4el6SI8XmkKSMp"
+ private val CURRENT_VERSION by lazy { Version(BuildConfig.VERSION_NAME) }
+
+ private val updaterScope = CoroutineScope(Job() + Dispatchers.IO)
+
+ private fun installer(context: Context): String = try {
+ val packageName = context.packageName
+ val pm = context.packageManager
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ pm.getInstallSourceInfo(packageName).installingPackageName ?: ""
+ } else {
+ @Suppress("DEPRECATION")
+ pm.getInstallerPackageName(packageName) ?: ""
+ }
+ } catch (_: Throwable) {
+ ""
+ }
+
+ fun installerIsGooglePlay(context: Context): Boolean = installer(context) == "com.android.vending"
+
+ sealed class Progress {
+ object Complete : Progress()
+ class Available(val version: String) : Progress() {
+ fun update() {
+ applicationScope.launch {
+ UserKnobs.setUpdaterNewerVersionConsented(version)
+ }
+ }
+ }
+
+ object Rechecking : Progress()
+ class Downloading(val bytesDownloaded: ULong, val bytesTotal: ULong) : Progress()
+ object Installing : Progress()
+ class NeedsUserIntervention(val intent: Intent, private val id: Int) : Progress() {
+
+ private suspend fun installerActive(): Boolean {
+ if (mutableState.firstOrNull() != this@NeedsUserIntervention)
+ return true
+ try {
+ if (Application.get().packageManager.packageInstaller.getSessionInfo(id)?.isActive == true)
+ return true
+ } catch (_: SecurityException) {
+ return true
+ }
+ return false
+ }
+
+ fun markAsDone() {
+ applicationScope.launch {
+ if (installerActive())
+ return@launch
+ delay(7.seconds)
+ if (installerActive())
+ return@launch
+ emitProgress(Failure(Exception("Ignored by user")))
+ }
+ }
+ }
+
+ class Failure(val error: Throwable) : Progress() {
+ fun retry() {
+ updaterScope.launch {
+ downloadAndUpdateWrapErrors()
+ }
+ }
+ }
+
+ class Corrupt(private val betterFile: String?) : Progress() {
+ val downloadUrl: String
+ get() = UPDATE_URL_FMT.format(betterFile ?: "")
+ }
+ }
+
+ private val mutableState = MutableStateFlow<Progress>(Progress.Complete)
+ val state = mutableState.asStateFlow()
+
+ private suspend fun emitProgress(progress: Progress, force: Boolean = false) {
+ if (force || mutableState.firstOrNull()?.javaClass != progress.javaClass)
+ mutableState.emit(progress)
+ }
+
+ private class Sha256Digest(hex: String) {
+ val bytes: ByteArray
+
+ init {
+ if (hex.length != 64)
+ throw InvalidParameterException("SHA256 hashes must be 32 bytes long")
+ bytes = hex.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
+ }
+ }
+
+ @OptIn(ExperimentalUnsignedTypes::class)
+ private class Version(version: String) : Comparable<Version> {
+ val parts: ULongArray
+
+ init {
+ val strParts = version.split(".")
+ if (strParts.isEmpty())
+ throw InvalidParameterException("Version has no parts")
+ parts = ULongArray(strParts.size)
+ for (i in parts.indices) {
+ parts[i] = strParts[i].toULong()
+ }
+ }
+
+ override fun toString(): String {
+ return parts.joinToString(".")
+ }
+
+ override fun compareTo(other: Version): Int {
+ for (i in 0 until max(parts.size, other.parts.size)) {
+ val lhsPart = if (i < parts.size) parts[i] else 0UL
+ val rhsPart = if (i < other.parts.size) other.parts[i] else 0UL
+ if (lhsPart > rhsPart)
+ return 1
+ else if (lhsPart < rhsPart)
+ return -1
+ }
+ return 0
+ }
+ }
+
+ private class Update(val fileName: String, val version: Version, val hash: Sha256Digest)
+
+ private fun versionOfFile(name: String): Version? {
+ if (!name.startsWith(APK_NAME_PREFIX) || !name.endsWith(APK_NAME_SUFFIX))
+ return null
+ return try {
+ Version(name.substring(APK_NAME_PREFIX.length, name.length - APK_NAME_SUFFIX.length))
+ } catch (_: Throwable) {
+ null
+ }
+ }
+
+ private fun verifySignedFileList(signifyDigest: String): List<Update> {
+ val updates = ArrayList<Update>(1)
+ val publicKeyBytes = Base64.decode(RELEASE_PUBLIC_KEY_BASE64, Base64.DEFAULT)
+ if (publicKeyBytes == null || publicKeyBytes.size != 32 + 10 || publicKeyBytes[0] != 'E'.code.toByte() || publicKeyBytes[1] != 'd'.code.toByte())
+ throw InvalidKeyException("Invalid public key")
+ val lines = signifyDigest.split("\n", limit = 3)
+ if (lines.size != 3)
+ throw InvalidParameterException("Invalid signature format: too few lines")
+ if (!lines[0].startsWith("untrusted comment: "))
+ throw InvalidParameterException("Invalid signature format: missing comment")
+ val signatureBytes = Base64.decode(lines[1], Base64.DEFAULT)
+ if (signatureBytes == null || signatureBytes.size != 64 + 10)
+ throw InvalidParameterException("Invalid signature format: wrong sized or missing signature")
+ for (i in 0..9) {
+ if (signatureBytes[i] != publicKeyBytes[i])
+ throw InvalidParameterException("Invalid signature format: wrong signer")
+ }
+ if (!Ed25519.verify(
+ lines[2].toByteArray(StandardCharsets.UTF_8),
+ signatureBytes.sliceArray(10 until 10 + 64),
+ publicKeyBytes.sliceArray(10 until 10 + 32)
+ )
+ )
+ throw SecurityException("Invalid signature")
+ for (line in lines[2].split("\n").dropLastWhile { it.isEmpty() }) {
+ val components = line.split(" ", limit = 2)
+ if (components.size != 2)
+ throw InvalidParameterException("Invalid file list format: too few components")
+ /* If version is null, it's not a file we understand, but still a legitimate entry, so don't throw. */
+ val version = versionOfFile(components[1]) ?: continue
+ updates.add(Update(components[1], version, Sha256Digest(components[0])))
+ }
+ return updates
+ }
+
+ private fun checkForUpdates(): Update? {
+ val connection = URL(UPDATE_URL_FMT.format(LATEST_FILE)).openConnection() as HttpURLConnection
+ connection.setRequestProperty("User-Agent", Application.USER_AGENT)
+ connection.connect()
+ if (connection.responseCode != HttpURLConnection.HTTP_OK)
+ throw IOException(connection.responseMessage)
+ var fileListBytes = ByteArray(1024 * 512 /* 512 KiB */)
+ connection.inputStream.use {
+ val len = it.read(fileListBytes)
+ if (len <= 0)
+ throw IOException("File list is empty")
+ fileListBytes = fileListBytes.sliceArray(0 until len)
+ }
+ return verifySignedFileList(fileListBytes.decodeToString()).maxByOrNull { it.version }
+ }
+
+ private suspend fun downloadAndUpdate() = withContext(Dispatchers.IO) {
+ val receiver = InstallReceiver()
+ val context = Application.get().applicationContext
+ val pendingIntent = withContext(Dispatchers.Main) {
+ ContextCompat.registerReceiver(context, receiver, IntentFilter(receiver.sessionId), ContextCompat.RECEIVER_NOT_EXPORTED)
+ PendingIntent.getBroadcast(
+ context,
+ 0,
+ Intent(receiver.sessionId).setPackage(context.packageName),
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
+ )
+ }
+
+ emitProgress(Progress.Rechecking)
+ val update = checkForUpdates()
+ if (update == null || update.version <= CURRENT_VERSION) {
+ emitProgress(Progress.Complete)
+ return@withContext
+ }
+
+ emitProgress(Progress.Downloading(0UL, 0UL), true)
+ val connection = URL(UPDATE_URL_FMT.format(update.fileName)).openConnection() as HttpURLConnection
+ connection.setRequestProperty("User-Agent", Application.USER_AGENT)
+ connection.connect()
+ if (connection.responseCode != HttpURLConnection.HTTP_OK)
+ throw IOException("Update could not be fetched: ${connection.responseCode}")
+
+ var downloadedByteLen: ULong = 0UL
+ val totalByteLen = (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) connection.contentLengthLong else connection.contentLength).toLong().toULong()
+ val fileBytes = ByteArray(1024 * 32 /* 32 KiB */)
+ val digest = MessageDigest.getInstance("SHA-256")
+ emitProgress(Progress.Downloading(downloadedByteLen, totalByteLen), true)
+
+ val installer = context.packageManager.packageInstaller
+ val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
+ params.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
+ params.setAppPackageName(context.packageName) /* Enforces updates; disallows new apps. */
+ val session = installer.openSession(installer.createSession(params))
+ var sessionFailure = true
+ try {
+ val installDest = session.openWrite(receiver.sessionId, 0, -1)
+
+ installDest.use { dest ->
+ connection.inputStream.use { src ->
+ while (true) {
+ val readLen = src.read(fileBytes)
+ if (readLen <= 0)
+ break
+
+ digest.update(fileBytes, 0, readLen)
+ dest.write(fileBytes, 0, readLen)
+
+ downloadedByteLen += readLen.toUInt()
+ emitProgress(Progress.Downloading(downloadedByteLen, totalByteLen), true)
+
+ if (downloadedByteLen >= 1024UL * 1024UL * 100UL /* 100 MiB */)
+ throw IOException("File too large")
+ }
+ }
+ }
+
+ emitProgress(Progress.Installing)
+ if (!digest.digest().contentEquals(update.hash.bytes))
+ throw SecurityException("Update has invalid hash")
+ sessionFailure = false
+ } finally {
+ if (sessionFailure) {
+ session.abandon()
+ session.close()
+ }
+ }
+ session.commit(pendingIntent.intentSender)
+ session.close()
+ }
+
+ private var updating = false
+ private suspend fun downloadAndUpdateWrapErrors() {
+ if (updating)
+ return
+ updating = true
+ try {
+ downloadAndUpdate()
+ } catch (e: Throwable) {
+ Log.e(TAG, "Update failure", e)
+ emitProgress(Progress.Failure(e))
+ }
+ updating = false
+ }
+
+ private class InstallReceiver : BroadcastReceiver() {
+ val sessionId = UUID.randomUUID().toString()
+
+ override fun onReceive(context: Context, intent: Intent) {
+ if (sessionId != intent.action)
+ return
+
+ when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE_INVALID)) {
+ PackageInstaller.STATUS_PENDING_USER_ACTION -> {
+ val id = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, 0)
+ val userIntervention = IntentCompat.getParcelableExtra(intent, Intent.EXTRA_INTENT, Intent::class.java)!!
+ applicationScope.launch {
+ emitProgress(Progress.NeedsUserIntervention(userIntervention, id))
+ }
+ }
+
+ PackageInstaller.STATUS_SUCCESS -> {
+ applicationScope.launch {
+ emitProgress(Progress.Complete)
+ }
+ context.applicationContext.unregisterReceiver(this)
+ }
+
+ else -> {
+ val id = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, 0)
+ try {
+ context.applicationContext.packageManager.packageInstaller.abandonSession(id)
+ } catch (_: SecurityException) {
+ }
+ val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) ?: "Installation error $status"
+ applicationScope.launch {
+ val e = Exception(message)
+ Log.e(TAG, "Update failure", e)
+ emitProgress(Progress.Failure(e))
+ }
+ context.applicationContext.unregisterReceiver(this)
+ }
+ }
+ }
+ }
+
+ fun monitorForUpdates() {
+ if (BuildConfig.DEBUG)
+ return
+
+ val context = Application.get()
+
+ if (installerIsGooglePlay(context))
+ return
+
+ if (!if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ @Suppress("DEPRECATION")
+ context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS)
+ } else {
+ context.packageManager.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong()))
+ }.requestedPermissions.contains(Manifest.permission.REQUEST_INSTALL_PACKAGES)
+ ) {
+ if (installer(context).isNotEmpty()) {
+ updaterScope.launch {
+ val update = try {
+ checkForUpdates()
+ } catch (_: Throwable) {
+ null
+ }
+ emitProgress(Progress.Corrupt(update?.fileName))
+ }
+ }
+ return
+ }
+
+ updaterScope.launch {
+ if (UserKnobs.updaterNewerVersionSeen.firstOrNull()?.let { Version(it) > CURRENT_VERSION } == true)
+ return@launch
+
+ var waitTime = 15
+ while (true) {
+ try {
+ val update = checkForUpdates() ?: continue
+ if (update.version > CURRENT_VERSION) {
+ Log.i(TAG, "Update available: ${update.version}")
+ UserKnobs.setUpdaterNewerVersionSeen(update.version.toString())
+ return@launch
+ }
+ } catch (_: Throwable) {
+ }
+ delay(waitTime.minutes)
+ waitTime = 45
+ }
+ }
+
+ UserKnobs.updaterNewerVersionSeen.onEach { ver ->
+ if (
+ ver != null &&
+ Version(ver) > CURRENT_VERSION &&
+ UserKnobs.updaterNewerVersionConsented.firstOrNull()?.let { Version(it) > CURRENT_VERSION } != true
+ )
+ emitProgress(Progress.Available(ver))
+ }.launchIn(applicationScope)
+
+ UserKnobs.updaterNewerVersionConsented.onEach { ver ->
+ if (ver != null && Version(ver) > CURRENT_VERSION)
+ updaterScope.launch {
+ downloadAndUpdateWrapErrors()
+ }
+ }.launchIn(applicationScope)
+ }
+
+ class AppUpdatedReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED)
+ return
+
+ if (installer(context) != context.packageName)
+ return
+
+ /* TODO: does not work because of restrictions placed on broadcast receivers. */
+ val start = Intent(context, MainActivity::class.java)
+ start.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ start.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ context.startActivity(start)
+ }
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/util/AdminKnobs.kt b/ui/src/main/java/com/wireguard/android/util/AdminKnobs.kt
index c7a683a3..2f90b2bb 100644
--- a/ui/src/main/java/com/wireguard/android/util/AdminKnobs.kt
+++ b/ui/src/main/java/com/wireguard/android/util/AdminKnobs.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
@@ -13,5 +13,5 @@ object AdminKnobs {
private val restrictions: RestrictionsManager? = Application.get().getSystemService()
val disableConfigExport: Boolean
get() = restrictions?.applicationRestrictions?.getBoolean("disable_config_export", false)
- ?: false
+ ?: false
}
diff --git a/ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt b/ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt
index d187a4c8..54d4da87 100644
--- a/ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt
+++ b/ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
@@ -31,25 +31,29 @@ object BiometricAuthenticator {
}
fun authenticate(
- @StringRes dialogTitleRes: Int,
- fragment: Fragment,
- callback: (Result) -> Unit
+ @StringRes dialogTitleRes: Int,
+ fragment: Fragment,
+ callback: (Result) -> Unit
) {
val authCallback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
Log.d(TAG, "BiometricAuthentication error: errorCode=$errorCode, msg=$errString")
- callback(when (errorCode) {
- BiometricPrompt.ERROR_CANCELED, BiometricPrompt.ERROR_USER_CANCELED,
- BiometricPrompt.ERROR_NEGATIVE_BUTTON -> {
- Result.Cancelled
- }
- BiometricPrompt.ERROR_HW_NOT_PRESENT, BiometricPrompt.ERROR_HW_UNAVAILABLE,
- BiometricPrompt.ERROR_NO_BIOMETRICS, BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> {
- Result.HardwareUnavailableOrDisabled
+ callback(
+ when (errorCode) {
+ BiometricPrompt.ERROR_CANCELED, BiometricPrompt.ERROR_USER_CANCELED,
+ BiometricPrompt.ERROR_NEGATIVE_BUTTON -> {
+ Result.Cancelled
+ }
+
+ 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))
}
- else -> Result.Failure(errorCode, fragment.getString(R.string.biometric_auth_error_reason, errString))
- })
+ )
}
override fun onAuthenticationFailed() {
@@ -64,9 +68,9 @@ object BiometricAuthenticator {
}
val biometricPrompt = BiometricPrompt(fragment, { Handler(Looper.getMainLooper()).post(it) }, authCallback)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
- .setTitle(fragment.getString(dialogTitleRes))
- .setAllowedAuthenticators(allowedAuthenticators)
- .build()
+ .setTitle(fragment.getString(dialogTitleRes))
+ .setAllowedAuthenticators(allowedAuthenticators)
+ .build()
if (BiometricManager.from(fragment.requireContext()).canAuthenticate(allowedAuthenticators) == BiometricManager.BIOMETRIC_SUCCESS) {
biometricPrompt.authenticate(promptInfo)
} else {
diff --git a/ui/src/main/java/com/wireguard/android/util/ClipboardUtils.kt b/ui/src/main/java/com/wireguard/android/util/ClipboardUtils.kt
index 65fdf078..c9a7f59d 100644
--- a/ui/src/main/java/com/wireguard/android/util/ClipboardUtils.kt
+++ b/ui/src/main/java/com/wireguard/android/util/ClipboardUtils.kt
@@ -1,11 +1,12 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util
import android.content.ClipData
import android.content.ClipboardManager
+import android.os.Build
import android.view.View
import android.widget.TextView
import androidx.core.content.getSystemService
@@ -29,6 +30,8 @@ object ClipboardUtils {
}
val service = view.context.getSystemService<ClipboardManager>() ?: return
service.setPrimaryClip(ClipData.newPlainText(data.second, data.first))
- Snackbar.make(view, view.context.getString(R.string.copied_to_clipboard, data.second), Snackbar.LENGTH_LONG).show()
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ Snackbar.make(view, view.context.getString(R.string.copied_to_clipboard, data.second), Snackbar.LENGTH_LONG).show()
+ }
}
}
diff --git a/ui/src/main/java/com/wireguard/android/util/DownloadsFileSaver.kt b/ui/src/main/java/com/wireguard/android/util/DownloadsFileSaver.kt
index 6a7256e0..ace1dc05 100644
--- a/ui/src/main/java/com/wireguard/android/util/DownloadsFileSaver.kt
+++ b/ui/src/main/java/com/wireguard/android/util/DownloadsFileSaver.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util
@@ -46,9 +46,9 @@ class DownloadsFileSaver(private val context: ComponentActivity) {
contentValues.put(MediaColumns.DISPLAY_NAME, name)
contentValues.put(MediaColumns.MIME_TYPE, mimeType)
val contentUri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
- ?: throw IOException(context.getString(R.string.create_downloads_file_error))
+ ?: throw IOException(context.getString(R.string.create_downloads_file_error))
val contentStream = contentResolver.openOutputStream(contentUri)
- ?: throw IOException(context.getString(R.string.create_downloads_file_error))
+ ?: throw IOException(context.getString(R.string.create_downloads_file_error))
@Suppress("DEPRECATION") var cursor = contentResolver.query(contentUri, arrayOf(MediaColumns.DATA), null, null, null)
var path: String? = null
if (cursor != null) {
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 9369415e..d617adec 100644
--- a/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt
+++ b/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt
@@ -1,11 +1,13 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util
import android.content.res.Resources
import android.os.RemoteException
+import com.google.zxing.ChecksumException
+import com.google.zxing.NotFoundException
import com.wireguard.android.Application
import com.wireguard.android.R
import com.wireguard.android.backend.BackendException
@@ -20,47 +22,47 @@ import java.net.InetAddress
object ErrorMessages {
private val BCE_REASON_MAP = mapOf(
- BadConfigException.Reason.INVALID_KEY to R.string.bad_config_reason_invalid_key,
- BadConfigException.Reason.INVALID_NUMBER to R.string.bad_config_reason_invalid_number,
- BadConfigException.Reason.INVALID_VALUE to R.string.bad_config_reason_invalid_value,
- BadConfigException.Reason.MISSING_ATTRIBUTE to R.string.bad_config_reason_missing_attribute,
- BadConfigException.Reason.MISSING_SECTION to R.string.bad_config_reason_missing_section,
- BadConfigException.Reason.SYNTAX_ERROR to R.string.bad_config_reason_syntax_error,
- BadConfigException.Reason.UNKNOWN_ATTRIBUTE to R.string.bad_config_reason_unknown_attribute,
- BadConfigException.Reason.UNKNOWN_SECTION to R.string.bad_config_reason_unknown_section
+ BadConfigException.Reason.INVALID_KEY to R.string.bad_config_reason_invalid_key,
+ BadConfigException.Reason.INVALID_NUMBER to R.string.bad_config_reason_invalid_number,
+ BadConfigException.Reason.INVALID_VALUE to R.string.bad_config_reason_invalid_value,
+ BadConfigException.Reason.MISSING_ATTRIBUTE to R.string.bad_config_reason_missing_attribute,
+ BadConfigException.Reason.MISSING_SECTION to R.string.bad_config_reason_missing_section,
+ BadConfigException.Reason.SYNTAX_ERROR to R.string.bad_config_reason_syntax_error,
+ BadConfigException.Reason.UNKNOWN_ATTRIBUTE to R.string.bad_config_reason_unknown_attribute,
+ BadConfigException.Reason.UNKNOWN_SECTION to R.string.bad_config_reason_unknown_section
)
private val BE_REASON_MAP = mapOf(
- BackendException.Reason.UNKNOWN_KERNEL_MODULE_NAME to R.string.module_version_error,
- BackendException.Reason.WG_QUICK_CONFIG_ERROR_CODE to R.string.tunnel_config_error,
- BackendException.Reason.TUNNEL_MISSING_CONFIG to R.string.no_config_error,
- BackendException.Reason.VPN_NOT_AUTHORIZED to R.string.vpn_not_authorized_error,
- BackendException.Reason.UNABLE_TO_START_VPN to R.string.vpn_start_error,
- BackendException.Reason.TUN_CREATION_ERROR to R.string.tun_create_error,
- BackendException.Reason.GO_ACTIVATION_ERROR_CODE to R.string.tunnel_on_error,
- BackendException.Reason.DNS_RESOLUTION_FAILURE to R.string.tunnel_dns_failure
+ BackendException.Reason.UNKNOWN_KERNEL_MODULE_NAME to R.string.module_version_error,
+ BackendException.Reason.WG_QUICK_CONFIG_ERROR_CODE to R.string.tunnel_config_error,
+ BackendException.Reason.TUNNEL_MISSING_CONFIG to R.string.no_config_error,
+ BackendException.Reason.VPN_NOT_AUTHORIZED to R.string.vpn_not_authorized_error,
+ BackendException.Reason.UNABLE_TO_START_VPN to R.string.vpn_start_error,
+ BackendException.Reason.TUN_CREATION_ERROR to R.string.tun_create_error,
+ BackendException.Reason.GO_ACTIVATION_ERROR_CODE to R.string.tunnel_on_error,
+ BackendException.Reason.DNS_RESOLUTION_FAILURE to R.string.tunnel_dns_failure
)
private val KFE_FORMAT_MAP = mapOf(
- Key.Format.BASE64 to R.string.key_length_explanation_base64,
- Key.Format.BINARY to R.string.key_length_explanation_binary,
- Key.Format.HEX to R.string.key_length_explanation_hex
+ Key.Format.BASE64 to R.string.key_length_explanation_base64,
+ Key.Format.BINARY to R.string.key_length_explanation_binary,
+ Key.Format.HEX to R.string.key_length_explanation_hex
)
private val KFE_TYPE_MAP = mapOf(
- KeyFormatException.Type.CONTENTS to R.string.key_contents_error,
- KeyFormatException.Type.LENGTH to R.string.key_length_error
+ KeyFormatException.Type.CONTENTS to R.string.key_contents_error,
+ KeyFormatException.Type.LENGTH to R.string.key_length_error
)
private val PE_CLASS_MAP = mapOf(
- InetAddress::class.java to R.string.parse_error_inet_address,
- InetEndpoint::class.java to R.string.parse_error_inet_endpoint,
- InetNetwork::class.java to R.string.parse_error_inet_network,
- Int::class.java to R.string.parse_error_integer
+ InetAddress::class.java to R.string.parse_error_inet_address,
+ InetEndpoint::class.java to R.string.parse_error_inet_endpoint,
+ InetNetwork::class.java to R.string.parse_error_inet_network,
+ Int::class.java to R.string.parse_error_integer
)
private val RSE_REASON_MAP = mapOf(
- RootShellException.Reason.NO_ROOT_ACCESS to R.string.error_root,
- RootShellException.Reason.SHELL_MARKER_COUNT_ERROR to R.string.shell_marker_count_error,
- RootShellException.Reason.SHELL_EXIT_STATUS_READ_ERROR to R.string.shell_exit_status_read_error,
- RootShellException.Reason.SHELL_START_ERROR to R.string.shell_start_error,
- RootShellException.Reason.CREATE_BIN_DIR_ERROR to R.string.create_bin_dir_error,
- RootShellException.Reason.CREATE_TEMP_DIR_ERROR to R.string.create_temp_dir_error
+ RootShellException.Reason.NO_ROOT_ACCESS to R.string.error_root,
+ RootShellException.Reason.SHELL_MARKER_COUNT_ERROR to R.string.shell_marker_count_error,
+ RootShellException.Reason.SHELL_EXIT_STATUS_READ_ERROR to R.string.shell_exit_status_read_error,
+ RootShellException.Reason.SHELL_START_ERROR to R.string.shell_start_error,
+ RootShellException.Reason.CREATE_BIN_DIR_ERROR to R.string.create_bin_dir_error,
+ RootShellException.Reason.CREATE_TEMP_DIR_ERROR to R.string.create_temp_dir_error
)
operator fun get(throwable: Throwable?): String {
@@ -78,15 +80,27 @@ object ErrorMessages {
val explanation = getBadConfigExceptionExplanation(resources, rootCause)
resources.getString(R.string.bad_config_error, reason, context) + explanation
}
+
rootCause is BackendException -> {
resources.getString(BE_REASON_MAP.getValue(rootCause.reason), *rootCause.format)
}
+
rootCause is RootShellException -> {
resources.getString(RSE_REASON_MAP.getValue(rootCause.reason), *rootCause.format)
}
- rootCause.message != null -> {
- rootCause.message!!
+
+ rootCause is NotFoundException -> {
+ resources.getString(R.string.error_no_qr_found)
+ }
+
+ rootCause is ChecksumException -> {
+ resources.getString(R.string.error_qr_checksum)
+ }
+
+ rootCause.localizedMessage != null -> {
+ rootCause.localizedMessage!!
}
+
else -> {
val errorType = rootCause.javaClass.simpleName
resources.getString(R.string.generic_error, errorType)
@@ -94,14 +108,16 @@ object ErrorMessages {
}
}
- private fun getBadConfigExceptionExplanation(resources: Resources,
- bce: BadConfigException): String {
+ private fun getBadConfigExceptionExplanation(
+ resources: Resources,
+ bce: BadConfigException
+ ): String {
if (bce.cause is KeyFormatException) {
val kfe = bce.cause as KeyFormatException?
if (kfe!!.type == KeyFormatException.Type.LENGTH) return resources.getString(KFE_FORMAT_MAP.getValue(kfe.format))
} else if (bce.cause is ParseException) {
val pe = bce.cause as ParseException?
- if (pe!!.message != null) return ": ${pe.message}"
+ if (pe!!.localizedMessage != null) return ": ${pe.localizedMessage}"
} else if (bce.location == BadConfigException.Location.LISTEN_PORT) {
return resources.getString(R.string.bad_config_explanation_udp_port)
} else if (bce.location == BadConfigException.Location.MTU) {
@@ -112,8 +128,10 @@ object ErrorMessages {
return ""
}
- private fun getBadConfigExceptionReason(resources: Resources,
- bce: BadConfigException): String {
+ private fun getBadConfigExceptionReason(
+ resources: Resources,
+ bce: BadConfigException
+ ): String {
if (bce.cause is KeyFormatException) {
val kfe = bce.cause as KeyFormatException?
return resources.getString(KFE_TYPE_MAP.getValue(kfe!!.type))
@@ -129,7 +147,8 @@ object ErrorMessages {
var cause = throwable
while (cause.cause != null) {
if (cause is BadConfigException || cause is BackendException ||
- cause is RootShellException) break
+ cause is RootShellException
+ ) break
val nextCause = cause.cause!!
if (nextCause is RemoteException) break
cause = nextCause
diff --git a/ui/src/main/java/com/wireguard/android/util/Extensions.kt b/ui/src/main/java/com/wireguard/android/util/Extensions.kt
index f653cb61..3bc85051 100644
--- a/ui/src/main/java/com/wireguard/android/util/Extensions.kt
+++ b/ui/src/main/java/com/wireguard/android/util/Extensions.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
@@ -25,7 +25,7 @@ val Any.applicationScope: CoroutineScope
val Preference.activity: SettingsActivity
get() = context as? SettingsActivity
- ?: throw IllegalStateException("Failed to resolve SettingsActivity")
+ ?: throw IllegalStateException("Failed to resolve SettingsActivity")
val Preference.lifecycleScope: CoroutineScope
get() = activity.lifecycleScope
diff --git a/ui/src/main/java/com/wireguard/android/util/QrCodeFromFileScanner.kt b/ui/src/main/java/com/wireguard/android/util/QrCodeFromFileScanner.kt
new file mode 100644
index 00000000..abc025a4
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/util/QrCodeFromFileScanner.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.util
+
+import android.content.ContentResolver
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.net.Uri
+import android.util.Log
+import com.google.zxing.BinaryBitmap
+import com.google.zxing.DecodeHintType
+import com.google.zxing.NotFoundException
+import com.google.zxing.RGBLuminanceSource
+import com.google.zxing.Reader
+import com.google.zxing.Result
+import com.google.zxing.common.HybridBinarizer
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+/**
+ * Encapsulates the logic of scanning a barcode from a file,
+ * @property contentResolver - Resolver to read the incoming data
+ * @property reader - An instance of zxing's [Reader] class to parse the image
+ */
+class QrCodeFromFileScanner(
+ private val contentResolver: ContentResolver,
+ private val reader: Reader,
+) {
+
+ private fun scanBitmapForResult(source: Bitmap): Result {
+ val width = source.width
+ val height = source.height
+ val pixels = IntArray(width * height)
+ source.getPixels(pixels, 0, width, 0, 0, width, height)
+
+ val bBitmap = BinaryBitmap(HybridBinarizer(RGBLuminanceSource(width, height, pixels)))
+ return reader.decode(bBitmap, mapOf(DecodeHintType.TRY_HARDER to true))
+ }
+
+ private fun downscaleBitmap(source: Bitmap, scaledSize: Int): Bitmap {
+
+ val originalWidth = source.width
+ val originalHeight = source.height
+
+ var newWidth = -1
+ var newHeight = -1
+ val multFactor: Float
+
+ when {
+ originalHeight > originalWidth -> {
+ newHeight = scaledSize
+ multFactor = originalWidth.toFloat() / originalHeight.toFloat()
+ newWidth = (newHeight * multFactor).toInt()
+ }
+
+ originalWidth > originalHeight -> {
+ newWidth = scaledSize
+ multFactor = originalHeight.toFloat() / originalWidth.toFloat()
+ newHeight = (newWidth * multFactor).toInt()
+ }
+
+ originalHeight == originalWidth -> {
+ newHeight = scaledSize
+ newWidth = scaledSize
+ }
+ }
+ return Bitmap.createScaledBitmap(source, newWidth, newHeight, false)
+ }
+
+ private fun doScan(data: Uri): Result {
+ Log.d(TAG, "Starting to scan an image: $data")
+ contentResolver.openInputStream(data).use { inputStream ->
+ val originalBitmap = BitmapFactory.decodeStream(inputStream)
+ ?: throw IllegalArgumentException("Can't decode stream to Bitmap")
+
+ return try {
+ scanBitmapForResult(originalBitmap).also {
+ Log.d(TAG, "Found result in original image")
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Original image scan finished with error: $e, will try downscaled image")
+ val scaleBitmap = downscaleBitmap(originalBitmap, 500)
+ scanBitmapForResult(originalBitmap).also { scaleBitmap.recycle() }
+ } finally {
+ originalBitmap.recycle()
+ }
+ }
+
+ }
+
+ /**
+ * Attempts to parse incoming data
+ * @return result of the decoding operation
+ * @throws NotFoundException when parser didn't find QR code in the image
+ */
+ suspend fun scan(data: Uri) = withContext(Dispatchers.Default) { doScan(data) }
+
+ companion object {
+ private const val TAG = "QrCodeFromFileScanner"
+
+ /**
+ * Given a reference to a file, check if this file could be parsed by this class
+ * @return true if the file can be parsed, false if not
+ */
+ fun validContentType(contentResolver: ContentResolver, data: Uri): Boolean {
+ return contentResolver.getType(data)?.startsWith("image/") == true
+ }
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt b/ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt
index 4a9ffed4..f7de2465 100644
--- a/ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt
+++ b/ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt
@@ -1,12 +1,20 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.util
+import android.icu.text.ListFormatter
+import android.icu.text.MeasureFormat
+import android.icu.text.RelativeDateTimeFormatter
+import android.icu.util.Measure
+import android.icu.util.MeasureUnit
+import android.os.Build
import com.wireguard.android.Application
import com.wireguard.android.R
+import java.util.Locale
+import kotlin.time.Duration.Companion.seconds
object QuantityFormatter {
fun formatBytes(bytes: Long): String {
@@ -19,4 +27,40 @@ object QuantityFormatter {
else -> context.getString(R.string.transfer_tibibytes, bytes / (1024.0 * 1024.0 * 1024.0) / 1024.0)
}
}
+
+ fun formatEpochAgo(epochMillis: Long): String {
+ var span = (System.currentTimeMillis() - epochMillis) / 1000
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
+ return Application.get().applicationContext.getString(R.string.latest_handshake_ago, span.seconds.toString())
+
+ if (span <= 0L)
+ return RelativeDateTimeFormatter.getInstance().format(RelativeDateTimeFormatter.Direction.PLAIN, RelativeDateTimeFormatter.AbsoluteUnit.NOW)
+ val measureFormat = MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
+ val parts = ArrayList<CharSequence>(4)
+ if (span >= 24 * 60 * 60L) {
+ val v = span / (24 * 60 * 60L)
+ parts.add(measureFormat.format(Measure(v, MeasureUnit.DAY)))
+ span -= v * (24 * 60 * 60L)
+ }
+ if (span >= 60 * 60L) {
+ val v = span / (60 * 60L)
+ parts.add(measureFormat.format(Measure(v, MeasureUnit.HOUR)))
+ span -= v * (60 * 60L)
+ }
+ if (span >= 60L) {
+ val v = span / 60L
+ parts.add(measureFormat.format(Measure(v, MeasureUnit.MINUTE)))
+ span -= v * 60L
+ }
+ if (span > 0L)
+ parts.add(measureFormat.format(Measure(span, MeasureUnit.SECOND)))
+
+ val joined = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
+ parts.joinToString()
+ else
+ ListFormatter.getInstance(Locale.getDefault(), ListFormatter.Type.UNITS, ListFormatter.Width.SHORT).format(parts)
+
+ return Application.get().applicationContext.getString(R.string.latest_handshake_ago, joined)
+ }
} \ No newline at end of file
diff --git a/ui/src/main/java/com/wireguard/android/util/TunnelImporter.kt b/ui/src/main/java/com/wireguard/android/util/TunnelImporter.kt
index 64993cda..daefc378 100644
--- a/ui/src/main/java/com/wireguard/android/util/TunnelImporter.kt
+++ b/ui/src/main/java/com/wireguard/android/util/TunnelImporter.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
@@ -24,7 +24,6 @@ import java.io.BufferedReader
import java.io.ByteArrayInputStream
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
-import java.util.ArrayList
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
@@ -135,12 +134,16 @@ object TunnelImporter {
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)
+ 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)
+ message = context.resources.getQuantityString(
+ R.plurals.import_partial_success,
+ tunnels.size + throwables.size,
+ tunnels.size, tunnels.size + throwables.size
+ )
messageCallback(message)
}
diff --git a/ui/src/main/java/com/wireguard/android/util/UserKnobs.kt b/ui/src/main/java/com/wireguard/android/util/UserKnobs.kt
index 224c1e1a..2c6ca8c3 100644
--- a/ui/src/main/java/com/wireguard/android/util/UserKnobs.kt
+++ b/ui/src/main/java/com/wireguard/android/util/UserKnobs.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
@@ -41,6 +41,12 @@ object UserKnobs {
it[DARK_THEME] ?: false
}
+ suspend fun setDarkTheme(on: Boolean) {
+ Application.getPreferencesDataStore().edit {
+ it[DARK_THEME] = on
+ }
+ }
+
private val ALLOW_REMOTE_CONTROL_INTENTS = booleanPreferencesKey("allow_remote_control_intents")
val allowRemoteControlIntents: Flow<Boolean>
get() = Application.getPreferencesDataStore().data.map {
@@ -82,4 +88,34 @@ object UserKnobs {
it[RUNNING_TUNNELS] = runningTunnels
}
}
+
+ private val UPDATER_NEWER_VERSION_SEEN = stringPreferencesKey("updater_newer_version_seen")
+ val updaterNewerVersionSeen: Flow<String?>
+ get() = Application.getPreferencesDataStore().data.map {
+ it[UPDATER_NEWER_VERSION_SEEN]
+ }
+
+ suspend fun setUpdaterNewerVersionSeen(newerVersionSeen: String?) {
+ Application.getPreferencesDataStore().edit {
+ if (newerVersionSeen == null)
+ it.remove(UPDATER_NEWER_VERSION_SEEN)
+ else
+ it[UPDATER_NEWER_VERSION_SEEN] = newerVersionSeen
+ }
+ }
+
+ private val UPDATER_NEWER_VERSION_CONSENTED = stringPreferencesKey("updater_newer_version_consented")
+ val updaterNewerVersionConsented: Flow<String?>
+ get() = Application.getPreferencesDataStore().data.map {
+ it[UPDATER_NEWER_VERSION_CONSENTED]
+ }
+
+ suspend fun setUpdaterNewerVersionConsented(newerVersionConsented: String?) {
+ Application.getPreferencesDataStore().edit {
+ if (newerVersionConsented == null)
+ it.remove(UPDATER_NEWER_VERSION_CONSENTED)
+ else
+ it[UPDATER_NEWER_VERSION_CONSENTED] = newerVersionConsented
+ }
+ }
}
diff --git a/ui/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.kt b/ui/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.kt
index ccfbce34..c73b1efc 100644
--- a/ui/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.kt
+++ b/ui/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.kt
@@ -1,25 +1,30 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.viewmodel
+import android.os.Build
import android.os.Parcel
import android.os.Parcelable
+import androidx.core.os.ParcelCompat
import androidx.databinding.ObservableArrayList
import androidx.databinding.ObservableList
import com.wireguard.config.BadConfigException
import com.wireguard.config.Config
import com.wireguard.config.Peer
-import java.util.ArrayList
class ConfigProxy : Parcelable {
val `interface`: InterfaceProxy
val peers: ObservableList<PeerProxy> = ObservableArrayList()
private constructor(parcel: Parcel) {
- `interface` = parcel.readParcelable(InterfaceProxy::class.java.classLoader)!!
- parcel.readTypedList(peers, PeerProxy.CREATOR)
+ `interface` = ParcelCompat.readParcelable(parcel, InterfaceProxy::class.java.classLoader, InterfaceProxy::class.java) ?: InterfaceProxy()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ ParcelCompat.readParcelableList(parcel, peers, PeerProxy::class.java.classLoader, PeerProxy::class.java)
+ } else {
+ parcel.readTypedList(peers, PeerProxy.CREATOR)
+ }
peers.forEach { it.bind(this) }
}
@@ -50,14 +55,18 @@ class ConfigProxy : Parcelable {
val resolvedPeers: MutableCollection<Peer> = ArrayList()
peers.forEach { resolvedPeers.add(it.resolve()) }
return Config.Builder()
- .setInterface(`interface`.resolve())
- .addPeers(resolvedPeers)
- .build()
+ .setInterface(`interface`.resolve())
+ .addPeers(resolvedPeers)
+ .build()
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeParcelable(`interface`, flags)
- dest.writeTypedList(peers)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ dest.writeParcelableList(peers, flags)
+ } else {
+ dest.writeTypedList(peers)
+ }
}
private class ConfigProxyCreator : Parcelable.Creator<ConfigProxy> {
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 2792f749..004ebed1 100644
--- a/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt
+++ b/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.viewmodel
diff --git a/ui/src/main/java/com/wireguard/android/viewmodel/PeerProxy.kt b/ui/src/main/java/com/wireguard/android/viewmodel/PeerProxy.kt
index d1cb1046..e78d0826 100644
--- a/ui/src/main/java/com/wireguard/android/viewmodel/PeerProxy.kt
+++ b/ui/src/main/java/com/wireguard/android/viewmodel/PeerProxy.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.viewmodel
@@ -16,8 +16,6 @@ import com.wireguard.config.Attribute
import com.wireguard.config.BadConfigException
import com.wireguard.config.Peer
import java.lang.ref.WeakReference
-import java.util.ArrayList
-import java.util.LinkedHashSet
class PeerProxy : BaseObservable, Parcelable {
private val dnsRoutes: MutableList<String?> = ArrayList()
@@ -240,24 +238,32 @@ class PeerProxy : BaseObservable, Parcelable {
peerProxy.setTotalPeers(sender.size)
}
- override fun onItemRangeChanged(sender: ObservableList<PeerProxy?>,
- positionStart: Int, itemCount: Int) {
+ override fun onItemRangeChanged(
+ sender: ObservableList<PeerProxy?>,
+ positionStart: Int, itemCount: Int
+ ) {
// Do nothing.
}
- override fun onItemRangeInserted(sender: ObservableList<PeerProxy?>,
- positionStart: Int, itemCount: Int) {
+ override fun onItemRangeInserted(
+ sender: ObservableList<PeerProxy?>,
+ positionStart: Int, itemCount: Int
+ ) {
onChanged(sender)
}
- override fun onItemRangeMoved(sender: ObservableList<PeerProxy?>,
- fromPosition: Int, toPosition: Int,
- itemCount: Int) {
+ override fun onItemRangeMoved(
+ sender: ObservableList<PeerProxy?>,
+ fromPosition: Int, toPosition: Int,
+ itemCount: Int
+ ) {
// Do nothing.
}
- override fun onItemRangeRemoved(sender: ObservableList<PeerProxy?>,
- positionStart: Int, itemCount: Int) {
+ override fun onItemRangeRemoved(
+ sender: ObservableList<PeerProxy?>,
+ positionStart: Int, itemCount: Int
+ ) {
onChanged(sender)
}
}
@@ -276,12 +282,12 @@ class PeerProxy : BaseObservable, Parcelable {
@JvmField
val CREATOR: Parcelable.Creator<PeerProxy> = PeerProxyCreator()
private val IPV4_PUBLIC_NETWORKS = setOf(
- "0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
- "64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
- "172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
- "176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
- "192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
- "193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
+ "0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
+ "64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
+ "172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
+ "176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
+ "192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
+ "193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
)
private val IPV4_WILDCARD = setOf("0.0.0.0/0")
}
diff --git a/ui/src/main/java/com/wireguard/android/widget/KeyInputFilter.kt b/ui/src/main/java/com/wireguard/android/widget/KeyInputFilter.kt
index 6f941c86..8c822dcb 100644
--- a/ui/src/main/java/com/wireguard/android/widget/KeyInputFilter.kt
+++ b/ui/src/main/java/com/wireguard/android/widget/KeyInputFilter.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget
@@ -13,10 +13,12 @@ import com.wireguard.crypto.Key
* InputFilter for entering WireGuard private/public keys encoded with base64.
*/
class KeyInputFilter : InputFilter {
- override fun filter(source: CharSequence,
- sStart: Int, sEnd: Int,
- dest: Spanned,
- dStart: Int, dEnd: Int): CharSequence? {
+ override fun filter(
+ source: CharSequence,
+ sStart: Int, sEnd: Int,
+ dest: Spanned,
+ dStart: Int, dEnd: Int
+ ): CharSequence? {
var replacement: SpannableStringBuilder? = null
var rIndex = 0
val dLength = dest.length
@@ -26,8 +28,9 @@ class KeyInputFilter : InputFilter {
// Restrict characters to the base64 character set.
// Ensure adding this character does not push the length over the limit.
if ((dIndex + 1 < Key.Format.BASE64.length && isAllowed(c) ||
- dIndex + 1 == Key.Format.BASE64.length && c == '=') &&
- dLength + (sIndex - sStart) < Key.Format.BASE64.length) {
+ dIndex + 1 == Key.Format.BASE64.length && c == '=') &&
+ dLength + (sIndex - sStart) < Key.Format.BASE64.length
+ ) {
++rIndex
} else {
if (replacement == null) replacement = SpannableStringBuilder(source, sStart, sEnd)
diff --git a/ui/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.kt b/ui/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.kt
index 38aee443..91c7da0c 100644
--- a/ui/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.kt
+++ b/ui/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget
@@ -11,10 +11,10 @@ import android.widget.RelativeLayout
import com.wireguard.android.R
class MultiselectableRelativeLayout @JvmOverloads constructor(
- context: Context? = null,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = 0,
- defStyleRes: Int = 0
+ context: Context? = null,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+ defStyleRes: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr, defStyleRes) {
private var multiselected = false
diff --git a/ui/src/main/java/com/wireguard/android/widget/NameInputFilter.kt b/ui/src/main/java/com/wireguard/android/widget/NameInputFilter.kt
index a047dab4..e21ebaba 100644
--- a/ui/src/main/java/com/wireguard/android/widget/NameInputFilter.kt
+++ b/ui/src/main/java/com/wireguard/android/widget/NameInputFilter.kt
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget
@@ -13,10 +13,12 @@ import com.wireguard.android.backend.Tunnel
* InputFilter for entering WireGuard configuration names (Linux interface names).
*/
class NameInputFilter : InputFilter {
- override fun filter(source: CharSequence,
- sStart: Int, sEnd: Int,
- dest: Spanned,
- dStart: Int, dEnd: Int): CharSequence? {
+ override fun filter(
+ source: CharSequence,
+ sStart: Int, sEnd: Int,
+ dest: Spanned,
+ dStart: Int, dEnd: Int
+ ): CharSequence? {
var replacement: SpannableStringBuilder? = null
var rIndex = 0
val dLength = dest.length
@@ -26,7 +28,8 @@ class NameInputFilter : InputFilter {
// Restrict characters to those valid in interfaces.
// Ensure adding this character does not push the length over the limit.
if (dIndex < Tunnel.NAME_MAX_LENGTH && isAllowed(c) &&
- dLength + (sIndex - sStart) < Tunnel.NAME_MAX_LENGTH) {
+ dLength + (sIndex - sStart) < Tunnel.NAME_MAX_LENGTH
+ ) {
++rIndex
} else {
if (replacement == null) replacement = SpannableStringBuilder(source, sStart, sEnd)
diff --git a/ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt b/ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt
index 2ebf4fd8..0e2eeff1 100644
--- a/ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt
+++ b/ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt
@@ -1,6 +1,6 @@
/*
* Copyright © 2018 The Android Open Source Project
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget
@@ -35,10 +35,10 @@ class SlashDrawable(private val mDrawable: Drawable) : Drawable() {
val radiusX = scale(CORNER_RADIUS, width)
val radiusY = scale(CORNER_RADIUS, height)
updateRect(
- scale(LEFT, width),
- scale(TOP, height),
- scale(RIGHT, width),
- scale(TOP + mCurrentSlashLength, height)
+ scale(LEFT, width),
+ scale(TOP, height),
+ scale(RIGHT, width),
+ scale(TOP + mCurrentSlashLength, height)
)
mPath.reset()
// Draw the slash vertically
diff --git a/ui/src/main/java/com/wireguard/android/widget/ToggleSwitch.kt b/ui/src/main/java/com/wireguard/android/widget/ToggleSwitch.kt
index 0b5fa09f..0bde810d 100644
--- a/ui/src/main/java/com/wireguard/android/widget/ToggleSwitch.kt
+++ b/ui/src/main/java/com/wireguard/android/widget/ToggleSwitch.kt
@@ -1,6 +1,6 @@
/*
* Copyright © 2013 The Android Open Source Project
- * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget
@@ -8,9 +8,9 @@ package com.wireguard.android.widget
import android.content.Context
import android.os.Parcelable
import android.util.AttributeSet
-import android.widget.Switch
+import com.google.android.material.materialswitch.MaterialSwitch
-class ToggleSwitch @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) : Switch(context, attrs) {
+class ToggleSwitch @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : MaterialSwitch(context, attrs) {
private var isRestoringState = false
private var listener: OnBeforeCheckedChangeListener? = null
override fun onRestoreInstanceState(state: Parcelable) {
diff --git a/ui/src/main/java/com/wireguard/android/widget/TvCardView.kt b/ui/src/main/java/com/wireguard/android/widget/TvCardView.kt
new file mode 100644
index 00000000..9f7d7011
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/widget/TvCardView.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import com.google.android.material.card.MaterialCardView
+import com.wireguard.android.R
+
+class TvCardView(context: Context?, attrs: AttributeSet?) : MaterialCardView(context, attrs) {
+ var isUp: Boolean = false
+ set(value) {
+ field = value
+ refreshDrawableState()
+ }
+ var isDeleting: Boolean = false
+ set(value) {
+ field = value
+ refreshDrawableState()
+ }
+
+ override fun onCreateDrawableState(extraSpace: Int): IntArray {
+ if (isUp || isDeleting) {
+ val drawableState = super.onCreateDrawableState(extraSpace + (if (isUp) 1 else 0) + (if (isDeleting) 1 else 0))
+ if (isUp) {
+ View.mergeDrawableStates(drawableState, STATE_IS_UP)
+ }
+ if (isDeleting) {
+ View.mergeDrawableStates(drawableState, STATE_IS_DELETING)
+ }
+ return drawableState
+ }
+ return super.onCreateDrawableState(extraSpace)
+ }
+
+ companion object {
+ private val STATE_IS_UP = intArrayOf(R.attr.state_isUp)
+ private val STATE_IS_DELETING = intArrayOf(R.attr.state_isDeleting)
+ }
+} \ No newline at end of file
diff --git a/ui/src/main/res/color/tv_list_item_tint.xml b/ui/src/main/res/color/tv_list_item_tint.xml
new file mode 100644
index 00000000..08e833a7
--- /dev/null
+++ b/ui/src/main/res/color/tv_list_item_tint.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item app:state_isUp="true" app:state_isDeleting="false" android:color="?attr/colorPrimaryInverse" />
+ <item android:state_focused="true" app:state_isDeleting="true" android:color="?attr/colorErrorContainer" />
+ <item android:color="?attr/colorOnSurfaceInverse" />
+</selector> \ No newline at end of file
diff --git a/ui/src/main/res/drawable/ic_action_add_white.xml b/ui/src/main/res/drawable/ic_action_add_white.xml
index 18fe19db..1ea440df 100644
--- a/ui/src/main/res/drawable/ic_action_add_white.xml
+++ b/ui/src/main/res/drawable/ic_action_add_white.xml
@@ -1,9 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0">
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
<path
- android:fillColor="?attr/colorOnSecondary"
+ android:fillColor="#FFFFFFFF"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</vector>
diff --git a/ui/src/main/res/drawable/ic_action_delete.xml b/ui/src/main/res/drawable/ic_action_delete.xml
index 51517c42..73eb7352 100644
--- a/ui/src/main/res/drawable/ic_action_delete.xml
+++ b/ui/src/main/res/drawable/ic_action_delete.xml
@@ -1,9 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
+ android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
- android:fillColor="?attr/colorOnPrimary"
+ android:fillColor="#FFFFFFFF"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
</vector>
diff --git a/ui/src/main/res/drawable/ic_action_edit.xml b/ui/src/main/res/drawable/ic_action_edit.xml
index 8d8acf85..d40c78d9 100644
--- a/ui/src/main/res/drawable/ic_action_edit.xml
+++ b/ui/src/main/res/drawable/ic_action_edit.xml
@@ -1,9 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
+ android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
- android:fillColor="?attr/colorOnPrimary"
+ android:fillColor="#FFFFFFFF"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
</vector>
diff --git a/ui/src/main/res/drawable/ic_action_edit_white.xml b/ui/src/main/res/drawable/ic_action_edit_white.xml
deleted file mode 100644
index 212fc72c..00000000
--- a/ui/src/main/res/drawable/ic_action_edit_white.xml
+++ /dev/null
@@ -1,9 +0,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="?attr/colorOnSecondary"
- android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
-</vector>
diff --git a/ui/src/main/res/drawable/ic_action_generate.xml b/ui/src/main/res/drawable/ic_action_generate.xml
index 7324eae4..51d26aed 100644
--- a/ui/src/main/res/drawable/ic_action_generate.xml
+++ b/ui/src/main/res/drawable/ic_action_generate.xml
@@ -1,9 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
+ android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
- android:fillColor="#FF000000"
+ android:fillColor="#FFFFFFFF"
android:pathData="M12,4L12,1L8,5l4,4L12,6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z" />
</vector>
diff --git a/ui/src/main/res/drawable/ic_action_open_white.xml b/ui/src/main/res/drawable/ic_action_open.xml
index 91075680..c9fd6fba 100644
--- a/ui/src/main/res/drawable/ic_action_open_white.xml
+++ b/ui/src/main/res/drawable/ic_action_open.xml
@@ -1,9 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
+ android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
- android:fillColor="?attr/colorOnSecondary"
+ android:fillColor="#FFFFFFFF"
android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6L6,2zM13,9L13,3.5L18.5,9L13,9z" />
</vector>
diff --git a/ui/src/main/res/drawable/ic_action_save.xml b/ui/src/main/res/drawable/ic_action_save.xml
index ed98e85a..6e618edb 100644
--- a/ui/src/main/res/drawable/ic_action_save.xml
+++ b/ui/src/main/res/drawable/ic_action_save.xml
@@ -2,9 +2,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
+ android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
- android:fillColor="?attr/colorOnPrimary"
+ android:fillColor="#FFFFFFFF"
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z" />
</vector>
diff --git a/ui/src/main/res/drawable/ic_action_scan_qr_code_white.xml b/ui/src/main/res/drawable/ic_action_scan_qr_code.xml
index a5ede695..4522ae46 100644
--- a/ui/src/main/res/drawable/ic_action_scan_qr_code_white.xml
+++ b/ui/src/main/res/drawable/ic_action_scan_qr_code.xml
@@ -1,9 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
+ android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
- android:fillColor="?attr/colorOnSecondary"
+ android:fillColor="#FFFFFFFF"
android:pathData="M4,4H10V10H4V4M20,4V10H14V4H20M14,15H16V13H14V11H16V13H18V11H20V13H18V15H20V18H18V20H16V18H13V20H11V16H14V15M16,15V18H18V15H16M4,20V14H10V20H4M6,6V8H8V6H6M16,6V8H18V6H16M6,16V18H8V16H6M4,11H6V13H4V11M9,11H13V15H11V13H9V11M11,6H13V10H11V6M2,2V6H0V2A2,2 0 0,1 2,0H6V2H2M22,0A2,2 0 0,1 24,2V6H22V2H18V0H22M2,18V22H6V24H2A2,2 0 0,1 0,22V18H2M22,22V18H24V22A2,2 0 0,1 22,24H18V22H22Z" />
</vector>
diff --git a/ui/src/main/res/drawable/ic_action_select_all.xml b/ui/src/main/res/drawable/ic_action_select_all.xml
index 43f5f15e..490ca715 100644
--- a/ui/src/main/res/drawable/ic_action_select_all.xml
+++ b/ui/src/main/res/drawable/ic_action_select_all.xml
@@ -2,9 +2,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
+ android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
- android:fillColor="?attr/colorOnPrimary"
+ android:fillColor="#FFFFFFFF"
android:pathData="M3 5L5 5 5 3C3.9 3 3 3.9 3 5Zm0 8l2 0 0 -2 -2 0 0 2zm4 8l2 0 0 -2 -2 0 0 2zM3 9L5 9 5 7 3 7 3 9Zm10 -6l-2 0 0 2 2 0 0 -2zm6 0l0 2 2 0C21 3.9 20.1 3 19 3ZM5 21L5 19 3 19c0 1.1 0.9 2 2 2zm-2 -4l2 0 0 -2 -2 0 0 2zM9 3L7 3 7 5 9 5 9 3Zm2 18l2 0 0 -2 -2 0 0 2zm8 -8l2 0 0 -2 -2 0 0 2zm0 8c1.1 0 2 -0.9 2 -2l-2 0 0 2zm0 -12l2 0 0 -2 -2 0 0 2zm0 8l2 0 0 -2 -2 0 0 2zm-4 4l2 0 0 -2 -2 0 0 2zm0 -16l2 0 0 -2 -2 0 0 2zM7 17L17 17 17 7 7 7 7 17Zm2 -8l6 0 0 6 -6 0 0 -6z" />
</vector>
diff --git a/ui/src/main/res/drawable/ic_action_share_white.xml b/ui/src/main/res/drawable/ic_action_share_white.xml
index 70843cf3..04ee5b74 100644
--- a/ui/src/main/res/drawable/ic_action_share_white.xml
+++ b/ui/src/main/res/drawable/ic_action_share_white.xml
@@ -1,6 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
+ android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
diff --git a/ui/src/main/res/drawable/ic_arrow_back.xml b/ui/src/main/res/drawable/ic_arrow_back.xml
index 0d549893..0df5dc6c 100644
--- a/ui/src/main/res/drawable/ic_arrow_back.xml
+++ b/ui/src/main/res/drawable/ic_arrow_back.xml
@@ -1,14 +1,15 @@
<!--
- ~ Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ ~ Copyright © 2017-2023 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:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
- android:fillColor="#000000"
+ android:fillColor="#FFFFFFFF"
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>
diff --git a/ui/src/main/res/drawable/ic_settings.xml b/ui/src/main/res/drawable/ic_settings.xml
index 6d1cfa71..af9f2634 100644
--- a/ui/src/main/res/drawable/ic_settings.xml
+++ b/ui/src/main/res/drawable/ic_settings.xml
@@ -1,9 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
+ android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
- android:fillColor="?android:attr/colorForeground"
+ android:fillColor="#FFFFFFFF"
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
</vector>
diff --git a/ui/src/main/res/drawable/list_item_background.xml b/ui/src/main/res/drawable/list_item_background.xml
index d517bbb6..16714e7b 100644
--- a/ui/src/main/res/drawable/list_item_background.xml
+++ b/ui/src/main/res/drawable/list_item_background.xml
@@ -6,7 +6,10 @@
<item
android:state_activated="true"
app:state_multiselected="true">
- <color android:color="?attr/colorMultiselectActiveBackground" />
+ <color android:color="?attr/colorSurfaceVariant" />
+ </item>
+ <item android:state_activated="true">
+ <color android:color="?attr/colorControlHighlight" />
</item>
</selector>
</item>
diff --git a/ui/src/main/res/drawable/tv_logo_banner.xml b/ui/src/main/res/drawable/tv_logo_banner.xml
index 646967d6..734702f3 100644
--- a/ui/src/main/res/drawable/tv_logo_banner.xml
+++ b/ui/src/main/res/drawable/tv_logo_banner.xml
@@ -1,5 +1,5 @@
<!--
- ~ Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ ~ Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
~ SPDX-License-Identifier: Apache-2.0
-->
diff --git a/ui/src/main/res/layout/add_tunnels_bottom_sheet.xml b/ui/src/main/res/layout/add_tunnels_bottom_sheet.xml
index 68f186b6..3a5ddf0a 100644
--- a/ui/src/main/res/layout/add_tunnels_bottom_sheet.xml
+++ b/ui/src/main/res/layout/add_tunnels_bottom_sheet.xml
@@ -8,7 +8,7 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/create_from_file"
- style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
+ style="@style/Widget.Material3.Button.TextButton.Icon"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:layout_marginStart="@dimen/normal_margin"
@@ -20,7 +20,7 @@
android:text="@string/create_from_file"
android:textAlignment="viewStart"
android:textColor="?attr/colorOnSurface"
- app:icon="@drawable/ic_action_open_white"
+ app:icon="@drawable/ic_action_open"
app:iconPadding="@dimen/bottom_sheet_icon_padding"
app:iconTint="?attr/colorSecondary"
app:layout_constraintBottom_toTopOf="@+id/create_from_qrcode"
@@ -31,7 +31,7 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/create_from_qrcode"
- style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
+ style="@style/Widget.Material3.Button.TextButton.Icon"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:layout_marginStart="@dimen/normal_margin"
@@ -44,7 +44,7 @@
android:text="@string/create_from_qr_code"
android:textAlignment="viewStart"
android:textColor="?attr/colorOnSurface"
- app:icon="@drawable/ic_action_scan_qr_code_white"
+ app:icon="@drawable/ic_action_scan_qr_code"
app:iconPadding="@dimen/bottom_sheet_icon_padding"
app:iconTint="?attr/colorSecondary"
app:layout_constraintBottom_toBottomOf="@+id/create_empty"
@@ -55,7 +55,7 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/create_empty"
- style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
+ style="@style/Widget.Material3.Button.TextButton.Icon"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:layout_marginStart="@dimen/normal_margin"
diff --git a/ui/src/main/res/layout/app_list_dialog_fragment.xml b/ui/src/main/res/layout/app_list_dialog_fragment.xml
index 4503de15..4000c64b 100644
--- a/ui/src/main/res/layout/app_list_dialog_fragment.xml
+++ b/ui/src/main/res/layout/app_list_dialog_fragment.xml
@@ -25,7 +25,7 @@
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
- style="@style/Widget.MaterialComponents.TabLayout.Colored"
+ style="@style/Widget.Material3.TabLayout.OnSurface"
android:layout_width="match_parent"
android:layout_height="wrap_content">
diff --git a/ui/src/main/res/layout/app_list_item.xml b/ui/src/main/res/layout/app_list_item.xml
index e4e4483c..d6a4b715 100644
--- a/ui/src/main/res/layout/app_list_item.xml
+++ b/ui/src/main/res/layout/app_list_item.xml
@@ -39,7 +39,6 @@
<TextView
android:id="@+id/app_name"
- style="?android:attr/textAppearanceMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
@@ -48,6 +47,7 @@
android:ellipsize="end"
android:maxLines="1"
android:text="@{key}"
+ android:textAppearance="?attr/textAppearanceBodyLarge"
tools:text="@tools:sample/full_names" />
<CheckBox
diff --git a/ui/src/main/res/layout/config_naming_dialog_fragment.xml b/ui/src/main/res/layout/config_naming_dialog_fragment.xml
index 0fd88c6c..63d3141d 100644
--- a/ui/src/main/res/layout/config_naming_dialog_fragment.xml
+++ b/ui/src/main/res/layout/config_naming_dialog_fragment.xml
@@ -24,7 +24,10 @@
android:hint="@string/tunnel_name"
android:imeOptions="actionDone"
android:inputType="textNoSuggestions|textVisiblePassword"
- app:filter="@{NameInputFilter.newInstance()}" />
+ app:filter="@{NameInputFilter.newInstance()}">
+
+ <requestFocus />
+ </com.google.android.material.textfield.TextInputEditText>
</com.google.android.material.textfield.TextInputLayout>
diff --git a/ui/src/main/res/layout/log_viewer_activity.xml b/ui/src/main/res/layout/log_viewer_activity.xml
index c3780470..2a377a15 100644
--- a/ui/src/main/res/layout/log_viewer_activity.xml
+++ b/ui/src/main/res/layout/log_viewer_activity.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ ~ Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
~ SPDX-License-Identifier: Apache-2.0
-->
diff --git a/ui/src/main/res/layout/log_viewer_entry.xml b/ui/src/main/res/layout/log_viewer_entry.xml
index 73680f0c..762d2ddf 100644
--- a/ui/src/main/res/layout/log_viewer_entry.xml
+++ b/ui/src/main/res/layout/log_viewer_entry.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ ~ Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
~ SPDX-License-Identifier: Apache-2.0
-->
@@ -12,20 +12,19 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/log_date"
- style="@style/TextAppearance.MaterialComponents.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?android:attr/textColorPrimary"
- android:textSize="10sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Fri Mar 13 10:17:37 GMT+05:30 2020" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/log_msg"
- style="@style/TextAppearance.MaterialComponents.Caption"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?android:attr/textColorPrimary"
app:layout_constraintTop_toBottomOf="@id/log_date"
tools:text="FATAL EXCEPTION: Thread-2" />
diff --git a/ui/src/main/res/layout/tunnel_detail_fragment.xml b/ui/src/main/res/layout/tunnel_detail_fragment.xml
index 8e34f082..332df04a 100644
--- a/ui/src/main/res/layout/tunnel_detail_fragment.xml
+++ b/ui/src/main/res/layout/tunnel_detail_fragment.xml
@@ -25,7 +25,7 @@
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="?attr/colorBackground"
+ android:background="?attr/colorSurface"
android:clickable="true"
android:focusable="true">
@@ -50,10 +50,10 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/interface_title"
- style="@style/SectionText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/interface_title"
+ android:textAppearance="?attr/textAppearanceTitleMedium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@@ -65,13 +65,13 @@
android:nextFocusDown="@id/interface_name_text"
android:nextFocusForward="@id/interface_name_text"
app:checked="@{tunnel.state == State.UP}"
- app:layout_constraintBaseline_toBaselineOf="@+id/interface_title"
+ app:layout_constraintBaseline_toBottomOf="@+id/interface_title"
app:layout_constraintEnd_toEndOf="parent"
app:onBeforeCheckedChanged="@{fragment::setTunnelState}" />
<TextView
android:id="@+id/interface_name_label"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/interface_name_text"
@@ -81,8 +81,7 @@
<TextView
android:id="@+id/interface_name_text"
- style="@style/DetailText"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/name"
android:nextFocusUp="@id/tunnel_switch"
@@ -90,13 +89,14 @@
android:nextFocusForward="@id/public_key_text"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{tunnel.name}"
+ android:textAppearance="?attr/textAppearanceBodyLarge"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/interface_name_label"
tools:text="wg0" />
<TextView
android:id="@+id/public_key_label"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/public_key_text"
@@ -106,8 +106,7 @@
<TextView
android:id="@+id/public_key_text"
- style="@style/DetailText"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/public_key"
android:ellipsize="end"
@@ -118,13 +117,14 @@
android:onClick="@{ClipboardUtils::copyTextView}"
android:singleLine="true"
android:text="@{config.interface.keyPair.publicKey.toBase64}"
+ android:textAppearance="?attr/textAppearanceBodyLarge"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/public_key_label"
tools:text="wOs2eguFEohqIZxlSJ1CAT9584tc6ejj9hfGFsoBVkA=" />
<TextView
android:id="@+id/addresses_label"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/addresses_text"
@@ -135,8 +135,7 @@
<TextView
android:id="@+id/addresses_text"
- style="@style/DetailText"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/addresses"
android:nextFocusUp="@id/public_key_text"
@@ -144,6 +143,7 @@
android:nextFocusForward="@id/dns_servers_text"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{config.interface.addresses}"
+ android:textAppearance="?attr/textAppearanceBodyLarge"
android:visibility="@{config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/addresses_label"
@@ -151,7 +151,7 @@
<TextView
android:id="@+id/dns_servers_label"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/dns_servers_text"
@@ -162,8 +162,7 @@
<TextView
android:id="@+id/dns_servers_text"
- style="@style/DetailText"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/dns_servers"
android:nextFocusUp="@id/addresses_text"
@@ -171,6 +170,7 @@
android:nextFocusForward="@id/dns_search_domains_text"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{config.interface.dnsServers}"
+ android:textAppearance="?attr/textAppearanceBodyLarge"
android:visibility="@{config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dns_servers_label"
@@ -178,7 +178,7 @@
<TextView
android:id="@+id/dns_search_domains_label"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/dns_search_domain_text"
@@ -189,8 +189,7 @@
<TextView
android:id="@+id/dns_search_domains_text"
- style="@style/DetailText"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/dns_search_domains"
android:nextFocusUp="@id/dns_servers_text"
@@ -198,6 +197,7 @@
android:nextFocusForward="@id/listen_port_text"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{config.interface.dnsSearchDomains}"
+ android:textAppearance="?attr/textAppearanceBodyLarge"
android:visibility="@{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"
@@ -218,7 +218,6 @@
<TextView
android:id="@+id/listen_port_text"
- style="@style/DetailText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:contentDescription="@string/listen_port"
@@ -228,6 +227,7 @@
android:nextFocusForward="@id/mtu_text"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{config.interface.listenPort}"
+ android:textAppearance="?attr/textAppearanceBodyLarge"
android:visibility="@{!config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintEnd_toStartOf="@id/mtu_label"
app:layout_constraintHorizontal_weight="0.5"
@@ -251,7 +251,6 @@
<TextView
android:id="@+id/mtu_text"
- style="@style/DetailText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:contentDescription="@string/mtu"
@@ -260,6 +259,7 @@
android:nextFocusForward="@id/applications_text"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{config.interface.mtu}"
+ android:textAppearance="?attr/textAppearanceBodyLarge"
android:visibility="@{!config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="0.5"
@@ -268,21 +268,27 @@
app:layout_constraintTop_toBottomOf="@+id/mtu_label"
tools:text="1500" />
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/listen_port_mtu_barrier"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:barrierDirection="bottom"
+ app:constraint_referenced_ids="listen_port_text,mtu_text" />
+
<TextView
android:id="@+id/applications_label"
- android:layout_width="match_parent"
+ 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}"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/listen_port_text" />
+ app:layout_constraintTop_toBottomOf="@+id/listen_port_mtu_barrier" />
<TextView
android:id="@+id/applications_text"
- style="@style/DetailText"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/applications"
android:nextFocusUp="@id/mtu_text"
@@ -290,6 +296,7 @@
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:textAppearance="?attr/textAppearanceBodyLarge"
android:visibility="@{config.interface.includedApplications.isEmpty() &amp;&amp; config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/applications_label"
@@ -311,4 +318,4 @@
tools:ignore="UselessLeaf" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
-</layout>
+</layout> \ No newline at end of file
diff --git a/ui/src/main/res/layout/tunnel_detail_peer.xml b/ui/src/main/res/layout/tunnel_detail_peer.xml
index 0fbee8f1..25081cea 100644
--- a/ui/src/main/res/layout/tunnel_detail_peer.xml
+++ b/ui/src/main/res/layout/tunnel_detail_peer.xml
@@ -23,16 +23,16 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/peer_title"
- style="@style/SectionText"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/peer"
+ android:textAppearance="?attr/textAppearanceTitleMedium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/public_key_label"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/public_key_text"
@@ -42,8 +42,7 @@
<TextView
android:id="@+id/public_key_text"
- style="@style/DetailText"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/public_key"
android:ellipsize="end"
@@ -53,13 +52,14 @@
android:onClick="@{ClipboardUtils::copyTextView}"
android:singleLine="true"
android:text="@{item.publicKey.toBase64}"
+ android:textAppearance="?attr/textAppearanceBodyLarge"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/public_key_label"
tools:text="wOs2eguFEohqIZxlSJ1CAT9584tc6ejj9hfGFsoBVkA=" />
<TextView
android:id="@+id/pre_shared_key_label"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/pre_shared_key_text"
@@ -70,8 +70,7 @@
<TextView
android:id="@+id/pre_shared_key_text"
- style="@style/DetailText"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/pre_shared_key"
android:ellipsize="end"
@@ -81,6 +80,7 @@
android:nextFocusForward="@id/allowed_ips_text"
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}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pre_shared_key_label"
@@ -88,7 +88,7 @@
<TextView
android:id="@+id/allowed_ips_label"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/allowed_ips_text"
@@ -99,8 +99,7 @@
<TextView
android:id="@+id/allowed_ips_text"
- style="@style/DetailText"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/allowed_ips"
android:nextFocusUp="@id/pre_shared_key_text"
@@ -108,6 +107,7 @@
android:nextFocusForward="@id/endpoint_text"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{item.allowedIps}"
+ android:textAppearance="?attr/textAppearanceBodyLarge"
android:visibility="@{item.allowedIps.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/allowed_ips_label"
@@ -115,7 +115,7 @@
<TextView
android:id="@+id/endpoint_label"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/endpoint_text"
@@ -126,8 +126,7 @@
<TextView
android:id="@+id/endpoint_text"
- style="@style/DetailText"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/endpoint"
android:nextFocusUp="@id/allowed_ips_text"
@@ -135,6 +134,7 @@
android:nextFocusForward="@id/persistent_keepalive_text"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{item.endpoint}"
+ android:textAppearance="?attr/textAppearanceBodyLarge"
android:visibility="@{!item.endpoint.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/endpoint_label"
@@ -142,7 +142,7 @@
<TextView
android:id="@+id/persistent_keepalive_label"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/persistent_keepalive_text"
@@ -153,8 +153,7 @@
<TextView
android:id="@+id/persistent_keepalive_text"
- style="@style/DetailText"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/persistent_keepalive"
android:nextFocusUp="@id/endpoint_text"
@@ -162,6 +161,7 @@
android:nextFocusForward="@id/transfer_text"
android:onClick="@{ClipboardUtils::copyTextView}"
android:text="@{@plurals/persistent_keepalive_seconds_unit(item.persistentKeepalive.orElse(0), item.persistentKeepalive.orElse(0))}"
+ android:textAppearance="?attr/textAppearanceBodyLarge"
android:visibility="@{!item.persistentKeepalive.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/persistent_keepalive_label"
@@ -169,9 +169,9 @@
<TextView
android:id="@+id/transfer_label"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_below="@+id/endpoint_text"
+ android:layout_below="@+id/persistent_keepalive_text"
android:layout_marginTop="8dp"
android:labelFor="@+id/transfer_text"
android:text="@string/transfer"
@@ -182,18 +182,46 @@
<TextView
android:id="@+id/transfer_text"
- style="@style/DetailText"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/transfer_label"
android:contentDescription="@string/transfer"
android:nextFocusUp="@id/persistent_keepalive_text"
android:onClick="@{ClipboardUtils::copyTextView}"
+ android:textAppearance="?attr/textAppearanceBodyLarge"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/transfer_label"
tools:text="1024 MB"
tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/latest_handshake_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/transfer_text"
+ android:layout_marginTop="8dp"
+ android:labelFor="@+id/latest_handshake_text"
+ android:text="@string/latest_handshake"
+ android:visibility="gone"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/transfer_text"
+ tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/latest_handshake_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/latest_handshake_label"
+ android:contentDescription="@string/latest_handshake"
+ android:nextFocusUp="@id/transfer_text"
+ android:onClick="@{ClipboardUtils::copyTextView}"
+ android:textAppearance="?attr/textAppearanceBodyLarge"
+ android:visibility="gone"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/latest_handshake_label"
+ tools:text="4 minutes, 27 seconds ago"
+ tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</layout>
diff --git a/ui/src/main/res/layout/tunnel_editor_fragment.xml b/ui/src/main/res/layout/tunnel_editor_fragment.xml
index 59572b32..0350486b 100644
--- a/ui/src/main/res/layout/tunnel_editor_fragment.xml
+++ b/ui/src/main/res/layout/tunnel_editor_fragment.xml
@@ -28,7 +28,7 @@
android:id="@+id/main_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:background="?attr/colorBackground">
+ android:background="?attr/colorSurface">
<ScrollView
android:layout_width="match_parent"
@@ -53,12 +53,12 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/interface_title"
- style="@style/SectionText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:layout_marginTop="32dp"
android:text="@string/interface_title"
+ android:textAppearance="?attr/textAppearanceTitleMedium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@@ -243,7 +243,7 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/set_excluded_applications"
- style="@style/Widget.MaterialComponents.Button.TextButton"
+ style="@style/Widget.Material3.Button.TextButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="4dp"
@@ -275,7 +275,7 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/add_peer_button"
- style="@style/Widget.MaterialComponents.Button.TextButton"
+ style="@style/Widget.Material3.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
diff --git a/ui/src/main/res/layout/tunnel_editor_peer.xml b/ui/src/main/res/layout/tunnel_editor_peer.xml
index 27a6d125..305fa927 100644
--- a/ui/src/main/res/layout/tunnel_editor_peer.xml
+++ b/ui/src/main/res/layout/tunnel_editor_peer.xml
@@ -37,11 +37,11 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/peer_title"
- style="@style/SectionText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/peer"
+ android:textAppearance="?attr/textAppearanceTitleMedium"
app:layout_constraintBottom_toTopOf="@+id/public_key_label_layout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
diff --git a/ui/src/main/res/layout/tunnel_list_fragment.xml b/ui/src/main/res/layout/tunnel_list_fragment.xml
index 436b63da..2ee2ff38 100644
--- a/ui/src/main/res/layout/tunnel_list_fragment.xml
+++ b/ui/src/main/res/layout/tunnel_list_fragment.xml
@@ -24,7 +24,7 @@
android:id="@+id/main_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="?attr/colorBackground"
+ android:background="?attr/colorSurface"
android:clipChildren="false">
<androidx.recyclerview.widget.RecyclerView
@@ -63,6 +63,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
+ android:layout_marginStart="@dimen/tunnel_list_placeholder_margin"
+ android:layout_marginEnd="@dimen/tunnel_list_placeholder_margin"
android:text="@string/tunnel_list_placeholder"
android:textSize="20sp" />
</LinearLayout>
diff --git a/ui/src/main/res/layout/tunnel_list_item.xml b/ui/src/main/res/layout/tunnel_list_item.xml
index 9c9517a7..3d5b02d1 100644
--- a/ui/src/main/res/layout/tunnel_list_item.xml
+++ b/ui/src/main/res/layout/tunnel_list_item.xml
@@ -34,26 +34,27 @@
android:descendantFocusability="beforeDescendants"
android:focusable="true"
android:nextFocusRight="@+id/tunnel_switch"
- android:padding="16dp">
+ android:paddingHorizontal="16dp"
+ android:paddingVertical="8dp">
<TextView
android:id="@+id/tunnel_name"
- style="?android:attr/textAppearanceMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
- android:layout_alignParentTop="true"
+ android:layout_centerVertical="true"
android:ellipsize="end"
android:maxLines="1"
android:text="@{key}"
+ android:textAppearance="?attr/textAppearanceBodyLarge"
tools:text="@sample/interface_names.json/names/names/name" />
<com.wireguard.android.widget.ToggleSwitch
android:id="@+id/tunnel_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_alignBaseline="@+id/tunnel_name"
android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
android:nextFocusLeft="@+id/tunnel_list_item"
app:checked="@{item.state == State.UP}"
app:onBeforeCheckedChanged="@{fragment::setTunnelState}"
diff --git a/ui/src/main/res/layout/tv_activity.xml b/ui/src/main/res/layout/tv_activity.xml
index 16207b15..cfa1f6ba 100644
--- a/ui/src/main/res/layout/tv_activity.xml
+++ b/ui/src/main/res/layout/tv_activity.xml
@@ -82,12 +82,12 @@
<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:textAppearance="?attr/textAppearanceTitleLarge"
android:visibility="@{filesRoot.isEmpty ? View.GONE : View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/banner_logo"
@@ -113,11 +113,11 @@
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:textAppearance="?attr/textAppearanceHeadlineSmall"
android:visibility="@{(filesRoot.isEmpty &amp;&amp; tunnels.isEmpty) ? View.VISIBLE : View.GONE}"
app:layout_constraintBottom_toTopOf="@id/delete_button"
app:layout_constraintEnd_toEndOf="parent"
@@ -127,7 +127,7 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/import_button"
- style="@style/Widget.MaterialComponents.Button"
+ style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
@@ -135,13 +135,12 @@
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"
+ style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
@@ -149,7 +148,6 @@
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>
diff --git a/ui/src/main/res/layout/tv_file_list_item.xml b/ui/src/main/res/layout/tv_file_list_item.xml
index 270a2531..d5afa0a4 100644
--- a/ui/src/main/res/layout/tv_file_list_item.xml
+++ b/ui/src/main/res/layout/tv_file_list_item.xml
@@ -21,7 +21,6 @@
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">
@@ -31,11 +30,10 @@
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"
+ android:textAppearance="?attr/textAppearanceTitleLarge"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
diff --git a/ui/src/main/res/layout/tv_tunnel_list_item.xml b/ui/src/main/res/layout/tv_tunnel_list_item.xml
index 15615536..2d6039f1 100644
--- a/ui/src/main/res/layout/tv_tunnel_list_item.xml
+++ b/ui/src/main/res/layout/tv_tunnel_list_item.xml
@@ -28,16 +28,18 @@
type="com.wireguard.android.model.ObservableTunnel" />
</data>
- <com.google.android.material.card.MaterialCardView
+ <com.wireguard.android.widget.TvCardView
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:backgroundTint="@color/tv_list_item_tint"
android:checkable="true"
android:focusable="true"
- app:contentPadding="8dp">
+ app:contentPadding="8dp"
+ app:isDeleting="@{isDeleting}"
+ app:isUp="@{item.state == State.UP}">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
@@ -45,20 +47,19 @@
<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"
+ android:textAppearance="?attr/textAppearanceTitleLarge"
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:textAppearance="?attr/textAppearanceBodyLarge"
android:visibility="@{isDeleting ? View.GONE : View.VISIBLE}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
@@ -76,6 +77,6 @@
</androidx.constraintlayout.widget.ConstraintLayout>
- </com.google.android.material.card.MaterialCardView>
+ </com.wireguard.android.widget.TvCardView>
</layout>
diff --git a/ui/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/ui/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index a8a8fa55..5c84730c 100644
--- a/ui/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/ui/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
+ <monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
diff --git a/ui/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/ui/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index a8a8fa55..5c84730c 100644
--- a/ui/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/ui/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
+ <monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
diff --git a/ui/src/main/res/resources.properties b/ui/src/main/res/resources.properties
new file mode 100644
index 00000000..467b3efe
--- /dev/null
+++ b/ui/src/main/res/resources.properties
@@ -0,0 +1 @@
+unqualifiedResLocale=en-US
diff --git a/ui/src/main/res/values-ar-rSA/strings.xml b/ui/src/main/res/values-ar-rSA/strings.xml
index 42499da8..73f6111a 100644
--- a/ui/src/main/res/values-ar-rSA/strings.xml
+++ b/ui/src/main/res/values-ar-rSA/strings.xml
@@ -1,19 +1,311 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
+ <plurals name="delete_error">
+ <item quantity="zero">غير قادر على حذف %d النفق: %s</item>
+ <item quantity="one">غير قادر على حذف %d النفق: %s</item>
+ <item quantity="two">غير قادر على حذف %d النفق: %s</item>
+ <item quantity="few">غير قادر على حذف %d أنفاق: %s</item>
+ <item quantity="many">غير قادر على حذف %d أنفاق: %s</item>
+ <item quantity="other">غير قادر على حذف %d أنفاق: %s</item>
+ </plurals>
<plurals name="delete_success">
- <item quantity="zero">تم حذف نفق %d بنجاح</item>
- <item quantity="one">تم حذف %d نفق بنجاح</item>
- <item quantity="two">تم حذف %d نفق بنجاح</item>
- <item quantity="few">تم حذف %d نفق بنجاح</item>
- <item quantity="many">تم حذف %d نفق بنجاح</item>
- <item quantity="other">تم حذف %d انفاق بنجاح</item>
+ <item quantity="zero">تم حذف %d من الأنفاق بنجاح</item>
+ <item quantity="one">تم حذف %d من الأنفاق بنجاح</item>
+ <item quantity="two">تم حذف %d من الأنفاق بنجاح</item>
+ <item quantity="few">تم حذف %d من الأنفاق بنجاح</item>
+ <item quantity="many">تم حذف %d من الأنفاق بنجاح</item>
+ <item quantity="other">تم حذف %d من الأنفاق بنجاح</item>
</plurals>
<plurals name="delete_title">
- <item quantity="zero">%d نفق محدد</item>
- <item quantity="one">%d نفق محدد</item>
- <item quantity="two">%d نفق محدد</item>
- <item quantity="few">%d عنصر محدد</item>
- <item quantity="many">%d عنصر محدد</item>
- <item quantity="other">%d عنصر محدد</item>
+ <item quantity="zero">تم تحديد %d من الأنفاق</item>
+ <item quantity="one">تم تحديد %d من الأنفاق</item>
+ <item quantity="two">تم تحديد %d من الأنفاق</item>
+ <item quantity="few">تم تحديد %d من الأنفاق</item>
+ <item quantity="many">تم تحديد %d من الأنفاق</item>
+ <item quantity="other">تم تحديد %d من الأنفاق</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="zero">تم استيراد %1$d من %2$d من الأنفاق</item>
+ <item quantity="one">تم استيراد %1$d من %2$d من الأنفاق</item>
+ <item quantity="two">تم استيراد %1$d من %2$d من الأنفاق</item>
+ <item quantity="few">تم استيراد %1$d من %2$d من الأنفاق</item>
+ <item quantity="many">تم استيراد %1$d من %2$d من الأنفاق</item>
+ <item quantity="other">تم استيراد %1$d من %2$d من الأنفاق</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="zero">تم استيراد %d نفق</item>
+ <item quantity="one">تم استيراد %d نفق</item>
+ <item quantity="two">تم استيراد نفقين</item>
+ <item quantity="few">تم استيراد %d أنفاق</item>
+ <item quantity="many">تم استيراد %d أنفاق</item>
+ <item quantity="other">تم استيراد %d أنفاق</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="zero">%d تطبيقات مستبعدة</item>
+ <item quantity="one">%d تطبيق مستبعد</item>
+ <item quantity="two">تطبيقين مستبعدين</item>
+ <item quantity="few">%d تطبيقات مستبعدة</item>
+ <item quantity="many">%d تطبيقات مستبعدة</item>
+ <item quantity="other">%d تطبيقات مستبعدة</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="zero">%d تطبيق مضمن</item>
+ <item quantity="one">%d تطبيق مضمن</item>
+ <item quantity="two">تطبيقين مضمننين</item>
+ <item quantity="few">%d تطبيقات مضمنة</item>
+ <item quantity="many">%d تطبيقات مضمنة</item>
+ <item quantity="other">%d تطبيقات مضمنة</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="zero">تم استبعاد %d</item>
+ <item quantity="one">تم استبعاد %d</item>
+ <item quantity="two">تم استبعاد %d</item>
+ <item quantity="few">تم استبعاد %d</item>
+ <item quantity="many">تم استبعاد %d</item>
+ <item quantity="other">تم استبعاد %d</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <item quantity="zero">تم تضمين %d</item>
+ <item quantity="one">تم تضمين %d</item>
+ <item quantity="two">تم تضمين %d</item>
+ <item quantity="few">تم تضمين %d</item>
+ <item quantity="many">تم تضمين %d</item>
+ <item quantity="other">تم تضمين %d</item>
+ </plurals>
+ <string name="all_applications">جميع التطبيقات</string>
+ <string name="exclude_from_tunnel">استبعاد</string>
+ <string name="include_in_tunnel">تضمين فقط</string>
+ <plurals name="include_n_applications">
+ <item quantity="zero">تضمين %d من التطبيقات</item>
+ <item quantity="one">تضمين تطبيق %d</item>
+ <item quantity="two">تضمين %d من التطبيقات</item>
+ <item quantity="few">تضمين %d من التطبيقات</item>
+ <item quantity="many">تضمين %d من التطبيقات</item>
+ <item quantity="other">تضمين %d من التطبيقات</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="zero">استبعاد %d تطبيقات</item>
+ <item quantity="one">استبعاد %d تطبيقات</item>
+ <item quantity="two">استبعاد طبيقين</item>
+ <item quantity="few">استبعاد %d تطبيقات</item>
+ <item quantity="many">استبعاد %d تطبيقات</item>
+ <item quantity="other">استبعاد %d تطبيقات</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="zero">كل %d ثانية</item>
+ <item quantity="one">كل ثانية</item>
+ <item quantity="two">كل ثانيتين</item>
+ <item quantity="few">كل %d ثوانٍ</item>
+ <item quantity="many">كل %d ثوانٍ</item>
+ <item quantity="other">كل %d ثوانٍ</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="zero">لا ثانية</item>
+ <item quantity="one">ثانية</item>
+ <item quantity="two">ثنيتين</item>
+ <item quantity="few">ثواني</item>
+ <item quantity="many">ثواني</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="bad_config_context">%2$s الخاص بـ %1$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="error_no_qr_found">لم يتم العثور على رمز الاستجابة السريعة (QR) في الصورة</string>
+ <string name="error_qr_checksum">فشل التحقق من مجموعة رمز الاستجابة السريعة (QR)</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="copied_to_clipboard">%s نسخ إلى الحافظة</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="tv_delete">اختر نفقاً لحذفه</string>
+ <string name="tv_select_a_storage_drive">تحديد محرك تخزين</string>
+ <string name="tv_no_file_picker">الرجاء تثبيت مدير ملفات لتتمكن من تصفح الملفات</string>
+ <string name="tv_add_tunnel_get_started">أضف نفقاً لتبدأ</string>
+ <string name="donate_title">♥️ تبرع لمشروع وايرجارد</string>
+ <string name="donate_summary">كل مساهمة تساعد</string>
+ <string name="donate_google_play_disappointment">شكرًا لك على دعم مشروع\n\nWireGuard! للأسف، نظرًا لسياسات Google، لا يُسمح لنا بالارتباط بجزء من صفحة الويب الخاصة بالمشروع حيث يمكنك التبرع. نأمل أن تتمكن من معرفة ذلك!\n\nشكرًا مرة أخرى على مساهمتك.</string>
+ <string name="disable_config_export_title">تعطيل تصدير التكوين</string>
+ <string name="disable_config_export_description">تعطيل تصدير الإعدادات يجعل المفاتيح الخاصة غير متاحة</string>
+ <string name="dns_servers">خوادم DNS</string>
+ <string name="dns_search_domains">البحث عن النطاقات</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_prepare">خطأ في تحضير النفق: %s</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">: مفاتيح وايرجارد في الأساس 64 (base64) يجب أن تكون 44 حرفاً (32 بايت)</string>
+ <string name="key_length_explanation_binary">: مفاتيح وايرجارد يجب أن تكون 32 بايت</string>
+ <string name="key_length_explanation_hex">: مفاتيح وايرجارد في نظام عد ستة عشري (hexadecimal) يجب أن تكون 64 حرفاً (32 بايت)</string>
+ <string name="latest_handshake">أحدث مصافحة</string>
+ <string name="latest_handshake_ago">منذ %s</string>
+ <string name="listen_port">منفذ الاستماع</string>
+ <string name="log_export_error">غير قادر على تصدير السجل: %s</string>
+ <string name="log_export_subject">ملف سجل اندرويد وايرجارد</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_enabler_disabled_summary">يمكن لوحدة النواة التجريبية أن تحسن الأداء</string>
+ <string name="module_enabler_disabled_title">تمكين خلفية وحدة النواة</string>
+ <string name="module_enabler_enabled_summary">خلفية مساحة المستخدم البطيئة قد تحسن الاستقرار</string>
+ <string name="module_enabler_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">تحميل وتثبيت الوحدة للنواة (Kernel Module)</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">عنوان بروتوكول الإنترنت (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">التحكم في أنفاق WireGuard، وتمكين الأنفاق وتعطيلها حسب الرغبة، مما قد يؤدي إلى تضليل حركة مرور الإنترنت</string>
+ <string name="permission_label">التحكم في أنفاق وايرجارد</string>
+ <string name="persistent_keepalive">الحفاظ المستمر</string>
+ <string name="pre_shared_key">مفتاح مسبق التشارك (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="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>
+ <string name="save">حفظ</string>
+ <string name="select_all">اختيار الكلّ</string>
+ <string name="settings">الإعدادات</string>
+ <string name="shell_exit_status_read_error">لا يمكن للقشرة (shell) قراءة حالة الخروج</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">غير قادر على تثبيت أدوات سطر الأوامر (لا يوجد root؟)</string>
+ <string name="tools_installer_initial">تثبيت الأدوات الاختيارية للبرمجة النصية (scripting)</string>
+ <string name="tools_installer_initial_magisk">تثبيت الأدوات الاختيارية للبرمجة النصية كوحدة ماجيسك (Magisk Module)</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="transfer_bytes">%d بايت</string>
+ <string name="transfer_gibibytes">%.2f جيبي بايت</string>
+ <string name="transfer_kibibytes">%.2f كيبيبايت</string>
+ <string name="transfer_mibibytes">%.2f مبيبايت</string>
+ <string name="transfer_rx_tx">rx: %1$s, tx: %2$s</string>
+ <string name="transfer_tibibytes">%.2f تيبي بايت</string>
+ <string name="tun_create_error">غير قادر على إنشاء جهاز tun</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_dns_failure">غير قادر على حل اسم مضيف نظام أسماء النطاقات (DNS hostname): \"%s\"</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="updater_avalable">يتوفر تحديث للتطبيق. الرجاء التحديث الآن.</string>
+ <string name="updater_action">تنزيل &amp; تحديث</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>
+ <string name="version_title">وايرجارد لأندويد النسخة %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">سيتم حفظ ملف Zip في مجلد التنزيلات</string>
+ <string name="zip_export_title">تصدير الأنفاق إلى ملف zip</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>
diff --git a/ui/src/main/res/values-ca-rES/strings.xml b/ui/src/main/res/values-ca-rES/strings.xml
index 06ec2c50..073bb798 100644
--- a/ui/src/main/res/values-ca-rES/strings.xml
+++ b/ui/src/main/res/values-ca-rES/strings.xml
@@ -204,7 +204,6 @@
<string name="tunnel_create_success">El túnel \"%s\" s\'ha creat correctament</string>
<string name="tunnel_error_already_exists">El túnel \"%s\" ja existeix</string>
<string name="tunnel_error_invalid_name">Nom no vàlid</string>
- <string name="tunnel_list_placeholder">Afegiu un túnel usant el botó blau</string>
<string name="tunnel_name">Nom del túnel</string>
<string name="tunnel_on_error">No s\'ha estat capaç d\'activar el túnel (wgTurnOn ha retornat %d)</string>
<string name="tunnel_dns_failure">Impossible resoldre el nom del domini \"%s\"</string>
diff --git a/ui/src/main/res/values-da-rDK/strings.xml b/ui/src/main/res/values-da-rDK/strings.xml
index 22758121..bf786c07 100644
--- a/ui/src/main/res/values-da-rDK/strings.xml
+++ b/ui/src/main/res/values-da-rDK/strings.xml
@@ -8,4 +8,185 @@
<item quantity="one">Slettede %d tunnel</item>
<item quantity="other">%d tunneller blev slettet</item>
</plurals>
+ <plurals name="delete_title">
+ <item quantity="one">%d tunnel valgt</item>
+ <item quantity="other">%d tunneler valgt</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="one">Importeret %1$d ud af %2$d tunneler</item>
+ <item quantity="other">Importeret %1$d ud af %2$d tunneler</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="one">Importeret %d tunnel</item>
+ <item quantity="other">Importeret %d tunneler</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="one">%d Ekskluderet Applikation</item>
+ <item quantity="other">%d Ekskluderet Applikationer</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="one">%d Inkluderet Applikation</item>
+ <item quantity="other">%d Inkluderet Applikationer</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="one">%d ekskluderet</item>
+ <item quantity="other">%d ekskluderet</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <item quantity="one">%d inkluderet</item>
+ <item quantity="other">%d inkluderet</item>
+ </plurals>
+ <string name="all_applications">Alle Applikationer</string>
+ <string name="exclude_from_tunnel">Ekskludér</string>
+ <string name="include_in_tunnel">Inkludér kun</string>
+ <plurals name="include_n_applications">
+ <item quantity="one">Inkludér %d app</item>
+ <item quantity="other">Inkludér %d apps</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="one">Ekskludér %d app</item>
+ <item quantity="other">Ekskludér %d apps</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="one">hvert sekund</item>
+ <item quantity="other">hver %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">Brug alle apps</string>
+ <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>
+ <string name="bad_config_error">%1$s i %2$s</string>
+ <string name="bad_config_explanation_positive_number">: Skal være positiv</string>
+ <string name="bad_config_reason_invalid_key">Ugyldig nøgle</string>
+ <string name="bad_config_reason_invalid_number">Ugyldigt nummer</string>
+ <string name="bad_config_reason_invalid_value">Ugyldig værdi</string>
+ <string name="bad_config_reason_missing_section">Mangler sektion</string>
+ <string name="bad_config_reason_syntax_error">Syntaksfejl</string>
+ <string name="bad_config_reason_unknown_attribute">Ukendt egenskab</string>
+ <string name="bad_config_reason_unknown_section">Ukendt sektion</string>
+ <string name="bad_config_reason_value_out_of_range">Værdi udenfor området</string>
+ <string name="bad_extension_error">Filen skal være .conf eller .zip</string>
+ <string name="error_no_qr_found">QR-kode ikke fundet i billede</string>
+ <string name="cancel">Annullér</string>
+ <string name="config_delete_error">Kunne ikke slette konfigurationsfilen %s</string>
+ <string name="config_exists_error">Konfiguration for \"%s\" findes allerede</string>
+ <string name="config_file_exists_error">Konfigurationsfilen \"%s\" findes allerede</string>
+ <string name="config_not_found_error">Konfigurationsfilen \"%s\" blev ikke fundet</string>
+ <string name="config_rename_error">Kan ikke omdøbe konfigurationsfilen \"%s\"</string>
+ <string name="config_save_error">Kan ikke gemme konfigurationen for \"%1$s\": %2$s</string>
+ <string name="config_save_success">Konfiguration for \"%s\" blev gemt</string>
+ <string name="create_activity_title">Opret WireGuard tunnel</string>
+ <string name="create_empty">Opret fra ny</string>
+ <string name="create_from_file">Importér fra fil eller arkiv</string>
+ <string name="create_from_qr_code">Scan fra QR-kode</string>
+ <string name="create_tunnel">Opret Tunnel</string>
+ <string name="copied_to_clipboard">%s kopieret til udklipsholder</string>
+ <string name="dark_theme_summary_off">Bruger lige nu lyst (dag) tema</string>
+ <string name="dark_theme_summary_on">Bruger lige nu mørkt (nat) tema</string>
+ <string name="dark_theme_title">Brug mørkt tema</string>
+ <string name="delete">Slet</string>
+ <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>
+ <string name="edit">Redigér</string>
+ <string name="endpoint">Slutpunkt</string>
+ <string name="error_prepare">Fejl ved forberedelse af tunnel: %s</string>
+ <string name="error_up">Fejl under aktivering af tunnel: %s</string>
+ <string name="exclude_private_ips">Eksludér private IP-adresser</string>
+ <string name="generate_new_private_key">Generér ny privat nøgle</string>
+ <string name="generic_error">Ukendt \"%s\" fejl</string>
+ <string name="hint_automatic">(auto)</string>
+ <string name="hint_generated">(genereret)</string>
+ <string name="hint_optional">(valgfri)</string>
+ <string name="hint_optional_discouraged">(valgfri, ikke anbefalet)</string>
+ <string name="hint_random">(tilfældig)</string>
+ <string name="illegal_filename_error">Ugyldigt filnavn \"%s\"</string>
+ <string name="import_error">Kunne ikke importere tunnel: %s</string>
+ <string name="import_from_qr_code">Importér tunnel fra QR-kode</string>
+ <string name="import_success">Importeret \"%s\"</string>
+ <string name="interface_title">Grænseflade</string>
+ <string name="key_contents_error">Ugyldige tegn i nøgle</string>
+ <string name="key_length_error">Forkert længde på nøgle</string>
+ <string name="latest_handshake_ago">%s siden</string>
+ <string name="listen_port">Lytteport</string>
+ <string name="log_export_error">Kunne ikke eksportere log: %s</string>
+ <string name="log_export_subject">WireGuard Android Log-fil</string>
+ <string name="log_export_success">Gemt til \"%s\"</string>
+ <string name="log_export_title">Eksportér log-fil</string>
+ <string name="log_saver_activity_label">Gem log</string>
+ <string name="log_viewer_pref_summary">Logfiler kan hjælpe ved fejlsøgning</string>
+ <string name="log_viewer_pref_title">Vis applikationslog</string>
+ <string name="log_viewer_title">Log</string>
+ <string name="logcat_error">Kunne ikke køre logcat: </string>
+ <string name="module_installer_error">Noget gik galt. Forsøg venligst igen</string>
+ <string name="module_installer_title">Hent og installér kerne-modul</string>
+ <string name="module_installer_working">Henter og installerer…</string>
+ <string name="mtu">MTU</string>
+ <string name="multiple_tunnels_title">Tillad flere samtidige tunneler</string>
+ <string name="name">Navn</string>
+ <string name="no_configs_error">Ingen konfigurationer fundet</string>
+ <string name="no_tunnels_error">Ingen tilgængelige tunneler</string>
+ <string name="parse_error_generic">streng</string>
+ <string name="parse_error_inet_address">IP adresse</string>
+ <string name="parse_error_inet_endpoint">slutpunkt</string>
+ <string name="parse_error_inet_network">IP netværk</string>
+ <string name="parse_error_integer">nummer</string>
+ <string name="peer">Modpart</string>
+ <string name="pre_shared_key">Forhåndsdelt nøgle</string>
+ <string name="pre_shared_key_enabled">aktiveret</string>
+ <string name="private_key">Privat nøgle</string>
+ <string name="public_key">Offentlig nøgle</string>
+ <string name="restore_on_boot_title">Gendan ved opstart</string>
+ <string name="save">Gem</string>
+ <string name="select_all">Vælg alle</string>
+ <string name="settings">Indstillinger</string>
+ <string name="toggle_all">Vælg/Fravælg Alle</string>
+ <string name="tools_installer_already">wg og wg-quick er allerede installeret</string>
+ <string name="tools_installer_working">Installerer wg og wg-quick</string>
+ <string name="transfer">Overførsel</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">Kunne ikke oprette tun enhed</string>
+ <string name="tunnel_config_error">Kunne ikke konfigurere tunnel (wg-quick returnerede %d)</string>
+ <string name="tunnel_create_error">Ikke i stand til at oprette tunnel: %s</string>
+ <string name="tunnel_create_success">Tunnelen blev succesfuldt oprettet \"%s\"</string>
+ <string name="tunnel_error_already_exists">Tunnel \"%s\" eksisterer allerede</string>
+ <string name="tunnel_error_invalid_name">Ugyldigt navn</string>
+ <string name="tunnel_list_placeholder">Tilføj en tunnel ved hjælp af knappen nedenfor</string>
+ <string name="tunnel_name">Tunnel Navn</string>
+ <string name="tunnel_dns_failure">Kunne ikke opslå DNS adresse: \"%s\"</string>
+ <string name="tunnel_rename_error">Kan ikke omdøbe tunnel: %s</string>
+ <string name="tunnel_rename_success">Tunnel blev succesfuldt omdøbt til \"%s\"</string>
+ <string name="type_name_kernel_module">Kerne modul</string>
+ <string name="unknown_error">Ukendt fejl</string>
+ <string name="updater_avalable">En applikationsopdatering er tilgængelig. Opdatér venligst nu.</string>
+ <string name="updater_action">Hent &amp; Opdatér</string>
+ <string name="updater_download_progress">Henter opdatering: %1$s / %2$s (%3$.2f%%)</string>
+ <string name="updater_download_progress_nototal">Henter opdatering: %s</string>
+ <string name="updater_installing">Installerer opdatering…</string>
+ <string name="version_summary_unknown">Ukendt %s version</string>
+ <string name="version_title">WireGuard for Android v%s</string>
+ <string name="zip_export_error">Ikke i stand til at eksportere tunneler: %s</string>
+ <string name="zip_export_success">Gemt til \"%s\"</string>
+ <string name="zip_export_title">Eksportér tunneler til zip-fil</string>
+ <string name="biometric_auth_error">Fejl ved godkendelse</string>
+ <string name="biometric_auth_error_reason">Fejl ved godkendelse: %s</string>
</resources>
diff --git a/ui/src/main/res/values-de/strings.xml b/ui/src/main/res/values-de/strings.xml
index db012dbf..372abcad 100644
--- a/ui/src/main/res/values-de/strings.xml
+++ b/ui/src/main/res/values-de/strings.xml
@@ -53,7 +53,7 @@
</plurals>
<plurals name="persistent_keepalive_seconds_suffix">
<item quantity="one">Sekunde</item>
- <item quantity="other">Sekunden</item>
+ <item quantity="other">seconds</item>
</plurals>
<string name="use_all_applications">Alle Apps verwenden</string>
<string name="add_peer">Gegenüber hinzufügen</string>
@@ -79,6 +79,8 @@
<string name="bad_config_reason_unknown_section">Unbekannter Abschnitt</string>
<string name="bad_config_reason_value_out_of_range">Wert ist außerhalb des gültigen Bereichs</string>
<string name="bad_extension_error">Dateiendung muss .conf oder .zip sein</string>
+ <string name="error_no_qr_found">Es wurde kein QR-Code im Bild gefunden</string>
+ <string name="error_qr_checksum">Die QR-Code Prüfsummenkontrolle ist fehlgeschlagen</string>
<string name="cancel">Abbrechen</string>
<string name="config_delete_error">Konfigurationsdatei %s kann nicht gelöscht werden</string>
<string name="config_exists_error">Konfiguration für „%s“ existiert bereits</string>
@@ -97,22 +99,27 @@
<string name="create_temp_dir_error">Lokales temporäres Verzeichnis kann nicht erstellt werden</string>
<string name="create_tunnel">Tunnel erstellen</string>
<string name="copied_to_clipboard">%s in die Zwischenanlage kopiert</string>
- <string name="dark_theme_summary_off">Verwende helles Design (Tag)</string>
- <string name="dark_theme_summary_on">Verwende dunkles Design (Nacht)</string>
+ <string name="dark_theme_summary_off">Helles (Tag) Design in Verwendung</string>
+ <string name="dark_theme_summary_on">Dunkles (Nacht) Design in Verwendung</string>
<string name="dark_theme_title">Dunkles Design verwenden</string>
<string name="delete">Entfernen</string>
<string name="tv_delete">Wählen Sie den zu löschenden Tunnel aus</string>
<string name="tv_select_a_storage_drive">Wählen Sie ein Speicherlaufwerk</string>
<string name="tv_no_file_picker">Bitte installieren Sie ein Dateiverwaltungsprogramm, um Dateien zu durchsuchen</string>
<string name="tv_add_tunnel_get_started">Fügen Sie einen Tunnel hinzu, um loszulegen</string>
+ <string name="donate_title">♥ Spende an das WireGuard Projekt</string>
+ <string name="donate_summary">Jeder Beitrag hilft</string>
+ <string name="donate_google_play_disappointment">Vielen Dank für Ihre Unterstützung des WireGuard-Projekts!\n\nLeider ist es uns aufgrund der Google-Richtlinien nicht gestattet, einen Link zu dem Teil der Projekt-Webseite zu setzen, auf dem Sie eine Spende tätigen können. Hoffentlich finden Sie das heraus!\n\nNochmals vielen Dank für Ihren Beitrag.</string>
<string name="disable_config_export_title">Deaktivieren Sie den Konfigurationsexport</string>
<string name="disable_config_export_description">Durch Deaktivieren des Konfigurationsexports werden private Schlüssel weniger zugänglich</string>
<string name="dns_servers">Nameserver</string>
+ <string name="dns_search_domains">Suchdomäne</string>
<string name="edit">Bearbeiten</string>
<string name="endpoint">Endpunkt</string>
<string name="error_down">Fehler beim Abschalten des Tunnels: %s</string>
<string name="error_fetching_apps">Fehler beim Abrufen der App-Liste: %s</string>
<string name="error_root">Bitte root-Zugriff anfordern und erneut versuchen</string>
+ <string name="error_prepare">Fehler beim Vorbereiten des Tunnels: %s</string>
<string name="error_up">Fehler beim Starten des Tunnels: %s</string>
<string name="exclude_private_ips">Private IPs ausschließen</string>
<string name="generate_new_private_key">Neuen privaten Schlüssel generieren</string>
@@ -132,6 +139,8 @@
<string name="key_length_explanation_base64">: WireGuard base64-Schlüssel müssen 44 Zeichen enthalten (32 Bytes)</string>
<string name="key_length_explanation_binary">: WireGuard-Schlüssel müssen 32 Bytes groß sein</string>
<string name="key_length_explanation_hex">: WireGuard Hex-Schlüssel müssen 64 Zeichen (32 Bytes) groß sein</string>
+ <string name="latest_handshake">Letzter Handshake</string>
+ <string name="latest_handshake_ago">vor %s</string>
<string name="listen_port">Eingangs-Port</string>
<string name="log_export_error">Konnte Protokoll nicht exportieren: %s</string>
<string name="log_export_subject">WireGuard Android Protokolldatei</string>
@@ -175,6 +184,10 @@
<string name="private_key">Privater Schlüssel</string>
<string name="public_key">Öffentlicher Schlüssel</string>
<string name="qr_code_hint">Tipp: Mit `qrencode -t ansiutf8 &lt; tunnel.conf` generieren.</string>
+ <string name="quick_settings_tile_add_title">Kachel zu Schnelleinstellungen hinzufügen</string>
+ <string name="quick_settings_tile_add_summary">Die Verknüpfung schaltet den letzten Tunnel um</string>
+ <string name="quick_settings_tile_add_failure">Verknüpfung kann nicht hinzugefügt werden: Fehler %d</string>
+ <string name="quick_settings_tile_action">Tunnel umschalten</string>
<string name="restore_on_boot_summary_off">Aktivierte Tunnel beim Systemstart nicht automatisch starten</string>
<string name="restore_on_boot_summary_on">Aktivierte Tunnel beim Systemstart automatisch wieder starten</string>
<string name="restore_on_boot_title">Beim Neustart wiederherstellen</string>
@@ -189,14 +202,14 @@
<string name="toggle_error">Fehler beim Umschalten des WireGuard-Tunnels: %s</string>
<string name="tools_installer_already">wg und wg-quick sind bereits installiert</string>
<string name="tools_installer_failure">Kommandozeilenwerkzeuge konnten nicht installiert werden (kein Root?)</string>
- <string name="tools_installer_initial">Optionale Tools für Skripte installieren</string>
+ <string name="tools_installer_initial">Optionale Werkzeuge für Skripte installieren</string>
<string name="tools_installer_initial_magisk">Optionale Werkzeuge für das Skripten als Magisk-Modul installieren</string>
<string name="tools_installer_initial_system">Optionale Werkzeuge für Skripte auf der Systempartition installieren</string>
<string name="tools_installer_success_magisk">wg und wg-quick als Magisk-Modul installiert (Neustart erforderlich)</string>
<string name="tools_installer_success_system">wg und wg-quick auf der Systempartition installiert</string>
<string name="tools_installer_title">Kommandozeilenwerkzeuge installieren</string>
<string name="tools_installer_working">Installiere wg und wg-quick</string>
- <string name="tools_unavailable_error">Erforderliche Tools nicht verfügbar</string>
+ <string name="tools_unavailable_error">Erforderliche Werkzeuge nicht verfügbar</string>
<string name="transfer">Transfer</string>
<string name="transfer_bytes">%d B</string>
<string name="transfer_gibibytes">%.2f GiB</string>
@@ -210,7 +223,7 @@
<string name="tunnel_create_success">Tunnel „%s “ erfolgreich erstellt</string>
<string name="tunnel_error_already_exists">Tunnel „%s“ existiert bereits</string>
<string name="tunnel_error_invalid_name">Ungültiger Name</string>
- <string name="tunnel_list_placeholder">Füge einen Tunnel mit der blauen Taste hinzu</string>
+ <string name="tunnel_list_placeholder">Füge einen Tunnel mit der Schaltfläche unten hinzu</string>
<string name="tunnel_name">Tunnelname</string>
<string name="tunnel_on_error">Tunnel kann nicht eingeschaltet werden (wgTurnOn gab %d zurück)</string>
<string name="tunnel_dns_failure">DNS-Hostname kann nicht aufgelöst werden: „%s“</string>
@@ -219,6 +232,17 @@
<string name="type_name_go_userspace">Go userspace</string>
<string name="type_name_kernel_module">Kernelmodul</string>
<string name="unknown_error">Unbekannter Fehler</string>
+ <string name="updater_avalable">Ein Anwendungsupdate ist verfügbar. Bitte jetzt aktualisieren.</string>
+ <string name="updater_action">Download &amp; Update</string>
+ <string name="updater_rechecking">Update-Metadaten abrufen…</string>
+ <string name="updater_download_progress">Update wird heruntergeladen: %1$s / %2$s (%3$.2f%%)</string>
+ <string name="updater_download_progress_nototal">Update wird heruntergeladen: %s</string>
+ <string name="updater_installing">Installiere Update…</string>
+ <string name="updater_failure">Fehler beim Aktualisieren: %s. Versuche es in Kürze erneut…</string>
+ <string name="updater_corrupt_title">Anwendung beschädigt</string>
+ <string name="updater_corrupt_message">Diese Anwendung ist beschädigt. Bitte laden Sie die APK erneut von der unten verlinkten Website herunter. Deinstallieren Sie danach diese Anwendung und installieren Sie sie mit der heruntergeladenen APK neu.</string>
+ <string name="updater_corrupt_navigate">Webseite öffnen</string>
+ <string name="version_summary">%1$s backend %2$s</string>
<string name="version_summary_checking">Überprüfe %s Backend-Version</string>
<string name="version_summary_unknown">Unbekannte %s Version</string>
<string name="version_title">WireGuard für Android v%s</string>
diff --git a/ui/src/main/res/values-el-rGR/strings.xml b/ui/src/main/res/values-el-rGR/strings.xml
index e9d1a779..d4afbf60 100644
--- a/ui/src/main/res/values-el-rGR/strings.xml
+++ b/ui/src/main/res/values-el-rGR/strings.xml
@@ -59,4 +59,68 @@
<string name="add_peer">Προσθήκη peer</string>
<string name="addresses">Διευθύνσεις</string>
<string name="applications">Εφαρμογές</string>
+ <string name="allow_remote_control_intents_summary_off">Οι εξωτερικές εφαρμογές δεν θα μπορούν να αλλάζουν την κατάσταση των tunnel (συνιστάται)</string>
+ <string name="allow_remote_control_intents_summary_on">Οι εξωτερικές εφαρμογές θα μπορούν να αλλάζουν την κατάσταση των tunnel (για προχωρημένους)</string>
+ <string name="allow_remote_control_intents_title">Επιτρέψτε τον έλεγχο από άλλες εφαρμογές</string>
+ <string name="allowed_ips">Επιτρεπόμενες IP</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_syntax_error">Σφάλμα σύνταξης</string>
+ <string name="bad_config_reason_unknown_attribute">Άγνωστη ιδιότητα</string>
+ <string name="bad_config_reason_value_out_of_range">Τιμή εκτός εύρους</string>
+ <string name="bad_extension_error">Το αρχείο πρέπει να είναι .conf ή .zip</string>
+ <string name="error_no_qr_found">Δεν βρέθηκε κωδικός QR στην εικόνα</string>
+ <string name="error_qr_checksum">Αποτυχία επαλήθευσης checksum κωδικού QR</string>
+ <string name="cancel">Ακύρωση</string>
+ <string name="create_empty">Δημιουργία από την αρχή</string>
+ <string name="create_from_file">Εισαγωγή από αρχείο ή αρχειοθήκη</string>
+ <string name="create_from_qr_code">Σάρωση από κωδικό QR</string>
+ <string name="dark_theme_title">Χρήση σκούρου θέματος</string>
+ <string name="delete">Διαγραφή</string>
+ <string name="dns_servers">Διακομιστές DNS</string>
+ <string name="dns_search_domains">Αναζήτηση τομέων</string>
+ <string name="edit">Επεξεργασία</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_optional">(προαιρετικό)</string>
+ <string name="key_contents_error">Μη έγκυροι χαρακτήρες στο κλειδί</string>
+ <string name="key_length_error">Εσφαλμένο μήκος κλειδιού</string>
+ <string name="key_length_explanation_binary">: Τα κλειδιά του WireGuard πρέπει να είναι 32 bytes</string>
+ <string name="log_export_title">Εξαγωγή αρχείου καταγραφής</string>
+ <string name="log_saver_activity_label">Αποθήκευση αρχείου καταγραφής</string>
+ <string name="log_viewer_title">Αρχείο καταγραφής</string>
+ <string name="mtu">MTU</string>
+ <string name="name">Όνομα</string>
+ <string name="parse_error_inet_address">Διεύθυνση IP</string>
+ <string name="parse_error_inet_network">Δίκτυο IP</string>
+ <string name="parse_error_integer">αριθμός</string>
+ <string name="peer">Peer</string>
+ <string name="private_key">Ιδιωτικό κλειδί</string>
+ <string name="public_key">Δημόσιο κλειδί</string>
+ <string name="save">Αποθήκευση</string>
+ <string name="select_all">Επιλογή όλων</string>
+ <string name="settings">Ρυθμίσεις</string>
+ <string name="tools_installer_title">Εγκατάσταση εργαλείων γραμμής εντολών</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_error_invalid_name">Μη έγκυρο όνομα</string>
+ <string name="unknown_error">Άγνωστο σφάλμα</string>
+ <string name="version_summary_unknown">Άγνωστη έκδοση %s</string>
+ <string name="version_title">WireGuard για Android v%s</string>
+ <string name="biometric_auth_error">Αποτυχία ελέγχου ταυτότητας</string>
+ <string name="biometric_auth_error_reason">Αποτυχία ελέγχου ταυτότητας: %s</string>
</resources>
diff --git a/ui/src/main/res/values-es-rES/strings.xml b/ui/src/main/res/values-es-rES/strings.xml
index da5b89fb..709b183b 100644
--- a/ui/src/main/res/values-es-rES/strings.xml
+++ b/ui/src/main/res/values-es-rES/strings.xml
@@ -21,12 +21,12 @@
<item quantity="other">%d túneles importados</item>
</plurals>
<plurals name="set_excluded_applications">
- <item quantity="one">%d aplicación excluida</item>
- <item quantity="other">%d aplicaciones excluidas</item>
+ <item quantity="one">%d Aplicación Excluida</item>
+ <item quantity="other">%d Aplicaciones Excluidas</item>
</plurals>
<plurals name="set_included_applications">
- <item quantity="one">%d aplicación incluída</item>
- <item quantity="other">%d aplicaciones incluidas</item>
+ <item quantity="one">%d Aplicación Incluida</item>
+ <item quantity="other">%d Aplicaciones Incluidas</item>
</plurals>
<plurals name="n_excluded_applications">
<item quantity="one">%d excluido</item>
@@ -59,9 +59,9 @@
<string name="add_peer">Añadir par</string>
<string name="addresses">Direcciones</string>
<string name="applications">Aplicaciones</string>
- <string name="allow_remote_control_intents_summary_off">Las aplicaciones externas no pueden cambiar túneles (recomendado)</string>
- <string name="allow_remote_control_intents_summary_on">Las aplicaciones externas pueden cambiar túneles (avanzado)</string>
- <string name="allow_remote_control_intents_title">Permitir aplicaciones de mando remoto</string>
+ <string name="allow_remote_control_intents_summary_off">Las aplicaciones externas no pueden cambiar el estado de los túneles (recomendado)</string>
+ <string name="allow_remote_control_intents_summary_on">Las aplicaciones externas pueden cambiar el estado de los túneles (avanzado)</string>
+ <string name="allow_remote_control_intents_title">Permitir aplicaciones de control remoto</string>
<string name="allowed_ips">IPs permitidas</string>
<string name="bad_config_context">%1$s de %2$s</string>
<string name="bad_config_context_top_level">%s</string>
@@ -79,6 +79,8 @@
<string name="bad_config_reason_unknown_section">Sección desconocida</string>
<string name="bad_config_reason_value_out_of_range">Valor fuera de rango</string>
<string name="bad_extension_error">El archivo debe ser .conf o .zip</string>
+ <string name="error_no_qr_found">Código QR no encontrado en la imagen</string>
+ <string name="error_qr_checksum">Falló la verificación de la suma de comprobación del código QR</string>
<string name="cancel">Cancelar</string>
<string name="config_delete_error">No se puede eliminar el archivo de configuración %s</string>
<string name="config_exists_error">La configuración para “%s” ya existe</string>
@@ -105,14 +107,19 @@
<string name="tv_select_a_storage_drive">Selecciones un dispositivo de almacenamiento</string>
<string name="tv_no_file_picker">Por favor, instale una herramienta de gestión de archivos para navegar por archivos</string>
<string name="tv_add_tunnel_get_started">Agregue un túnel para empezar</string>
- <string name="disable_config_export_title">Deshabilitar la exportación de configuración</string>
+ <string name="donate_title">♥ Donar al Proyecto WireGuard</string>
+ <string name="donate_summary">Todas las contribuciones ayudan</string>
+ <string name="donate_google_play_disappointment">¡Gracias por apoyar el proyecto WireGuard!\n\nLamentablemente, debido a las políticas de Google, no estamos autorizados a enlazar a la parte de la página web del proyecto donde puedes hacer una donación. ¡Esperemos que puedas averiguar esto!\n\nGracias de nuevo por tu contribución.</string>
+ <string name="disable_config_export_title">Inhabilitar la exportación de configuración</string>
<string name="disable_config_export_description">Desactivar la exportación de configuración hace que las claves privadas sean menos accesibles</string>
<string name="dns_servers">Servidores DNS</string>
+ <string name="dns_search_domains">Buscar dominios</string>
<string name="edit">Editar</string>
- <string name="endpoint">Punto final</string>
+ <string name="endpoint">Endpoint</string>
<string name="error_down">Error al bajar el túnel: %s</string>
<string name="error_fetching_apps">Error al obtener la lista de aplicaciones: %s</string>
<string name="error_root">Por favor, obtén acceso root y vuelve a intentarlo</string>
+ <string name="error_prepare">Error al preparar el túnel: %s</string>
<string name="error_up">Error al abrir el túnel: %s</string>
<string name="exclude_private_ips">Excluir direcciones privadas</string>
<string name="generate_new_private_key">Generar nueva clave privada</string>
@@ -132,6 +139,8 @@
<string name="key_length_explanation_base64">Las claves base64 de WireGuard deben tener 44 caracteres (32 bytes)</string>
<string name="key_length_explanation_binary">: Las claves WireGuard deben tener 32 bytes</string>
<string name="key_length_explanation_hex">: Las claves hexadecimales de Wirex deben tener 64 caracteres (32 bytes)</string>
+ <string name="latest_handshake">Última comunicación</string>
+ <string name="latest_handshake_ago">hace %s</string>
<string name="listen_port">Puerto de escucha</string>
<string name="log_export_error">No se pudo exportar el registro: %s</string>
<string name="log_export_subject">Archivo de registro WireGuard Android</string>
@@ -139,7 +148,7 @@
<string name="log_export_title">Exportar archivo de registro</string>
<string name="log_saver_activity_label">Guardar registro</string>
<string name="log_viewer_pref_summary">Los registros pueden ayudar con la depuración</string>
- <string name="log_viewer_pref_title">Ver registro de aplicación</string>
+ <string name="log_viewer_pref_title">Ver registro de la aplicación</string>
<string name="log_viewer_title">Registro</string>
<string name="logcat_error">No se puede ejecutar logcat: </string>
<string name="module_enabler_disabled_summary">El módulo experimental del kernel puede mejorar el rendimiento</string>
@@ -162,14 +171,14 @@
<string name="no_tunnels_error">No existen túneles</string>
<string name="parse_error_generic">cadena</string>
<string name="parse_error_inet_address">Dirección IP</string>
- <string name="parse_error_inet_endpoint">punto final</string>
+ <string name="parse_error_inet_endpoint">Endpoint</string>
<string name="parse_error_inet_network">Red IP</string>
<string name="parse_error_integer">número</string>
<string name="parse_error_reason">No se puede analizar %1$s “%2$s”</string>
<string name="peer">Pares</string>
<string name="permission_description">controlar túneles de WireGuard, habilitando y desactivando túneles a su antojo, lo que podría conducir mal al tráfico de Internet</string>
<string name="permission_label">controlar túneles de WireGuard</string>
- <string name="persistent_keepalive">Mantenimiento persistente</string>
+ <string name="persistent_keepalive">Keepalive persistente</string>
<string name="pre_shared_key">Clave precompartida</string>
<string name="pre_shared_key_enabled">activado</string>
<string name="private_key">Clave privada</string>
@@ -181,7 +190,7 @@
<string name="save">Guardar</string>
<string name="select_all">Seleccionar todo</string>
<string name="settings">Preferencias</string>
- <string name="shell_exit_status_read_error">Shell no puede leer estado de salida</string>
+ <string name="shell_exit_status_read_error">Shell no puede leer el estado de salida</string>
<string name="shell_marker_count_error">Shell esperaba 4 marcadores, recibió %d</string>
<string name="shell_start_error">No se pudo iniciar Shell: %d</string>
<string name="success_application_will_restart">Éxito. La aplicación se reiniciará ahora…</string>
@@ -210,7 +219,7 @@
<string name="tunnel_create_success">Túnel creado con éxito “%s”</string>
<string name="tunnel_error_already_exists">Túnel “%s” ya existe</string>
<string name="tunnel_error_invalid_name">Nombre inválido</string>
- <string name="tunnel_list_placeholder">Añadir un túnel usando el botón azul</string>
+ <string name="tunnel_list_placeholder">Añadir un túnel usando el botón azul de abajo</string>
<string name="tunnel_name">Nombre del túnel</string>
<string name="tunnel_on_error">No se puede activar el túnel (wgTurnOn devolvió %d)</string>
<string name="tunnel_dns_failure">No se puede resolver el nombre de host DNS: “%s”</string>
diff --git a/ui/src/main/res/values-et-rEE/strings.xml b/ui/src/main/res/values-et-rEE/strings.xml
new file mode 100644
index 00000000..9beaafd9
--- /dev/null
+++ b/ui/src/main/res/values-et-rEE/strings.xml
@@ -0,0 +1,257 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="one">%d tunnelit ei saa kustutada: %s</item>
+ <item quantity="other">%d tunnelit ei saa kustutada: %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="one">%d tunnel kustutatud</item>
+ <item quantity="other">%d tunnelit kustutatud</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="one">%d tunnel valitud</item>
+ <item quantity="other">%d tunnelit valitud</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="one">Imporditud %1$d tunnel %2$d-st</item>
+ <item quantity="other">Imporditud %1$d tunnelit %2$d-st</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="one">Imporditud %d tunnel</item>
+ <item quantity="other">Imporditud %d tunnelit</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="one">%d välistatud rakendus</item>
+ <item quantity="other">%d välistatud rakendust</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="one">%d kaasatud rakendus</item>
+ <item quantity="other">%d kaasatud rakendust</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="one">%d välistatud</item>
+ <item quantity="other">%d välistatud</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <item quantity="one">%d kaasatud</item>
+ <item quantity="other">%d kaasatud</item>
+ </plurals>
+ <string name="all_applications">Kõik rakendused</string>
+ <string name="exclude_from_tunnel">Välista</string>
+ <string name="include_in_tunnel">Kaasa ainult</string>
+ <plurals name="include_n_applications">
+ <item quantity="one">Kaasa %d rakendus</item>
+ <item quantity="other">Kaasa %d rakendust</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="one">Välista %d rakendus</item>
+ <item quantity="other">Välista %d rakendust</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="one">iga sekund</item>
+ <item quantity="other">iga %d sekundi järel</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="one">sekund</item>
+ <item quantity="other">sekundit</item>
+ </plurals>
+ <string name="use_all_applications">Kasuta kõiki rakendusi</string>
+ <string name="add_peer">Lisa partner</string>
+ <string name="addresses">Aadressid</string>
+ <string name="applications">Rakendused</string>
+ <string name="allow_remote_control_intents_summary_off">Välised rakendused ei saa tunneleid lülitada (soovituslik)</string>
+ <string name="allow_remote_control_intents_summary_on">Välised rakendused saavad tunneleid lülitada (edasijõudnud)</string>
+ <string name="allow_remote_control_intents_title">Luba kaugjuhtimise rakendused</string>
+ <string name="allowed_ips">Lubatud IP\'d</string>
+ <string name="bad_config_context">sektsiooni %1$s asukohas %2$s</string>
+ <string name="bad_config_context_top_level">%s</string>
+ <string name="bad_config_error">%1$s asukohas %2$s</string>
+ <string name="bad_config_explanation_pka">: Peab olema positiivne ja mitte suurem kui 65535</string>
+ <string name="bad_config_explanation_positive_number">: Peab olema positiivne</string>
+ <string name="bad_config_explanation_udp_port">: Peab olema korrektne UDP pordi number</string>
+ <string name="bad_config_reason_invalid_key">Vigane võti</string>
+ <string name="bad_config_reason_invalid_number">Vigane arv</string>
+ <string name="bad_config_reason_invalid_value">Vigane väärtus</string>
+ <string name="bad_config_reason_missing_attribute">Puudub atribuut</string>
+ <string name="bad_config_reason_missing_section">Puudub sektsioon</string>
+ <string name="bad_config_reason_syntax_error">Süntaksiviga</string>
+ <string name="bad_config_reason_unknown_attribute">Tundmatu atribuut</string>
+ <string name="bad_config_reason_unknown_section">Tundmatu sektsioon</string>
+ <string name="bad_config_reason_value_out_of_range">Väärtus lubatud vahemikust väljas</string>
+ <string name="bad_extension_error">Faililaiend peab olema .conf või .zip</string>
+ <string name="error_no_qr_found">QR-koodi ei leitud pildilt</string>
+ <string name="error_qr_checksum">QR-koodi kontrollsumma verifitseerimine ebaõnnestus</string>
+ <string name="cancel">Tühista</string>
+ <string name="config_delete_error">Seadistusfaili %s kustutamine ebaõnnestus</string>
+ <string name="config_exists_error">\"%s\" seadistus on juba olemas</string>
+ <string name="config_file_exists_error">Seadistusfail \"%s\" on juba olemas</string>
+ <string name="config_not_found_error">Seadistusfaili \"%s\" ei leitud</string>
+ <string name="config_rename_error">Seadistusfaili \"%s\" ümbernimetamine ebaõnnestus</string>
+ <string name="config_save_error">\"%1$s\" seadistuse salvestamine ebaõnnestus: %2$s</string>
+ <string name="config_save_success">\"%s\" seadistus salvestatud</string>
+ <string name="create_activity_title">Loo WireGuard tunnel</string>
+ <string name="create_bin_dir_error">Lokaalse programmfaili kataloogi tekitamine ebaõnnestus</string>
+ <string name="create_downloads_file_error">Faili loomine allalaadimiste kataloogis ebaõnnestus</string>
+ <string name="create_empty">Sisesta käsitsi</string>
+ <string name="create_from_file">Impordi failist või arhiivist</string>
+ <string name="create_from_qr_code">Skaneeri QR-koodist</string>
+ <string name="create_output_dir_error">Väljundkataloogi tekitamine ebaõnnestus</string>
+ <string name="create_temp_dir_error">Ajutise kataloogi tekitamine ebaõnnestus</string>
+ <string name="create_tunnel">Loo uus tunnel</string>
+ <string name="copied_to_clipboard">%s kopeeritud lõikelauale</string>
+ <string name="dark_theme_summary_off">Hetkel kasutusel hele (päevane) teema</string>
+ <string name="dark_theme_summary_on">Hetkel kasutusel tume (öine) teema</string>
+ <string name="dark_theme_title">Kasuta tumedat teemat</string>
+ <string name="delete">Kustuta</string>
+ <string name="tv_delete">Vali tunnel, mida kustutada</string>
+ <string name="tv_select_a_storage_drive">Vali salvestusseade</string>
+ <string name="tv_no_file_picker">Failide vaatamiseks paigalda failide haldusvahend</string>
+ <string name="tv_add_tunnel_get_started">Alustamiseks lisa uus tunnel</string>
+ <string name="donate_title">♥ Anneta WireGuard\'i projektile</string>
+ <string name="donate_summary">Iga panus aitab</string>
+ <string name="donate_google_play_disappointment">Aitäh, et toetad WireGuard\'i projekti!\n\nKahjuks ei saa me Google\'i eeskirjade tõttu linkida projekti veebilehele, kus saab annetusi teha. Loodetavasti leiad selle ise!\n\n
+Aitäh veelkord sinu panuse eest.</string>
+ <string name="disable_config_export_title">Keela seadistuste eksportimine</string>
+ <string name="disable_config_export_description">Seadistuste eksportimise keelamine teeb privaatvõtmetele ligipääsu keerulisemaks</string>
+ <string name="dns_servers">DNS-serverid</string>
+ <string name="dns_search_domains">DNS-i otsingudomeenid</string>
+ <string name="edit">Muuda</string>
+ <string name="endpoint">Lõpp-punkt</string>
+ <string name="error_down">Viga tunneli väljalülitamisel: %s</string>
+ <string name="error_fetching_apps">Viga rakenduste nimekirja pärimisel: %s</string>
+ <string name="error_root">Hangi juurkasutaja õigused ja proovi uuesti</string>
+ <string name="error_prepare">Viga tunneli ettevalmistamisel: %s</string>
+ <string name="error_up">Viga tunneli sisselülitamisel: %s</string>
+ <string name="exclude_private_ips">Keela privaatsed IP\'d</string>
+ <string name="generate_new_private_key">Tekita uus privaatvõti</string>
+ <string name="generic_error">Tundmatu \"%s\" viga</string>
+ <string name="hint_automatic">(automaatne)</string>
+ <string name="hint_generated">(genereeritud)</string>
+ <string name="hint_optional">(valikuline)</string>
+ <string name="hint_optional_discouraged">(valikuline, mittesoovituslik)</string>
+ <string name="hint_random">(juhuslik)</string>
+ <string name="illegal_filename_error">Lubamatu failinimi \"%s\"</string>
+ <string name="import_error">Tunneli importimine ebaõnnestus: %s</string>
+ <string name="import_from_qr_code">Impordi tunnel QR-koodist</string>
+ <string name="import_success">Imporditud \"%s\"</string>
+ <string name="interface_title">Liides</string>
+ <string name="key_contents_error">Lubamatud sümbolid võtmes</string>
+ <string name="key_length_error">Sobimatu võtme pikkus</string>
+ <string name="key_length_explanation_base64">: WireGuard base64 võti peab olema 44 sümbolit (32 baiti) pikk</string>
+ <string name="key_length_explanation_binary">: WireGuard võtmed peavad olema 32 baiti</string>
+ <string name="key_length_explanation_hex">: WireGuard hex võti peab olema 64 sümbolit (32 baiti) pikk</string>
+ <string name="latest_handshake">Viimane kätlus</string>
+ <string name="latest_handshake_ago">%s tagasi</string>
+ <string name="listen_port">Kuulamisport</string>
+ <string name="log_export_error">Logifaili eksportimine ebaõnnestus: %s</string>
+ <string name="log_export_subject">WireGuard Android logifail</string>
+ <string name="log_export_success">Salvestatud faili \"%s\"</string>
+ <string name="log_export_title">Ekspordi logifail</string>
+ <string name="log_saver_activity_label">Salvesta logi</string>
+ <string name="log_viewer_pref_summary">Logid võivad aidata vigade uurimisel</string>
+ <string name="log_viewer_pref_title">Vaata rakenduse logi</string>
+ <string name="log_viewer_title">Logi</string>
+ <string name="logcat_error">Viga logcat käivitamisel: </string>
+ <string name="module_enabler_disabled_summary">Eksperimentaalne tuumamoodul võib jõudlust parandada</string>
+ <string name="module_enabler_disabled_title">Luba tuumamooduli kasutamine</string>
+ <string name="module_enabler_enabled_summary">Aeglasem kasutajamaa moodul võib stabiilsust parandada</string>
+ <string name="module_enabler_enabled_title">Keela tuumamooduli kasutamine</string>
+ <string name="module_installer_error">Midagi läks valesti. Palun proovi uuesti</string>
+ <string name="module_installer_initial">Eksperimentaalne tuumamoodul võib jõudlust parandada</string>
+ <string name="module_installer_not_found">Sinu seadme jaoks ei ole mooduleid saadaval</string>
+ <string name="module_installer_title">Laadi alla ja paigalda tuumamoodul</string>
+ <string name="module_installer_working">Allalaadimine ja paigaldamine…</string>
+ <string name="module_version_error">Tuumamooduli versiooni tuvastamine ebaõnnestus</string>
+ <string name="mtu">MTU</string>
+ <string name="multiple_tunnels_summary_off">Ühe tunneli sisselülitamine lülitab ülejäänud välja</string>
+ <string name="multiple_tunnels_summary_on">Mitu tunnelit saavad olla samaaegselt sisselülitatud</string>
+ <string name="multiple_tunnels_title">Luba mitu samaaegset tunnelit</string>
+ <string name="name">Nimi</string>
+ <string name="no_config_error">Üritan käivitada tunnelit ilma konfiguratsioonita</string>
+ <string name="no_configs_error">Seadistusi ei leitud</string>
+ <string name="no_tunnels_error">Tunneleid ei ole</string>
+ <string name="parse_error_generic">string</string>
+ <string name="parse_error_inet_address">IP-aadress</string>
+ <string name="parse_error_inet_endpoint">lõpp-punkt</string>
+ <string name="parse_error_inet_network">IP võrk</string>
+ <string name="parse_error_integer">number</string>
+ <string name="parse_error_reason">Ei saa töödelda %1$s “%2$s”</string>
+ <string name="peer">Partner</string>
+ <string name="permission_description">kontrollida WireGuard tunneleid, neid sisse ja välja lülitades, potentsiaalselt võrguliiklust kõrvale juhtides</string>
+ <string name="permission_label">kontrollida WireGuard tunneleid</string>
+ <string name="persistent_keepalive">Püsiv ühendushoidik</string>
+ <string name="pre_shared_key">Eeljagatud võti</string>
+ <string name="pre_shared_key_enabled">lubatud</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>
+ <string name="save">Salvesta</string>
+ <string name="select_all">Vali kõik</string>
+ <string name="settings">Seaded</string>
+ <string name="shell_exit_status_read_error">Kest ei saa lugeda väljundstaatust</string>
+ <string name="shell_marker_count_error">Kest ootas 4 markerit, saadi %d</string>
+ <string name="shell_start_error">Kesta käivitumine ebaõnnestus: %d</string>
+ <string name="success_application_will_restart">Tegevus õnnestus. Rakendus taaskäivitub…</string>
+ <string name="toggle_all">Vaheta kõik</string>
+ <string name="toggle_error">WireGuard tunneli lülitamine ebaõnnestus: %s</string>
+ <string name="tools_installer_already">wg ja wg-quick on juba paigaldatud</string>
+ <string name="tools_installer_failure">Käsurea tööriistade paigaldamine ebaõnnestus (juurkasutaja õigused puuduvad?)</string>
+ <string name="tools_installer_initial">Paigalda täiendavad tööriistad skriptimiseks</string>
+ <string name="tools_installer_initial_magisk">Paigalda täiendavad tööriistad skriptimiseks Magisk moodulina</string>
+ <string name="tools_installer_initial_system">Paigalda täiendavad tööriistad skriptimiseks süsteemipartitsioonile</string>
+ <string name="tools_installer_success_magisk">wg ja wg-quick paigaldatud Magisk moodulina (vajalik taaskäivitus)</string>
+ <string name="tools_installer_success_system">wg ja wg-quick paigaldatud süsteemipartitsioonile</string>
+ <string name="tools_installer_title">Paigalda käsurea tööriistad</string>
+ <string name="tools_installer_working">Paigaldatakse wg ja wg-quick</string>
+ <string name="tools_unavailable_error">Vajalikud tööriistad ei ole saadaval</string>
+ <string name="transfer">Andmemaht</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">sisse: %1$s, välja: %2$s</string>
+ <string name="transfer_tibibytes">%.2f TiB</string>
+ <string name="tun_create_error">Tunnelit ei saa luua</string>
+ <string name="tunnel_config_error">Tunneli seadistamine ebaõnnestus (wg-quick tagastas %d)</string>
+ <string name="tunnel_create_error">Tunneli loomine ebaõnnestus: %s</string>
+ <string name="tunnel_create_success">Tunnel \"%s\" lisatud</string>
+ <string name="tunnel_error_already_exists">Tunnel \"%s\" on juba olemas</string>
+ <string name="tunnel_error_invalid_name">Sobimatu nimi</string>
+ <string name="tunnel_list_placeholder">Lisa tunnel alloleva nupu abil</string>
+ <string name="tunnel_name">Tunneli nimi</string>
+ <string name="tunnel_on_error">Tunneli sisselülitamine ebaõnnestus (wgTurnOn tagastas %d)</string>
+ <string name="tunnel_dns_failure">Ei saa lahendada DNS hostinime: \"%s\"</string>
+ <string name="tunnel_rename_error">Tunneli ümbernimetamine ebaõnnestus: %s</string>
+ <string name="tunnel_rename_success">Tunnel edukalt ümbernimetatud \"%s\" -iks</string>
+ <string name="type_name_go_userspace">Go kasutajamaa</string>
+ <string name="type_name_kernel_module">Tuumamoodul</string>
+ <string name="unknown_error">Tundmatu viga</string>
+ <string name="updater_avalable">Rakenduse uuendus on saadaval. Palun uuenda nüüd.</string>
+ <string name="updater_action">Laadi alla ja uuenda</string>
+ <string name="updater_rechecking">Uuenduse andmete laadimine…</string>
+ <string name="updater_download_progress">Uuenduse allalaadimine: %1$s / %2$s (%3$.2f%%)</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>
+ <string name="version_title">WireGuard Androidile v%s</string>
+ <string name="vpn_not_authorized_error">Kasutaja ei lubanud VPN teenust</string>
+ <string name="vpn_start_error">Androidi VPN teenuse käivitamine ebaõnnestus</string>
+ <string name="zip_export_error">Tunnelite eksportimine ebaõnnestus: %s</string>
+ <string name="zip_export_success">Salvestatud faili \"%s\"</string>
+ <string name="zip_export_summary">Zip fail salvestatakse allalaadimiste kausta</string>
+ <string name="zip_export_title">Ekspordi tunnelid zip-faili</string>
+ <string name="biometric_prompt_zip_exporter_title">Autendi tunnelite eksportimiseks</string>
+ <string name="biometric_prompt_private_key_title">Autendi privaatvõtme vaatamiseks</string>
+ <string name="biometric_auth_error">Autentimine ebaõnnestus</string>
+ <string name="biometric_auth_error_reason">Autentimine ebaõnnestus: %s</string>
+</resources>
diff --git a/ui/src/main/res/values-fa-rIR/strings.xml b/ui/src/main/res/values-fa-rIR/strings.xml
index cc3df4de..e9000214 100644
--- a/ui/src/main/res/values-fa-rIR/strings.xml
+++ b/ui/src/main/res/values-fa-rIR/strings.xml
@@ -6,7 +6,7 @@
</plurals>
<plurals name="delete_success">
<item quantity="one">%d تونل با موقیت حذف شد</item>
- <item quantity="other">%d تونل‌ها با موقیت حذف شدند</item>
+ <item quantity="other">%d تونل‌ با موفقیت حذف شد</item>
</plurals>
<plurals name="delete_title">
<item quantity="one">%d تونل انتخاب شد</item>
@@ -80,6 +80,8 @@
<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="error_no_qr_found">کد QR در تصویر یافت نشد</string>
+ <string name="error_qr_checksum">بررسی checksum کد QR ناموفق بود</string>
<string name="cancel">لغو</string>
<string name="config_delete_error">نمی‌توان پرونده پیکربندی %s را حذف کرد</string>
<string name="config_exists_error">پیکربندی برای ”%s” در حال حاضر وجود دارد</string>
@@ -97,6 +99,7 @@
<string name="create_output_dir_error">نمی‌توان دایرکتوری خروجی را ایجاد کرد</string>
<string name="create_temp_dir_error">نمی‌توان دایرکتوری موقت محلی را ساخت</string>
<string name="create_tunnel">ساختن تونل</string>
+ <string name="copied_to_clipboard">%s در کلیپ‌بورد کپی شد</string>
<string name="dark_theme_summary_off">اکنون از پوسته روشن(روز) استفاده می‌شود</string>
<string name="dark_theme_summary_on">اکنون از پوسته تاریک(شب) استفاده می‌شود</string>
<string name="dark_theme_title">استفاده از پوسته تاریک</string>
@@ -108,6 +111,7 @@
<string name="disable_config_export_title">غیرفعال سازی خروجی گرفتن از کانفیگ ها</string>
<string name="disable_config_export_description">غیرفعال سازی خروجی گرفتن، دسترسی کلیدهای خصوصی را کم می کند</string>
<string name="dns_servers">سرورهای DNS</string>
+ <string name="dns_search_domains">جست‌وجوی دامنه‌ها</string>
<string name="edit">ویرایش</string>
<string name="endpoint">نقطه پایان</string>
<string name="error_down">خطا هنگام بستن تونل: %s</string>
@@ -211,14 +215,15 @@
<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_dns_failure">ناتوان در یافتن DNS نام میزبان: \"%s\"</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>
diff --git a/ui/src/main/res/values-fi-rFI/strings.xml b/ui/src/main/res/values-fi-rFI/strings.xml
index e0db2c04..d1a714e5 100644
--- a/ui/src/main/res/values-fi-rFI/strings.xml
+++ b/ui/src/main/res/values-fi-rFI/strings.xml
@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
+ <plurals name="import_total_success">
+ <item quantity="one">Tuotiin %d tunneli</item>
+ <item quantity="other">Tuotiin %d tunnelia</item>
+ </plurals>
<plurals name="persistent_keepalive_seconds_unit">
<item quantity="one">joka sekunti</item>
<item quantity="other">%d sekunnin välein</item>
@@ -14,6 +18,9 @@
<string name="applications">Sovellukset</string>
<string name="allowed_ips">Sallitut IP-osoitteet</string>
<string name="bad_config_context_top_level">%s</string>
+ <string name="bad_config_explanation_pka">: Oltava positiivinen ja enintään 65535</string>
+ <string name="bad_config_explanation_positive_number">: Oltava positiivinen</string>
+ <string name="bad_config_explanation_udp_port">: Oltava kelvollinen UDP portin numero</string>
<string name="bad_config_reason_invalid_key">Virheellinen avain</string>
<string name="bad_config_reason_invalid_number">Virheellinen luku</string>
<string name="bad_config_reason_invalid_value">Virheellinen arvo</string>
@@ -25,10 +32,18 @@
<string name="bad_config_reason_value_out_of_range">Arvo alueen ulkopuolella</string>
<string name="bad_extension_error">Tiedoston on oltava .conf tai .zip</string>
<string name="cancel">Peruuta</string>
+ <string name="config_delete_error">Asetustiedostoa %s ei voi poistaa</string>
<string name="config_not_found_error">Asetustiedostoa “%s” ei löydy</string>
<string name="config_rename_error">Asetustiedostoa \"%s\" ei voi nimetä uudelleen</string>
<string name="config_save_success">Asetustiedosto \"%s\" tallennettu onnistuneesti</string>
<string name="create_activity_title">Luo WireGuard Tunnel</string>
+ <string name="dark_theme_title">Käytä tummaa teemaa</string>
+ <string name="delete">Poista</string>
+ <string name="tv_delete">Valitse poistettava tunneli</string>
+ <string name="dns_servers">DNS palvelimet</string>
+ <string name="dns_search_domains">Hakudomaini</string>
+ <string name="edit">Muokkaa</string>
+ <string name="endpoint">Päätepiste</string>
<string name="exclude_private_ips">Jätä pois yksityiset IP-osoitteet</string>
<string name="generate_new_private_key">Luo uusi yksityinen avain</string>
<string name="generic_error">Tuntematon ”%s” virhe</string>
@@ -47,12 +62,21 @@
<string name="key_length_explanation_binary">: WireGuard-avainten on oltava 32 tavua</string>
<string name="key_length_explanation_hex">: WireGuardin base64-avainten pituus on oltava 64 merkkiä (32 tavua)</string>
<string name="listen_port">Kuuntele porttia</string>
+ <string name="log_viewer_title">Loki</string>
+ <string name="module_installer_error">Jokin meni pieleen. Yritä uudelleen</string>
<string name="mtu">MTU</string>
+ <string name="name">Nimi</string>
<string name="no_configs_error">Asetuksia ei löydy</string>
<string name="no_tunnels_error">Tunneleita ei ole</string>
<string name="parse_error_generic">merkkijono</string>
<string name="parse_error_inet_address">IP-osoite</string>
<string name="parse_error_inet_network">IP-verkko</string>
+ <string name="peer">Osapuoli</string>
+ <string name="pre_shared_key_enabled">käytössä</string>
+ <string name="private_key">Yksityinen avain</string>
+ <string name="public_key">Julkinen avain</string>
+ <string name="save">Tallenna</string>
+ <string name="settings">Asetukset</string>
<string name="transfer_bytes">%d B</string>
<string name="transfer_gibibytes">%.2f GiB</string>
<string name="transfer_kibibytes">%.2f KiB</string>
@@ -60,4 +84,7 @@
<string name="transfer_rx_tx">rx: %1$s, tx: %2$s</string>
<string name="transfer_tibibytes">%.2f TiB</string>
<string name="tunnel_name">Tunnelin nimi</string>
+ <string name="version_summary_unknown">Tuntematon %s versio</string>
+ <string name="biometric_auth_error">Varmennusvirhe</string>
+ <string name="biometric_auth_error_reason">Varmennusvirhe: %s</string>
</resources>
diff --git a/ui/src/main/res/values-fr/strings.xml b/ui/src/main/res/values-fr/strings.xml
index 33f413b1..d4d4b95f 100644
--- a/ui/src/main/res/values-fr/strings.xml
+++ b/ui/src/main/res/values-fr/strings.xml
@@ -6,7 +6,7 @@
</plurals>
<plurals name="delete_success">
<item quantity="one">Suppression réussie du tunnel %d</item>
- <item quantity="other">Supprimé avec succès %d tunnels</item>
+ <item quantity="other">%d tunnels supprimés avec succès</item>
</plurals>
<plurals name="delete_title">
<item quantity="one">%d tunnel sélectionné</item>
@@ -79,20 +79,22 @@
<string name="bad_config_reason_unknown_section">Section inconnue</string>
<string name="bad_config_reason_value_out_of_range">Valeur hors limite</string>
<string name="bad_extension_error">Le fichier doit être .conf ou .zip</string>
+ <string name="error_no_qr_found">Le code QR est introuvable dans l’image</string>
+ <string name="error_qr_checksum">La vérification de la somme de contrôle du QR code a échoué</string>
<string name="cancel">Annuler</string>
<string name="config_delete_error">Impossible de supprimer le fichier de configuration %s</string>
<string name="config_exists_error">La configuration de « %s » existe déjà</string>
<string name="config_file_exists_error">Le fichier de configuration « %s » existe déjà</string>
- <string name="config_not_found_error">Fichier de configuration «%s» introuvable</string>
- <string name="config_rename_error">Impossible de renommer le fichier de configuration «%s»</string>
- <string name="config_save_error">Impossible d’enregistrer la configuration pour «%1$s» : %2$s</string>
- <string name="config_save_success">Configuration enregistrée avec succès pour “%s”</string>
+ <string name="config_not_found_error">Fichier de configuration « %s » introuvable</string>
+ <string name="config_rename_error">Impossible de renommer le fichier de configuration « %s »</string>
+ <string name="config_save_error">Impossible d’enregistrer la configuration pour « %1$s » : %2$s</string>
+ <string name="config_save_success">Configuration enregistrée avec succès pour « %s »</string>
<string name="create_activity_title">Créer un tunnel WireGuard</string>
<string name="create_bin_dir_error">Impossible de créer le répertoire binaire local</string>
<string name="create_downloads_file_error">Impossible de créer le fichier dans le répertoire des téléchargements</string>
<string name="create_empty">Créer à partir de zéro</string>
<string name="create_from_file">Importer depuis un fichier ou une archive</string>
- <string name="create_from_qr_code">Créer avec un scan de QR code</string>
+ <string name="create_from_qr_code">Importer depuis un QR code</string>
<string name="create_output_dir_error">Impossible de créer le répertoire de sortie</string>
<string name="create_temp_dir_error">Impossible de créer le répertoire temporaire local</string>
<string name="create_tunnel">Créer un tunnel</string>
@@ -105,14 +107,19 @@
<string name="tv_select_a_storage_drive">Sélectionner un disque de stockage</string>
<string name="tv_no_file_picker">Veuillez installer un utilitaire de gestion de fichiers pour parcourir les fichiers</string>
<string name="tv_add_tunnel_get_started">Ajouter un tunnel pour commencer</string>
+ <string name="donate_title">♥ Faire un don au projet WireGuard</string>
+ <string name="donate_summary">Chaque contribution aide</string>
+ <string name="donate_google_play_disappointment">Merci de votre soutien au projet WireGuard !\n\nMalheureusement, en raisons des politiques de Google, nous ne pouvons pas vous rediriger vers la page vous permettant de faire un don. Heureusement, vous pouvez le trouver par vous-même !\n\nMerci encore pour votre soutien.</string>
<string name="disable_config_export_title">Désactiver l\'export de configuration</string>
<string name="disable_config_export_description">La désactivation de l\'export de configuration rend les clés privées moins accessibles</string>
<string name="dns_servers">Serveurs DNS</string>
+ <string name="dns_search_domains">Domaines de recherche DNS</string>
<string name="edit">Modifier</string>
<string name="endpoint">Point de terminaison</string>
<string name="error_down">Erreur lors de la désactivation du tunnel : %s</string>
<string name="error_fetching_apps">Erreur lors de la récupération de la liste d\'applications : %s</string>
<string name="error_root">Veuillez obtenir l\'accès root et essayez à nouveau</string>
+ <string name="error_prepare">Erreur lors de la préparation du tunnel : %s</string>
<string name="error_up">Erreur lors de la mise en place du tunnel : %s</string>
<string name="exclude_private_ips">Exclure les IPs privées</string>
<string name="generate_new_private_key">Générer une nouvelle clé privée</string>
@@ -132,6 +139,8 @@
<string name="key_length_explanation_base64">: Les clés base64 WireGuard doivent comporter 44 caractères (32 octets)</string>
<string name="key_length_explanation_binary">: Les clés WireGuard doivent comporter 32 octets</string>
<string name="key_length_explanation_hex">: Les clés hexadécimales WireGuard doivent comporter 64 caractères (32 octets)</string>
+ <string name="latest_handshake">Dernière liaison</string>
+ <string name="latest_handshake_ago">Il y a %s</string>
<string name="listen_port">Port d\'écoute</string>
<string name="log_export_error">Impossible d\'exporter le journal : %s</string>
<string name="log_export_subject">Fichier journal d\'Android WireGuard</string>
@@ -175,6 +184,10 @@
<string name="private_key">Clé privée</string>
<string name="public_key">Clé publique</string>
<string name="qr_code_hint">Astuce : générez avec \"qrencode -t ansiutf8 &lt; tunnel.conf\".</string>
+ <string name="quick_settings_tile_add_title">Ajouter une bascule au volet des paramètres</string>
+ <string name="quick_settings_tile_add_summary">Cette bascule active le dernier tunnel utilisé</string>
+ <string name="quick_settings_tile_add_failure">Impossible d\'ajouter la bascule : erreur %d</string>
+ <string name="quick_settings_tile_action">Activer le tunnel</string>
<string name="restore_on_boot_summary_off">N\'affichera pas les tunnels activés au démarrage</string>
<string name="restore_on_boot_summary_on">Les tunnels activés seront affichés au démarrage</string>
<string name="restore_on_boot_title">Restaurer au démarrage</string>
@@ -210,15 +223,25 @@
<string name="tunnel_create_success">Tunnel «%s » créé avec succès</string>
<string name="tunnel_error_already_exists">Le tunnel « %s » existe déjà</string>
<string name="tunnel_error_invalid_name">Nom invalide</string>
- <string name="tunnel_list_placeholder">Ajouter un tunnel en utilisant le bouton bleu</string>
+ <string name="tunnel_list_placeholder">Ajoutez un tunnel en utilisant le bouton ci-dessous</string>
<string name="tunnel_name">Nom du tunnel</string>
<string name="tunnel_on_error">Impossible d\'activer le tunnel (wgTurnOn a retourné %d)</string>
<string name="tunnel_dns_failure">Impossible de résoudre le nom d\'hôte DNS: “%s”</string>
<string name="tunnel_rename_error">Impossible de renommer le tunnel : %s</string>
<string name="tunnel_rename_success">Tunnel renommé avec succès en «%s »</string>
- <string name="type_name_go_userspace">Nom de l\'espace utilisateur</string>
+ <string name="type_name_go_userspace">Implémentation Go en espace utilisateur</string>
<string name="type_name_kernel_module">Module noyau</string>
<string name="unknown_error">Erreur inconnue</string>
+ <string name="updater_avalable">Une mise à jour est disponible. Veuillez mettre l\'application à jour.</string>
+ <string name="updater_action">Télécharger &amp; Mettre à jour</string>
+ <string name="updater_rechecking">Récupération des métadonnées de la mise à jour…</string>
+ <string name="updater_download_progress">Téléchargement de la mise à jour : %1$s / %2$s (%3$.2f%%)</string>
+ <string name="updater_download_progress_nototal">Téléchargement de la mise à jour : %s</string>
+ <string name="updater_installing">Installation de la mise à jour…</string>
+ <string name="updater_failure">Erreur lors de la mise à jour : %s. Nous réessaierons dans un instant…</string>
+ <string name="updater_corrupt_title">Application corrompue</string>
+ <string name="updater_corrupt_message">Cette application est corrompue. Veuillez réinstaller le fichier APK depuis le site ci-dessous. Ensuite, désinstallez cette application puis réinstallez-la à l\'aide du fichier APK téléchargé.</string>
+ <string name="updater_corrupt_navigate">Accéder au site internet</string>
<string name="version_summary">%1$s backend %2$s</string>
<string name="version_summary_checking">Vérification de la version %s du backend</string>
<string name="version_summary_unknown">Version %s inconnue</string>
diff --git a/ui/src/main/res/values-hi-rIN/strings.xml b/ui/src/main/res/values-hi-rIN/strings.xml
index b614b475..737f83a8 100644
--- a/ui/src/main/res/values-hi-rIN/strings.xml
+++ b/ui/src/main/res/values-hi-rIN/strings.xml
@@ -76,6 +76,7 @@
<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="error_no_qr_found">छवि में क्यूआर कोड नहीं मिला</string>
<string name="cancel">रद्द</string>
<string name="config_delete_error">कॉन्फ़िगरेशन फ़ाइल %s को नहीं हटा सकता</string>
<string name="config_exists_error">“%s” के लिए कॉन्फ़िगरेशन पहले से मौजूद है</string>
@@ -194,7 +195,6 @@
<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>
@@ -202,6 +202,7 @@
<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 बैकएंड %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>
diff --git a/ui/src/main/res/values-hi/strings.xml b/ui/src/main/res/values-hi/strings.xml
index 1d354ede..1566d7a0 100644
--- a/ui/src/main/res/values-hi/strings.xml
+++ b/ui/src/main/res/values-hi/strings.xml
@@ -154,7 +154,6 @@
<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>
diff --git a/ui/src/main/res/values-hu-rHU/strings.xml b/ui/src/main/res/values-hu-rHU/strings.xml
new file mode 100644
index 00000000..bdc19fe3
--- /dev/null
+++ b/ui/src/main/res/values-hu-rHU/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="all_applications">Minden alkalmazás</string>
+ <string name="exclude_from_tunnel">Kizárás</string>
+ <string name="addresses">Címek</string>
+ <string name="applications">Alkalmazások</string>
+ <string name="allowed_ips">Engedélyezett IP-k</string>
+ <string name="bad_config_reason_invalid_key">Érvénytelen kulcs</string>
+ <string name="bad_config_reason_invalid_value">Helytelen érték</string>
+ <string name="bad_config_reason_syntax_error">Szintaktikai hiba</string>
+ <string name="bad_config_reason_unknown_section">Ismeretlen szekció</string>
+ <string name="bad_config_reason_value_out_of_range">Az érték a megengedett tartományon kívül van</string>
+ <string name="bad_extension_error">A fájl .conf vagy .zip legyen</string>
+ <string name="config_not_found_error">Konfigurációs állomány \"%s\" nem található meg</string>
+ <string name="dns_servers">DNS szerverek</string>
+ <string name="dns_search_domains">Domain keresés</string>
+ <string name="edit">Szerkesztés</string>
+ <string name="endpoint">Végpont</string>
+ <string name="log_saver_activity_label">Log mentése</string>
+ <string name="log_viewer_pref_title">Alkalmazás log megtekintése</string>
+ <string name="name">Név</string>
+ <string name="parse_error_inet_address">IP cím</string>
+ <string name="parse_error_inet_network">IP hálózat</string>
+ <string name="parse_error_integer">szám</string>
+ <string name="pre_shared_key_enabled">engedélyezve</string>
+ <string name="public_key">Nyilvános kulcs</string>
+ <string name="quick_settings_tile_action">Csatorna átkapcsolása</string>
+ <string name="save">Mentés</string>
+ <string name="select_all">Összes kijelölése</string>
+ <string name="settings">Beállítások</string>
+ <string name="toggle_all">Összes átkapcsolása</string>
+ <string name="tunnel_name">Csatorna neve</string>
+ <string name="type_name_kernel_module">Kernel modul</string>
+ <string name="unknown_error">Ismeretlen hiba</string>
+ <string name="updater_corrupt_navigate">Weboldal megnyitása</string>
+</resources>
diff --git a/ui/src/main/res/values-in/strings.xml b/ui/src/main/res/values-in/strings.xml
index 7852ffd6..d7dd3d1a 100644
--- a/ui/src/main/res/values-in/strings.xml
+++ b/ui/src/main/res/values-in/strings.xml
@@ -66,6 +66,8 @@
<string name="bad_config_reason_unknown_section">Bagian tak diketahui</string>
<string name="bad_config_reason_value_out_of_range">Nilai di luar rentang</string>
<string name="bad_extension_error">Berkas harus .conf atau .zip</string>
+ <string name="error_no_qr_found">Kode QR tidak ditemukan dalam gambar</string>
+ <string name="error_qr_checksum">Verifikasi ceksum kode QR gagal</string>
<string name="cancel">Batalkan</string>
<string name="config_delete_error">Tidak dapat menghapus berkas konfigurasi %s</string>
<string name="config_exists_error">Sudah ada konfigurasi untuk “%s”</string>
@@ -95,6 +97,7 @@
<string name="disable_config_export_title">Nonaktifkan ekspor konfigurasi</string>
<string name="disable_config_export_description">Menonaktifkan ekspor konfigurasi akan membuat kunci pribadi sulit diakses</string>
<string name="dns_servers">Server DNS</string>
+ <string name="dns_search_domains">Cari domain</string>
<string name="edit">Edit</string>
<string name="endpoint">Endpoint</string>
<string name="error_down">Kesalahan pada tunel: %s</string>
@@ -197,7 +200,6 @@
<string name="tunnel_create_success">Tunel “%s” Berhasil dibuat</string>
<string name="tunnel_error_already_exists">Tunel “%s” sudah ada</string>
<string name="tunnel_error_invalid_name">Nama tidak valid</string>
- <string name="tunnel_list_placeholder">Tambahkan tunel menggunakan tombol +</string>
<string name="tunnel_name">Nama tunel</string>
<string name="tunnel_on_error">Tidak dapat mengaktifkan tunel (wgTurnOn %d dikembalikan)</string>
<string name="tunnel_dns_failure">Tidak bisa mencari nama host DNS \"%s\"</string>
diff --git a/ui/src/main/res/values-it/strings.xml b/ui/src/main/res/values-it/strings.xml
index 519d4b52..8fba2592 100644
--- a/ui/src/main/res/values-it/strings.xml
+++ b/ui/src/main/res/values-it/strings.xml
@@ -25,7 +25,7 @@
<item quantity="other">%d applicazioni escluse</item>
</plurals>
<plurals name="set_included_applications">
- <item quantity="one">%d applicazione esclusa</item>
+ <item quantity="one">%d applicazione inclusa</item>
<item quantity="other">%d applicazioni incluse</item>
</plurals>
<plurals name="n_excluded_applications">
@@ -33,7 +33,7 @@
<item quantity="other">%d escluse</item>
</plurals>
<plurals name="n_included_applications">
- <item quantity="one">%d escluse</item>
+ <item quantity="one">%d inclusa</item>
<item quantity="other">%d incluse</item>
</plurals>
<string name="all_applications">Tutte le applicazioni</string>
@@ -59,9 +59,9 @@
<string name="add_peer">Aggiungi peer</string>
<string name="addresses">Indirizzi</string>
<string name="applications">Applicazioni</string>
- <string name="allow_remote_control_intents_summary_off">Le applicazioni esterne non possono attivare tunnel (consigliato)</string>
- <string name="allow_remote_control_intents_summary_on">Le applicazioni esterne possono attivare tunnel (avanzato)</string>
- <string name="allow_remote_control_intents_title">Consenti applicazioni di controllo remoto</string>
+ <string name="allow_remote_control_intents_summary_off">Le app esterne non possono attivare tunnel (consigliato)</string>
+ <string name="allow_remote_control_intents_summary_on">Le app esterne possono attivare tunnel (avanzato)</string>
+ <string name="allow_remote_control_intents_title">Consenti app di controllo remoto</string>
<string name="allowed_ips">IP consentiti</string>
<string name="bad_config_context">%2$s di %1$s</string>
<string name="bad_config_context_top_level">%s</string>
@@ -79,6 +79,8 @@
<string name="bad_config_reason_unknown_section">Sezione sconosciuta</string>
<string name="bad_config_reason_value_out_of_range">Valore fuori scala</string>
<string name="bad_extension_error">Il file deve essere .conf o .zip</string>
+ <string name="error_no_qr_found">Codice QR non trovato nell\'immagine</string>
+ <string name="error_qr_checksum">Verifica checksum del codice QR fallita</string>
<string name="cancel">Annulla</string>
<string name="config_delete_error">Impossibile eliminare il file di configurazione %s</string>
<string name="config_exists_error">La configurazione per “%s” esiste già</string>
@@ -92,7 +94,7 @@
<string name="create_downloads_file_error">Impossibile creare il file nella cartella di download</string>
<string name="create_empty">Crea da zero</string>
<string name="create_from_file">Importa da file o archivio</string>
- <string name="create_from_qr_code">Scansione da codice QR</string>
+ <string name="create_from_qr_code">Scansiona da codice QR</string>
<string name="create_output_dir_error">Impossibile creare la cartella di output</string>
<string name="create_temp_dir_error">Impossibile creare la cartella locale temporanea</string>
<string name="create_tunnel">Crea tunnel</string>
@@ -105,21 +107,26 @@
<string name="tv_select_a_storage_drive">Seleziona un\'unità di archiviazione</string>
<string name="tv_no_file_picker">Installa un\'utilità di gestione file per sfogliare i file</string>
<string name="tv_add_tunnel_get_started">Aggiungi un tunnel per iniziare</string>
+ <string name="donate_title">♥ Dona al progetto WireGuard</string>
+ <string name="donate_summary">Ogni contributo aiuta</string>
+ <string name="donate_google_play_disappointment">Grazie per il sostegno al progetto WireGuard!\n\nPurtroppo, a causa delle politiche di Google, non siamo autorizzati a linkare la pagina del progetto dove puoi fare una donazione. Speriamo che la troverai!\n\nGrazie ancora per il tuo contributo.</string>
<string name="disable_config_export_title">Disattiva esportazione config</string>
<string name="disable_config_export_description">Disabilitare l\'esportazione della configurazione rende le chiavi private meno accessibili</string>
<string name="dns_servers">Server DNS</string>
+ <string name="dns_search_domains">Domini di ricerca DNS</string>
<string name="edit">Modifica</string>
<string name="endpoint">Endpoint</string>
<string name="error_down">Errore di disattivazione del tunnel: %s</string>
<string name="error_fetching_apps">Errore di recupero dell\'elenco applicazioni: %s</string>
<string name="error_root">Accedi come root e riprova</string>
+ <string name="error_prepare">Errore di preparazione del tunnel: %s</string>
<string name="error_up">Errore di attivazione del tunnel: %s</string>
<string name="exclude_private_ips">Escludi IP privati</string>
<string name="generate_new_private_key">Genera nuova chiave privata</string>
<string name="generic_error">Errore “%s” sconosciuto</string>
<string name="hint_automatic">(auto)</string>
- <string name="hint_generated">(generato)</string>
- <string name="hint_optional">(facoltativo)</string>
+ <string name="hint_generated">(generata)</string>
+ <string name="hint_optional">(facoltativa)</string>
<string name="hint_optional_discouraged">(facoltativo, non consigliato)</string>
<string name="hint_random">(casuale)</string>
<string name="illegal_filename_error">Nome file “%s” non valido</string>
@@ -132,6 +139,8 @@
<string name="key_length_explanation_base64">: le chiavi base64 di WireGuard devono essere di 44 caratteri (32 byte)</string>
<string name="key_length_explanation_binary">: le chiavi di WireGuard devono essere di 32 byte</string>
<string name="key_length_explanation_hex">: le chiavi esadecimali di WireGuard devono essere di 64 caratteri (32 byte)</string>
+ <string name="latest_handshake">Ultima negoziazione</string>
+ <string name="latest_handshake_ago">%s fa</string>
<string name="listen_port">Porta in ascolto</string>
<string name="log_export_error">Impossibile esportare il log: %s</string>
<string name="log_export_subject">File di log WireGuard Android</string>
@@ -175,6 +184,10 @@
<string name="private_key">Chiave privata</string>
<string name="public_key">Chiave pubblica</string>
<string name="qr_code_hint">Suggerimento: genera con `qrencode -t ansiutf8 &lt; tunnel.conf`.</string>
+ <string name="quick_settings_tile_add_title">Aggiungi riquadro ale impostazioni rapide</string>
+ <string name="quick_settings_tile_add_summary">La scorciatoia attiva/disattiva il tunnel più recente</string>
+ <string name="quick_settings_tile_add_failure">Impossibile aggiungere la scorciatoia: errore %d</string>
+ <string name="quick_settings_tile_action">Attiva/disattiva tunnel</string>
<string name="restore_on_boot_summary_off">Non attiverà i tunnel configurati all\'avvio</string>
<string name="restore_on_boot_summary_on">Attiverà i tunnel configurati all\'avvio</string>
<string name="restore_on_boot_title">Ripristina all\'avvio</string>
@@ -210,7 +223,7 @@
<string name="tunnel_create_success">Tunnel “%s” creato correttamente</string>
<string name="tunnel_error_already_exists">Il tunnel “%s” esiste già</string>
<string name="tunnel_error_invalid_name">Nome non valido</string>
- <string name="tunnel_list_placeholder">Aggiungi un tunnel usando il pulsante blu</string>
+ <string name="tunnel_list_placeholder">Aggiungi un tunnel usando il pulsante sotto</string>
<string name="tunnel_name">Nome tunnel</string>
<string name="tunnel_on_error">Impossibile attivare il tunnel (wgTurnOn ha risposto %d)</string>
<string name="tunnel_dns_failure">Impossibile risolve il nome di domino: \"%s\"</string>
@@ -219,6 +232,16 @@
<string name="type_name_go_userspace">Spazio utente Go</string>
<string name="type_name_kernel_module">Modulo kernel</string>
<string name="unknown_error">Errore sconosciuto</string>
+ <string name="updater_avalable">È disponibile un aggiornamento dell\'app. Si prega di aggiornare ora.</string>
+ <string name="updater_action">Scarica e aggiorna</string>
+ <string name="updater_rechecking">Recupero metadati aggiornamento…</string>
+ <string name="updater_download_progress">Scaricamento aggiornamento: %1$s / %2$s (%3$.2f%%)</string>
+ <string name="updater_download_progress_nototal">Scaricamento aggiornamento: %s</string>
+ <string name="updater_installing">Installazione aggiornamento…</string>
+ <string name="updater_failure">Aggiornamento fallito: %s. Riprovo momentaneamente…</string>
+ <string name="updater_corrupt_title">Applicazione danneggiata</string>
+ <string name="updater_corrupt_message">Questa applicazione è danneggiata. Riscarica l\'APK dal sito collegato qui sotto. Dopo, disinstalla questa applicazione e reinstallala dall\'APK scaricato.</string>
+ <string name="updater_corrupt_navigate">Apri sito web</string>
<string name="version_summary">Backend %1$s %2$s</string>
<string name="version_summary_checking">Controllo versione backend %s</string>
<string name="version_summary_unknown">Versione %s sconosciuta</string>
diff --git a/ui/src/main/res/values-ja/strings.xml b/ui/src/main/res/values-ja/strings.xml
index e10a05a8..26e99af3 100644
--- a/ui/src/main/res/values-ja/strings.xml
+++ b/ui/src/main/res/values-ja/strings.xml
@@ -66,6 +66,8 @@
<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="error_no_qr_found">QRコードが見つかりません</string>
+ <string name="error_qr_checksum">QRコードのチェックサムの確認に失敗しました</string>
<string name="cancel">キャンセル</string>
<string name="config_delete_error">設定ファイル %s を削除できません</string>
<string name="config_exists_error">\"%s\" の定義はすでに存在します</string>
@@ -92,14 +94,19 @@
<string name="tv_select_a_storage_drive">ストレージを選択</string>
<string name="tv_no_file_picker">ファイルを参照するにはファイル管理アプリをインストールしてください</string>
<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>
+ <string name="dns_search_domains">サーチドメイン</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_prepare">トンネル準備中エラー: %s</string>
<string name="error_up">トンネル起動時エラー: %s</string>
<string name="exclude_private_ips">プライベート IP アドレスを除外</string>
<string name="generate_new_private_key">新しい秘密鍵を生成する</string>
@@ -119,6 +126,8 @@
<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 hex 鍵は64文字 (32バイト) でなければなりません</string>
+ <string name="latest_handshake">直近のハンドシェイク</string>
+ <string name="latest_handshake_ago">%s 前</string>
<string name="listen_port">Listen ポート</string>
<string name="log_export_error">ログをエクスポートできません: %s</string>
<string name="log_export_subject">WireGuard Android ログファイル</string>
@@ -162,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>
@@ -197,7 +210,7 @@
<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_list_placeholder">下のボタンを使用してトンネルを追加</string>
<string name="tunnel_name">トンネル名</string>
<string name="tunnel_on_error">トンネルを有効にできません (wgTurnOn が %d を返却)</string>
<string name="tunnel_dns_failure">DNSホスト名を解決できませんでした: “%s”</string>
@@ -206,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-ko-rKR/strings.xml b/ui/src/main/res/values-ko-rKR/strings.xml
index c975945a..0f42c7a8 100644
--- a/ui/src/main/res/values-ko-rKR/strings.xml
+++ b/ui/src/main/res/values-ko-rKR/strings.xml
@@ -66,6 +66,8 @@
<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="error_no_qr_found">이미지에서 QR 코드를 찾을 수 없습니다</string>
+ <string name="error_qr_checksum">QR 코드 체크섬 검증 실패</string>
<string name="cancel">취소</string>
<string name="config_delete_error">설정파일 %s를 삭제할 수 없음</string>
<string name="config_exists_error">\"%s\"에 대한 설정이 이미 존재함</string>
@@ -95,6 +97,7 @@
<string name="disable_config_export_title">설정 내보내기 기능을 중지</string>
<string name="disable_config_export_description">설정 내보내기 기능을 중지하면 개인키 유출을 줄일 수 있음</string>
<string name="dns_servers">DNS 서버</string>
+ <string name="dns_search_domains">Dns 도메인 검색</string>
<string name="edit"> 수정</string>
<string name="endpoint">엔드포인트</string>
<string name="error_down">터널 중단 시 오류 발생: %s</string>
@@ -197,7 +200,6 @@
<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>
diff --git a/ui/src/main/res/values-night/colors.xml b/ui/src/main/res/values-night/colors.xml
deleted file mode 100644
index 586486da..00000000
--- a/ui/src/main/res/values-night/colors.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
- <!-- Base palette -->
- <color name="primary_color">#ff212121</color>
- <color name="primary_light_color">#ff484848</color>
- <color name="primary_dark_color">#ff000000</color>
- <color name="secondary_color">#ff4285f4</color>
- <color name="secondary_light_color">#ff80b4ff</color>
- <color name="secondary_dark_color">#ff0059c1</color>
- <color name="primary_text_color">#ffffffff</color>
- <color name="secondary_text_color">#ffffffff</color>
-
- <!-- Theme variables -->
- <color name="list_multiselect_background">#1aeeeeee</color>
- <color name="status_bar_color">@color/primary_color</color>
- <color name="navigation_bar_color">#aa212121</color>
-
- <!-- Log viewer tag colors -->
- <color name="debug_tag_color">#aaaaaa</color>
- <color name="error_tag_color">#ff0000</color>
- <color name="info_tag_color">#00ff00</color>
- <color name="warning_tag_color">#ffff00</color>
-</resources>
diff --git a/ui/src/main/res/values-night/logviewer_colors.xml b/ui/src/main/res/values-night/logviewer_colors.xml
new file mode 100644
index 00000000..a87312b4
--- /dev/null
+++ b/ui/src/main/res/values-night/logviewer_colors.xml
@@ -0,0 +1,6 @@
+<resources>
+ <color name="debug_tag_color">#aaaaaa</color>
+ <color name="error_tag_color">#ff0000</color>
+ <color name="info_tag_color">#00ff00</color>
+ <color name="warning_tag_color">#ffff00</color>
+</resources>
diff --git a/ui/src/main/res/values-night/themes.xml b/ui/src/main/res/values-night/themes.xml
new file mode 100644
index 00000000..e074cb92
--- /dev/null
+++ b/ui/src/main/res/values-night/themes.xml
@@ -0,0 +1,31 @@
+<resources>
+
+ <style name="WireGuardTheme" parent="Theme.Material3.Dark">
+ <item name="colorPrimary">@color/md_theme_dark_primary</item>
+ <item name="colorOnPrimary">@color/md_theme_dark_onPrimary</item>
+ <item name="colorPrimaryContainer">@color/md_theme_dark_primaryContainer</item>
+ <item name="colorOnPrimaryContainer">@color/md_theme_dark_onPrimaryContainer</item>
+ <item name="colorSecondary">@color/md_theme_dark_secondary</item>
+ <item name="colorOnSecondary">@color/md_theme_dark_onSecondary</item>
+ <item name="colorSecondaryContainer">@color/md_theme_dark_secondaryContainer</item>
+ <item name="colorOnSecondaryContainer">@color/md_theme_dark_onSecondaryContainer</item>
+ <item name="colorTertiary">@color/md_theme_dark_tertiary</item>
+ <item name="colorOnTertiary">@color/md_theme_dark_onTertiary</item>
+ <item name="colorTertiaryContainer">@color/md_theme_dark_tertiaryContainer</item>
+ <item name="colorOnTertiaryContainer">@color/md_theme_dark_onTertiaryContainer</item>
+ <item name="colorError">@color/md_theme_dark_error</item>
+ <item name="colorErrorContainer">@color/md_theme_dark_errorContainer</item>
+ <item name="colorOnError">@color/md_theme_dark_onError</item>
+ <item name="colorOnErrorContainer">@color/md_theme_dark_onErrorContainer</item>
+ <item name="android:colorBackground">@color/md_theme_dark_background</item>
+ <item name="colorOnBackground">@color/md_theme_dark_onBackground</item>
+ <item name="colorSurface">@color/md_theme_dark_surface</item>
+ <item name="colorOnSurface">@color/md_theme_dark_onSurface</item>
+ <item name="colorSurfaceVariant">@color/md_theme_dark_surfaceVariant</item>
+ <item name="colorOnSurfaceVariant">@color/md_theme_dark_onSurfaceVariant</item>
+ <item name="colorOutline">@color/md_theme_dark_outline</item>
+ <item name="colorOnSurfaceInverse">@color/md_theme_dark_inverseOnSurface</item>
+ <item name="colorSurfaceInverse">@color/md_theme_dark_inverseSurface</item>
+ <item name="colorPrimaryInverse">@color/md_theme_dark_inversePrimary</item>
+ </style>
+</resources>
diff --git a/ui/src/main/res/values-nl-rNL/strings.xml b/ui/src/main/res/values-nl-rNL/strings.xml
index eee46420..50317fe0 100644
--- a/ui/src/main/res/values-nl-rNL/strings.xml
+++ b/ui/src/main/res/values-nl-rNL/strings.xml
@@ -4,4 +4,237 @@
<item quantity="one">Kan %d tunnel niet verwijderen: %s</item>
<item quantity="other">Kan %d tunnels niet verwijderen: %s</item>
</plurals>
+ <plurals name="delete_success">
+ <item quantity="one">%d tunnel succesvol verwijderd</item>
+ <item quantity="other">%d tunnels succesvol verwijderd</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="one">%d tunnel geselecteerd</item>
+ <item quantity="other">%d tunnels geselecteerd</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="one">%1$d van %2$d tunnels geïmporteerd</item>
+ <item quantity="other">%1$d van de %2$d tunnels geïmporteerd</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="one">%d tunnel geïmporteerd</item>
+ <item quantity="other">%d tunnels geïmporteerd</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="one">%d uitgesloten applicatie(s)</item>
+ <item quantity="other">%d uitgesloten applicaties</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="one">%d inbegrepen applicatie</item>
+ <item quantity="other">%d inbegrepen applicaties</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="one">%d uitgesloten</item>
+ <item quantity="other">%d uitgesloten</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <item quantity="one">%d inbegrepen</item>
+ <item quantity="other">%d inbegrepen</item>
+ </plurals>
+ <string name="all_applications">Alle applicaties</string>
+ <string name="exclude_from_tunnel">Uitsluiten</string>
+ <string name="include_in_tunnel">Alleen opnemen</string>
+ <plurals name="include_n_applications">
+ <item quantity="one">Neem %d app op</item>
+ <item quantity="other">Voeg %d apps toe</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="one">%d app uitsluiten</item>
+ <item quantity="other">%d apps uitsluiten</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="one">iedere seconde</item>
+ <item quantity="other">iedere %d seconden</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="one">seconde</item>
+ <item quantity="other">seconden</item>
+ </plurals>
+ <string name="use_all_applications">Gebruik alle applicaties</string>
+ <string name="add_peer">Peer toevoegen</string>
+ <string name="addresses">Adressen</string>
+ <string name="applications">Applicaties</string>
+ <string name="allow_remote_control_intents_summary_off">Externe apps kunnen mogelijk geen tunnels in-/uitschakelen (aanbevolen)</string>
+ <string name="allow_remote_control_intents_summary_on">Externe apps kunnen tunnels in-/uitschakelen (geavanceerd)</string>
+ <string name="allow_remote_control_intents_title">Controle door externe besturingsapps toestaan</string>
+ <string name="allowed_ips">Toegestane IP-adressen</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_pka">: moet positief zijn en niet meer dan 65535</string>
+ <string name="bad_config_explanation_positive_number">: Moet positief zijn</string>
+ <string name="bad_config_explanation_udp_port">: Moet een geldig UDP poortnummer zijn</string>
+ <string name="bad_config_reason_invalid_key">Ongeldige sleutel</string>
+ <string name="bad_config_reason_invalid_number">Ongeldig nummer</string>
+ <string name="bad_config_reason_invalid_value">Ongeldige waarde</string>
+ <string name="bad_config_reason_missing_attribute">Attribuut ontbreekt</string>
+ <string name="bad_config_reason_missing_section">Ontbrekende sectie</string>
+ <string name="bad_config_reason_syntax_error">Syntaxfout</string>
+ <string name="bad_config_reason_unknown_attribute">Onbekend attribuut</string>
+ <string name="bad_config_reason_unknown_section">Onbekende sectie</string>
+ <string name="bad_config_reason_value_out_of_range">Waarde buiten bereik</string>
+ <string name="bad_extension_error">Bestand moet .conf of .zip zijn</string>
+ <string name="error_no_qr_found">QR-code niet gevonden in afbeelding</string>
+ <string name="error_qr_checksum">QR-code checksum verificatie mislukt</string>
+ <string name="cancel">Annuleren</string>
+ <string name="config_delete_error">Kan configuratiebestand %s niet verwijderen</string>
+ <string name="config_exists_error">Configuratie voor \"%s\" bestaat al</string>
+ <string name="config_file_exists_error">Configuratiebestand \"%s\" bestaat al</string>
+ <string name="config_not_found_error">Configuratiebestand \"%s\" niet gevonden</string>
+ <string name="config_rename_error">Kan configuratiebestand \"%s\" \" niet hernoemen</string>
+ <string name="config_save_error">Kan de configuratie voor \"%1$s\" niet opslaan: %2$s</string>
+ <string name="config_save_success">Configuratie succesvol opgeslagen voor \"%s\"</string>
+ <string name="create_activity_title">WireGuard tunnel aanmaken</string>
+ <string name="create_bin_dir_error">Kan geen lokale \'bin\' map aanmaken</string>
+ <string name="create_downloads_file_error">Kan bestand niet maken in downloadmap</string>
+ <string name="create_empty">Begin met lege configuratie</string>
+ <string name="create_from_file">Importeren uit bestand of archief</string>
+ <string name="create_from_qr_code">Scan van QR code</string>
+ <string name="create_output_dir_error">Kan de output map niet aanmaken</string>
+ <string name="create_temp_dir_error">Kan geen tijdelijke map aanmaken</string>
+ <string name="create_tunnel">Maak nieuwe tunnel</string>
+ <string name="copied_to_clipboard">%s gekopieerd naar klembord</string>
+ <string name="dark_theme_summary_off">Momenteel wordt licht (dag) thema gebruikt</string>
+ <string name="dark_theme_summary_on">Momenteel wordt donker (nacht) thema gebruikt</string>
+ <string name="dark_theme_title">Gebruik donker thema</string>
+ <string name="delete">Verwijder</string>
+ <string name="tv_delete">Selecteer tunnel om te verwijderen</string>
+ <string name="tv_select_a_storage_drive">Selecteer een opslaglocatie</string>
+ <string name="tv_no_file_picker">Installeer een bestandsbeheer applicatie</string>
+ <string name="tv_add_tunnel_get_started">Voeg een tunnel toe om te beginnen</string>
+ <string name="donate_title">♥️ Doneer aan het WireGuard Project</string>
+ <string name="donate_summary">Elke bijdrage helpt</string>
+ <string name="donate_google_play_disappointment">Bedankt voor het steunen van het WireGuard Project!\n\nHelaas, als gevolg van Google beleid, We mogen niet linken naar de webpagina van het project waar u een donatie kunt doen. Hopelijk kunt u deze zelf wel vinden!\n\nNogmaals bedankt voor uw bijdrage.</string>
+ <string name="disable_config_export_title">Config export uitschakelen</string>
+ <string name="disable_config_export_description">Het uitschakelen van configuratie export maakt privésleutels minder toegankelijk</string>
+ <string name="dns_servers">DNS-servers</string>
+ <string name="dns_search_domains">DNS-zoekdomeinen</string>
+ <string name="edit">Bewerken</string>
+ <string name="endpoint">Eindpunt</string>
+ <string name="error_down">Fout bij stoppen tunnel: %s</string>
+ <string name="error_fetching_apps">Fout bij ophalen van apps-lijst: %s</string>
+ <string name="error_root">Verkrijg root toegang en probeer het opnieuw</string>
+ <string name="error_prepare">Fout bij voorbereiden tunnel: %s</string>
+ <string name="error_up">Fout bij het starten van tunnel: %s</string>
+ <string name="exclude_private_ips">Privé-IP\'s uitsluiten</string>
+ <string name="generate_new_private_key">Nieuwe privésleutel genereren</string>
+ <string name="generic_error">Onbekend fout: \"%s\"</string>
+ <string name="hint_automatic">(auto)</string>
+ <string name="hint_generated">(gegenereerd)</string>
+ <string name="hint_optional">(optioneel)</string>
+ <string name="hint_optional_discouraged">(optioneel, niet aanbevolen)</string>
+ <string name="hint_random">(willekeurig)</string>
+ <string name="illegal_filename_error">Ongeldige bestandsnaam \"%s\" \"</string>
+ <string name="import_error">Kan tunnel niet importeren: %s</string>
+ <string name="import_from_qr_code">Importeer Tunnel uit QR Code</string>
+ <string name="import_success">Geïmporteerd \"%s\"</string>
+ <string name="interface_title">Interface</string>
+ <string name="key_contents_error">Slechte tekens in de veld</string>
+ <string name="key_length_error">Onjuiste sleutellengte</string>
+ <string name="key_length_explanation_base64">: WireGuard base64 sleutels moeten 44 tekens zijn (32 bytes)</string>
+ <string name="key_length_explanation_binary">: WireGuard sleutels moeten 32 bytes zijn</string>
+ <string name="key_length_explanation_hex">: WireGuard hex sleutels moeten 64 tekens zijn (32 bytes)</string>
+ <string name="latest_handshake">Recentste uitwisseling</string>
+ <string name="latest_handshake_ago">%s geleden</string>
+ <string name="listen_port">Luister op poort</string>
+ <string name="log_export_error">Kan logboek niet exporteren: %s</string>
+ <string name="log_export_subject">WireGuard Android logbestand</string>
+ <string name="log_export_success">Opgeslagen in \"%s\"</string>
+ <string name="log_export_title">Exporteer logboek naar bestand</string>
+ <string name="log_saver_activity_label">Logboek opslaan</string>
+ <string name="log_viewer_pref_summary">Logboeken kunnen helpen bij het debuggen</string>
+ <string name="log_viewer_pref_title">Bekijk applicatielogboek</string>
+ <string name="log_viewer_title">Log</string>
+ <string name="logcat_error">Kan logcat niet uitvoeren: </string>
+ <string name="module_enabler_disabled_summary">De experimentele kernel module kan de prestaties verbeteren</string>
+ <string name="module_enabler_disabled_title">Kernel module backend inschakelen</string>
+ <string name="module_enabler_enabled_summary">De langzamere userspace backend kan de stabiliteit verbeteren</string>
+ <string name="module_enabler_enabled_title">Uitschakelen kernel module backend</string>
+ <string name="module_installer_error">Er ging iets mis. Probeer het nog eens</string>
+ <string name="module_installer_initial">De experimentele kernel module kan de prestaties verbeteren</string>
+ <string name="module_installer_not_found">Er zijn geen modules beschikbaar voor uw apparaat</string>
+ <string name="module_installer_title">Download en installeer kernel module</string>
+ <string name="module_installer_working">Downloaden en installeren…</string>
+ <string name="module_version_error">Niet in staat om kernel module versie te bepalen</string>
+ <string name="mtu">MTU</string>
+ <string name="multiple_tunnels_summary_off">Het inschakelen van één tunnel zal anderen uitzetten</string>
+ <string name="multiple_tunnels_summary_on">Meerdere tunnels kunnen tegelijkertijd actief zijn</string>
+ <string name="multiple_tunnels_title">Meerdere gelijktijdige tunnels toestaan</string>
+ <string name="name">Naam</string>
+ <string name="no_config_error">Probeer een tunnel zonder configuratie te starten</string>
+ <string name="no_configs_error">Geen configuraties gevonden</string>
+ <string name="no_tunnels_error">Geen tunnels gedefinieerd</string>
+ <string name="parse_error_generic">string</string>
+ <string name="parse_error_inet_address">IP-adres</string>
+ <string name="parse_error_inet_endpoint">eindpunt</string>
+ <string name="parse_error_inet_network">IP netwerk</string>
+ <string name="parse_error_integer">nummer</string>
+ <string name="parse_error_reason">Kan %1$s%2$s niet parsen</string>
+ <string name="peer">Peer</string>
+ <string name="permission_description">beheer WireGuard tunnels, zet tunnels naar keuze aan en uit, en misleid mogelijk het Internetverkeer</string>
+ <string name="permission_label">WireGuard tunnels beheren</string>
+ <string name="persistent_keepalive">Voortdurende verbindingstest</string>
+ <string name="pre_shared_key">Gedeelde sleutel</string>
+ <string name="pre_shared_key_enabled">ingeschakeld</string>
+ <string name="private_key">Privésleutel</string>
+ <string name="public_key">Publieke sleutel</string>
+ <string name="qr_code_hint">Tip: genereer met `qrencode -t ansiutf8 &lt; tunnel.conf`.</string>
+ <string name="quick_settings_tile_add_title">Voeg tegel toe aan snelle instellingen</string>
+ <string name="quick_settings_tile_add_summary">De sneltoets schakelt de meest recente tunnel aan</string>
+ <string name="quick_settings_tile_add_failure">Kan geen sneltoets toevoegen: fout %d</string>
+ <string name="quick_settings_tile_action">tunnel in-/uitschakelen</string>
+ <string name="restore_on_boot_summary_off">Zal ingeschakelde tunnels niet aanzetten bij opstarten</string>
+ <string name="restore_on_boot_summary_on">Zal ingeschakelde tunnels aanzetten bij opstarten</string>
+ <string name="restore_on_boot_title">Tunnel starten bij herstart</string>
+ <string name="save">Opslaan</string>
+ <string name="select_all">Selecteer alles</string>
+ <string name="settings">Instellingen</string>
+ <string name="shell_exit_status_read_error">Shell kan de exitstatus niet lezen</string>
+ <string name="shell_marker_count_error">Shell verwachtte 4 markeringen, ontving er %d</string>
+ <string name="shell_start_error">Shell kon niet starten: %d</string>
+ <string name="success_application_will_restart">Succes. De toepassing zal nu herstarten…</string>
+ <string name="toggle_all">Alles wisselen</string>
+ <string name="toggle_error">Fout bij omschakelen Wireguard tunnel: %s</string>
+ <string name="tools_installer_already">wg and wg-quick zijn al geïnstalleerd</string>
+ <string name="tools_installer_failure">Kan de command-line tools niet installeren (geen root?)</string>
+ <string name="tools_installer_initial">Optionele tools voor scripts installeren</string>
+ <string name="tools_installer_title">Installeer command line tools</string>
+ <string name="tools_installer_working">Installeren van wg en wg-quick</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_tibibytes">%.2f TiB</string>
+ <string name="tun_create_error">Kan tun apparaat niet aanmaken</string>
+ <string name="tunnel_error_already_exists">Tunnel \"%s\" bestaat al</string>
+ <string name="tunnel_error_invalid_name">Ongeldige naam</string>
+ <string name="tunnel_list_placeholder">Voeg een tunnel toe met de knop hieronder</string>
+ <string name="tunnel_name">Tunnelnaam</string>
+ <string name="tunnel_rename_error">Kan tunnel niet hernoemen: %s</string>
+ <string name="tunnel_rename_success">Tunnel succesvol hernoemd naar \"%s\"</string>
+ <string name="type_name_go_userspace">Go userspace</string>
+ <string name="type_name_kernel_module">Kernel module</string>
+ <string name="unknown_error">Onbekende fout</string>
+ <string name="updater_avalable">Er is een nieuwe versie beschikbaar. Werk het programma s.v.p. bij.</string>
+ <string name="updater_action">Download &amp; installeer updates</string>
+ <string name="updater_rechecking">Update metadata downloaden…</string>
+ <string name="updater_download_progress">Ophalen nieuwe versie: %1$s / %2$s (%3$.2f%%)</string>
+ <string name="updater_download_progress_nototal">Updates downloaden: %s</string>
+ <string name="updater_installing">Update wordt geïnstalleerd…</string>
+ <string name="updater_failure">Bijwerken mislukt: %s. Zal het zo opnieuw proberen…</string>
+ <string name="updater_corrupt_title">Toepassing beschadigd</string>
+ <string name="updater_corrupt_message">De toepassing is beschadigd. Download de APK opnieuw van de onderstaande website. De-installeer daarna het programma, en herinstalleer het met de gedownloade APK.</string>
+ <string name="updater_corrupt_navigate">Open website</string>
+ <string name="version_title">WireGuard voor Android v%s</string>
+ <string name="zip_export_summary">Zip-bestand wordt opgeslagen in de downloadmap</string>
+ <string name="zip_export_title">Exporteer tunnels naar zip-bestand</string>
+ <string name="biometric_prompt_zip_exporter_title">Authenticeer om de tunnel configuratie te exporteren</string>
+ <string name="biometric_prompt_private_key_title">Authenticeer om de persoonlijke sleutel te bekijken</string>
+ <string name="biometric_auth_error">Authenticatiefout</string>
+ <string name="biometric_auth_error_reason">Authenticatiefout: %s</string>
</resources>
diff --git a/ui/src/main/res/values-no-rNO/strings.xml b/ui/src/main/res/values-no-rNO/strings.xml
index b0616f4e..ca51975e 100644
--- a/ui/src/main/res/values-no-rNO/strings.xml
+++ b/ui/src/main/res/values-no-rNO/strings.xml
@@ -21,7 +21,7 @@
<item quantity="other">Importerte %d tunneler</item>
</plurals>
<plurals name="set_excluded_applications">
- <item quantity="one">%d ekskludert program</item>
+ <item quantity="one">%d ekskludert app</item>
<item quantity="other">%d ekskluderte apper</item>
</plurals>
<plurals name="set_included_applications">
@@ -79,6 +79,8 @@
<string name="bad_config_reason_unknown_section">Ukjent seksjon</string>
<string name="bad_config_reason_value_out_of_range">Verdien er utenfor gyldig område</string>
<string name="bad_extension_error">Filen må være .conf eller .zip</string>
+ <string name="error_no_qr_found">Ingen QR-kode funnet i bildet</string>
+ <string name="error_qr_checksum">Feil ved sjekksumverifisering av QR-kode</string>
<string name="cancel">Avbryt</string>
<string name="config_delete_error">Kan ikke slette konfigurasjonsfilen %s</string>
<string name="config_exists_error">Konfigurasjon for «%s» finnes allerede</string>
@@ -105,14 +107,19 @@
<string name="tv_select_a_storage_drive">Velg en lagringsenhet</string>
<string name="tv_no_file_picker">Vennligst installer et filhåndteringsverktøy for å bla i filer</string>
<string name="tv_add_tunnel_get_started">Opprett en ny tunnel for å komme i gang</string>
+ <string name="donate_title">♥ Donér til WireGuard-prosjektet</string>
+ <string name="donate_summary">Hvert bidrag hjelper</string>
+ <string name="donate_google_play_disappointment">Takk for at du støtter WireGuard-prosjektet!\n\nPå grunn av Googles retningslinjer, kan vi dessverre ikke linke til den delen av prosjektets nettside der du kan donere. Forhåpentligvis klarer du å finne denne selv!\n\nVi takker igjen for ditt bidrag.</string>
<string name="disable_config_export_title">Deaktiver eksport av konfigurasjon</string>
<string name="disable_config_export_description">Deaktivering av konfigurasjonseksport gjør private nøkler mindre tilgjengelig</string>
<string name="dns_servers">DNS tjenere</string>
+ <string name="dns_search_domains">Søk gjennom domener</string>
<string name="edit">Rediger</string>
<string name="endpoint">Endepunkt</string>
<string name="error_down">Feil når tunnel skulle tas ned: %s</string>
<string name="error_fetching_apps">Feil ved henting av applikasjonsliste: %s</string>
<string name="error_root">Vennligst få root-tilgang og prøv igjen</string>
+ <string name="error_prepare">Feil ved klargjøring av tunnel: %s</string>
<string name="error_up">Feil når tunnel skulle tas opp: %s</string>
<string name="exclude_private_ips">Utelukk private IP-adresser</string>
<string name="generate_new_private_key">Lag ny privat nøkkel</string>
@@ -210,14 +217,16 @@
<string name="tunnel_create_success">Opprettet tunnelen «%s»</string>
<string name="tunnel_error_already_exists">Tunnel «%s» finnes allerede</string>
<string name="tunnel_error_invalid_name">Ugyldig navn</string>
- <string name="tunnel_list_placeholder">Opprett ny tunnel med den blå knappen</string>
+ <string name="tunnel_list_placeholder">Legg til en tunnel ved å bruke knappen under</string>
<string name="tunnel_name">Tunnelnavn</string>
<string name="tunnel_on_error">Kan ikke slå på tunnel (wgTurnOn returnerte %d)</string>
+ <string name="tunnel_dns_failure">Kan ikke slå opp DNS-vertsnavn: “%s\"</string>
<string name="tunnel_rename_error">Kan ikke endre navn på tunnel: %s</string>
<string name="tunnel_rename_success">Endret navn på tunnelen til «%s»</string>
<string name="type_name_go_userspace">Bruk userspace</string>
<string name="type_name_kernel_module">Kjernemodul</string>
<string name="unknown_error">Ukjent feil</string>
+ <string name="version_summary">%1$s backend %2$s</string>
<string name="version_summary_checking">Sjekker %s backend versjon</string>
<string name="version_summary_unknown">Ukjent %s versjon</string>
<string name="version_title">WireGuard for Android v%s</string>
diff --git a/ui/src/main/res/values-pa-rIN/strings.xml b/ui/src/main/res/values-pa-rIN/strings.xml
index 1d7ac096..10de7d9e 100644
--- a/ui/src/main/res/values-pa-rIN/strings.xml
+++ b/ui/src/main/res/values-pa-rIN/strings.xml
@@ -210,7 +210,6 @@
<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>
diff --git a/ui/src/main/res/values-pl-rPL/strings.xml b/ui/src/main/res/values-pl-rPL/strings.xml
index 119bdc74..3c9de47d 100644
--- a/ui/src/main/res/values-pl-rPL/strings.xml
+++ b/ui/src/main/res/values-pl-rPL/strings.xml
@@ -105,6 +105,8 @@
<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 posiadać rozszerzenie CONF lub ZIP</string>
+ <string name="error_no_qr_found">Kod QR nie został znaleziony w obrazie</string>
+ <string name="error_qr_checksum">Weryfikacja sumy kontrolnej kodu QR nie powiodła się</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>
@@ -134,6 +136,7 @@
<string name="disable_config_export_title">Wyłącz eksportowanie konfiguracji</string>
<string name="disable_config_export_description">Wyłączenie eksportowania konfiguracji sprawi, że klucze prywatne będą mniej dostępne</string>
<string name="dns_servers">Serwery DNS</string>
+ <string name="dns_search_domains">Sufiksy 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>
@@ -236,7 +239,6 @@
<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_dns_failure">Nie można odnaleźć nazwy hosta DNS: “%s”</string>
@@ -245,6 +247,7 @@
<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">%1$s backend %2$s</string>
<string name="version_summary_checking">Sprawdzanie wersji implementacji: %s</string>
<string name="version_summary_unknown">Nieznana wersja %s</string>
<string name="version_title">WireGuard dla systemu Android v%s</string>
diff --git a/ui/src/main/res/values-pt-rBR/strings.xml b/ui/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 00000000..eb8ab339
--- /dev/null
+++ b/ui/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,226 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="one">Não é possível excluir o túnel %d: %s</item>
+ <item quantity="other">Não foi possível excluir túneis %d: %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="one">Túnel %d excluído com sucesso</item>
+ <item quantity="other">Túneis %d excluídos com êxito</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="one">Túnel %d selecionado</item>
+ <item quantity="other">Túneis %d selecionados</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="one">Importados %1$d dos %2$d túneis</item>
+ <item quantity="other">Importados %1$d dos %2$d túneis</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="one">Importado %d túnel</item>
+ <item quantity="other">Importados %d túneis</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="one">Aplicação %d Excluída</item>
+ <item quantity="other">Aplicações %d Excluídas</item>
+ </plurals>
+ <string name="all_applications">Todos as aplicativos</string>
+ <string name="exclude_from_tunnel">Retirar</string>
+ <string name="include_in_tunnel">Incluir somente</string>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="one">segundo</item>
+ <item quantity="other">segundos</item>
+ </plurals>
+ <string name="use_all_applications">Usar todas aplicações</string>
+ <string name="add_peer">Adicionar Par</string>
+ <string name="addresses">Endereço</string>
+ <string name="applications">Aplicativo</string>
+ <string name="allow_remote_control_intents_summary_off">Aplicativos externos podem não alternar túneis (recomendado)</string>
+ <string name="allow_remote_control_intents_summary_on">Aplicativos externos podem alternar túneis (avançado)</string>
+ <string name="allow_remote_control_intents_title">Permitir controle remoto de apps</string>
+ <string name="allowed_ips">IPs Permitidos</string>
+ <string name="bad_config_context">%2$s da %1$s</string>
+ <string name="bad_config_context_top_level">%s</string>
+ <string name="bad_config_error">%1$s em %2$s</string>
+ <string name="bad_config_explanation_pka">: Deve ser positivo e não mais que 65535</string>
+ <string name="bad_config_explanation_positive_number">: Deve ser positivo</string>
+ <string name="bad_config_explanation_udp_port">: Deve ser um número de porta UDP válido</string>
+ <string name="bad_config_reason_invalid_key">Chave inválida</string>
+ <string name="bad_config_reason_invalid_number">Número inválido</string>
+ <string name="bad_config_reason_invalid_value">Valor inválido</string>
+ <string name="bad_config_reason_missing_attribute">Atributo ausente</string>
+ <string name="bad_config_reason_missing_section">Seção em falta</string>
+ <string name="bad_config_reason_syntax_error">Erro de sintaxe</string>
+ <string name="bad_config_reason_unknown_attribute">Atributo desconhecido</string>
+ <string name="bad_config_reason_unknown_section">Seção desconhecida</string>
+ <string name="bad_config_reason_value_out_of_range">Valor fora do intervalo</string>
+ <string name="bad_extension_error">O arquivo deve ser .conf ou .zip</string>
+ <string name="error_no_qr_found">Código QR não encontrado na imagem</string>
+ <string name="error_qr_checksum">Falha na verificação do código QR</string>
+ <string name="cancel">Cancelar</string>
+ <string name="config_delete_error">Não é possível excluir o arquivo de configuração %s</string>
+ <string name="config_exists_error">Configuração para \"%s\" já existe</string>
+ <string name="config_file_exists_error">Arquivo de configuração “%s” já existe</string>
+ <string name="config_not_found_error">Arquivo de configuração “%s” não encontrado</string>
+ <string name="config_rename_error">Não é possível renomear o arquivo de configuração “%s”</string>
+ <string name="config_save_error">Não pode salvar a configuração para \"%1$s\": %2$s</string>
+ <string name="config_save_success">Configuração salva com sucesso para “%s”</string>
+ <string name="create_activity_title">Criar túnel WireGuard</string>
+ <string name="create_bin_dir_error">Não é possível criar o diretório local do binário</string>
+ <string name="create_downloads_file_error">Não é possível criar arquivo no diretório de downloads</string>
+ <string name="create_empty">Criar do zero</string>
+ <string name="create_from_file">Criar a partir de arquivo</string>
+ <string name="create_from_qr_code">Ler código QR</string>
+ <string name="create_output_dir_error">Não é possível criar o diretório de saída</string>
+ <string name="create_temp_dir_error">Não é possível criar o diretório temporário local</string>
+ <string name="create_tunnel">Criar túnel</string>
+ <string name="copied_to_clipboard">%s copiado para a área de transferência</string>
+ <string name="dark_theme_summary_off">Atualmente usando tema claro (dia)</string>
+ <string name="dark_theme_summary_on">Atualmente usando tema escuro (noite)</string>
+ <string name="dark_theme_title">Usar tema escuro</string>
+ <string name="delete">Excluir</string>
+ <string name="tv_delete">Selecione o túnel para excluir</string>
+ <string name="tv_select_a_storage_drive">Selecione uma unidade de armazenamento</string>
+ <string name="tv_no_file_picker">Por favor, instale um utilitário de gerenciamento de arquivos para procurar arquivos</string>
+ <string name="tv_add_tunnel_get_started">Adicione um túnel para começar</string>
+ <string name="donate_title">♥️ Doar para o projeto WireGuard</string>
+ <string name="donate_summary">Todas as contribuições ajudam</string>
+ <string name="donate_google_play_disappointment">Obrigado por apoiar o Projeto WireGuard!\n\nInfelizmente, devido às políticas do Google, não temos permissão para vincular a parte da página do projeto onde você pode fazer uma doação. Esperamos que você consiga descobrir isso!\n\nObrigado novamente pela sua contribuição.</string>
+ <string name="disable_config_export_title">Desativar exportação de configuração</string>
+ <string name="disable_config_export_description">Desativar a exportação de configuração torna as chaves privadas menos acessíveis</string>
+ <string name="dns_servers">Servidores DNS</string>
+ <string name="dns_search_domains">Domínios de pesquisa de DNS</string>
+ <string name="edit">Editar</string>
+ <string name="endpoint">Endpoint</string>
+ <string name="error_down">Erro ao derrubar o túnel: %s</string>
+ <string name="error_fetching_apps">Erro ao obter lista de apps: %s</string>
+ <string name="error_root">Por favor, obtenha acesso root e tente novamente</string>
+ <string name="error_prepare">Erro ao preparar o túnel: %s</string>
+ <string name="error_up">Erro ao criar túnel: %s</string>
+ <string name="exclude_private_ips">Excluir IPs privados</string>
+ <string name="generate_new_private_key">Gerar uma nova chave privada</string>
+ <string name="generic_error">Erro desconhecido “%s”</string>
+ <string name="hint_automatic">(automático)</string>
+ <string name="hint_generated">(gerado)</string>
+ <string name="hint_optional">(opcional)</string>
+ <string name="hint_optional_discouraged">(opcional, não recomendado)</string>
+ <string name="hint_random">(aleatório)</string>
+ <string name="illegal_filename_error">Nome de arquivo inválido “%s”</string>
+ <string name="import_error">Não foi possível importar o túnel: %s</string>
+ <string name="import_from_qr_code">Importar Túnel por QR Code</string>
+ <string name="import_success">Importado “%s”</string>
+ <string name="interface_title">Interface</string>
+ <string name="key_contents_error">Caracteres inválidos na chave</string>
+ <string name="key_length_error">Chave com tamanho incorreto</string>
+ <string name="key_length_explanation_base64">: Chaves base64 do WireGuard devem ter 44 caracteres (32 bytes)</string>
+ <string name="key_length_explanation_binary">: Chaves do WireGuard devem ter 32 bytes</string>
+ <string name="key_length_explanation_hex">: Chaves hex do WireGuard devem ter 64 caracteres (32 bytes)</string>
+ <string name="latest_handshake">Último handshake</string>
+ <string name="latest_handshake_ago">%s atrás</string>
+ <string name="listen_port">Porta de escuta</string>
+ <string name="log_export_error">Não foi possível exportar o log: %s</string>
+ <string name="log_export_subject">Arquivo de log do WireGuard Android</string>
+ <string name="log_export_success">Salvo em “%s”</string>
+ <string name="log_export_title">Exportar arquivo de log</string>
+ <string name="log_saver_activity_label">Salvar log</string>
+ <string name="log_viewer_pref_summary">Registros podem ajudar na depuração</string>
+ <string name="log_viewer_pref_title">Exibir registros da aplicação</string>
+ <string name="log_viewer_title">Registro</string>
+ <string name="logcat_error">Não foi possível executar o logcat: </string>
+ <string name="module_enabler_disabled_summary">O módulo do Kernel experimental pode melhorar o desempenho</string>
+ <string name="module_enabler_disabled_title">Habilitar módulo backend do kernel</string>
+ <string name="module_enabler_enabled_summary">O backend do userspace mais lento pode aumentar a estabilidade</string>
+ <string name="module_enabler_enabled_title">Desativar backend do módulo do kernel</string>
+ <string name="module_installer_error">Ocorreu um erro. Tente novamente</string>
+ <string name="module_installer_initial">O módulo experimental do kernel pode melhorar o desempenho</string>
+ <string name="module_installer_not_found">Não há módulos disponíveis para o seu dispositivo</string>
+ <string name="module_installer_title">Baixar e instalar módulo do kernel</string>
+ <string name="module_installer_working">Baixando e instalando…</string>
+ <string name="module_version_error">Não foi possível determinar a versão do módulo do kernel</string>
+ <string name="mtu">MTU</string>
+ <string name="multiple_tunnels_summary_off">Ligar um túnel irá desligar outros</string>
+ <string name="multiple_tunnels_summary_on">Múltiplos túneis podem ser ativados simultaneamente</string>
+ <string name="multiple_tunnels_title">Permitir múltiplos túneis simultâneos</string>
+ <string name="name">Nome</string>
+ <string name="no_config_error">Não é possível criar um túnel sem configuração</string>
+ <string name="no_configs_error">Nenhuma configuração foi encontrada</string>
+ <string name="no_tunnels_error">Não existem túneis</string>
+ <string name="parse_error_generic">string</string>
+ <string name="parse_error_inet_address">Endereço IP</string>
+ <string name="parse_error_inet_endpoint">endpoint</string>
+ <string name="parse_error_inet_network">Rede IP</string>
+ <string name="parse_error_reason">Não é possível analisar %1$s “%2$s”</string>
+ <string name="permission_description">permite controlar túneis do WireGuard, ativando e desativando túneis a vontade, potencialmente desviando o tráfego na Internet</string>
+ <string name="permission_label">controlar túneis do WireGuard</string>
+ <string name="persistent_keepalive">Keepalive persistente</string>
+ <string name="pre_shared_key">Chave pré-partilhada</string>
+ <string name="pre_shared_key_enabled">ativo</string>
+ <string name="private_key">Chave Privada</string>
+ <string name="public_key">Chave pública</string>
+ <string name="qr_code_hint">Dica: gerar com `qrencode -t ansiutf8 &lt; tunnel.conf`.</string>
+ <string name="quick_settings_tile_add_title">Adicionar botão ao painel de configurações rápidas</string>
+ <string name="quick_settings_tile_add_summary">A tecla de atalho alterna o túnel mais recente</string>
+ <string name="quick_settings_tile_add_failure">Não foi possível adicionar o atalho de botão: erro %d</string>
+ <string name="restore_on_boot_title">Restaurar na inicialização</string>
+ <string name="save">Salvar</string>
+ <string name="select_all">Selecionar tudo</string>
+ <string name="settings">Definições</string>
+ <string name="shell_exit_status_read_error">O Shell não pode ler o status de saída</string>
+ <string name="shell_marker_count_error">Shell esperava 4 marcadores, mas recebeu apenas %d</string>
+ <string name="shell_start_error">Shell falhou ao iniciar: %d</string>
+ <string name="success_application_will_restart">Sucesso. O aplicativo irá reiniciar agora…</string>
+ <string name="toggle_error">Erro ao ativar o túnel do WireGuard: %s</string>
+ <string name="tools_installer_already">wg e wg-quick já estão instalados</string>
+ <string name="tools_installer_failure">Não foi possível instalar ferramentas de linha de comando (sem root?)</string>
+ <string name="tools_installer_initial">Instalar ferramentas opcionais para scripting</string>
+ <string name="tools_installer_initial_magisk">Instalar ferramentas opcionais para scripting como o módulo Magisk</string>
+ <string name="tools_installer_initial_system">Instalar ferramentas opcionais para escrever na partição do sistema</string>
+ <string name="tools_installer_success_magisk">wg e wg-quick instalado como um módulo Magisk (é necessário reiniciar)</string>
+ <string name="tools_installer_success_system">wg e wg-quick instalados na partição do sistema</string>
+ <string name="tools_installer_title">Instalar ferramentas de linha de comando</string>
+ <string name="tools_installer_working">Instalando wg e wg-quick</string>
+ <string name="tools_unavailable_error">Ferramentas necessárias indisponíveis</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">Não foi possível criar o túnel: %s</string>
+ <string name="tunnel_config_error">Não foi possível configurar o túnel (wg-quick retornou %d)</string>
+ <string name="tunnel_create_error">Não foi possível criar o túnel: %s</string>
+ <string name="tunnel_create_success">Túnel criado com sucesso “%s”</string>
+ <string name="tunnel_error_already_exists">Túnel “%s” já existe</string>
+ <string name="tunnel_error_invalid_name">Nome inválido</string>
+ <string name="tunnel_list_placeholder">Adicionar um túnel usando o botão abaixo</string>
+ <string name="tunnel_name">Nome do túnel</string>
+ <string name="tunnel_on_error">Não foi possível ativar o túnel (wgTurnOn retornou %d)</string>
+ <string name="tunnel_dns_failure">Não foi possível resolver host DNS: \"%s\"</string>
+ <string name="tunnel_rename_error">Não foi possível renomear o túnel: %s</string>
+ <string name="tunnel_rename_success">Renomeado com sucesso o túnel para “%s”</string>
+ <string name="type_name_kernel_module">Modo de Kernel</string>
+ <string name="unknown_error">Erro desconhecido</string>
+ <string name="updater_avalable">Uma atualização do aplicativo está disponível. Pôr favor atualize agora.</string>
+ <string name="updater_action">Baixar &amp; Atualizar</string>
+ <string name="updater_rechecking">Obtendo metadados de atualização…</string>
+ <string name="updater_download_progress">Baixando a atualização: %1$s / %2$s (%3$.2f%%)</string>
+ <string name="updater_download_progress_nototal">Baixando atualização: %s</string>
+ <string name="updater_installing">Instalando atualização…</string>
+ <string name="updater_failure">Falha na atualização: %s. Tentaremos novamente em breve…</string>
+ <string name="updater_corrupt_title">Aplicativo corrompido</string>
+ <string name="updater_corrupt_message">Este aplicativo está corrompido. Por favor, baixe novamente o APK do site vinculado abaixo. Depois disso, desinstale o aplicativo e reinstale-o a partir do APK baixado.</string>
+ <string name="updater_corrupt_navigate">Abrir site</string>
+ <string name="version_summary_checking">Verificando a versão do backend %s</string>
+ <string name="version_summary_unknown">Versão %s desconhecida</string>
+ <string name="version_title">WireGuard para Android v%s</string>
+ <string name="vpn_not_authorized_error">Serviço de VPN não autorizado pelo usuário</string>
+ <string name="vpn_start_error">Não foi possível iniciar o serviço de VPN do Android</string>
+ <string name="zip_export_error">Não foi possível exportar túneis: %s</string>
+ <string name="zip_export_success">Salvo em “%s”</string>
+ <string name="zip_export_summary">O arquivo Zip será salvo na pasta de downloads</string>
+ <string name="zip_export_title">Exportar túneis para arquivo zip</string>
+ <string name="biometric_prompt_zip_exporter_title">Autenticar para exportar túneis</string>
+ <string name="biometric_prompt_private_key_title">Autenticar para ver a chave privada</string>
+ <string name="biometric_auth_error">Falha de autenticação</string>
+ <string name="biometric_auth_error_reason">Falha de autenticação: %s</string>
+</resources>
diff --git a/ui/src/main/res/values-pt-rPT/strings.xml b/ui/src/main/res/values-pt-rPT/strings.xml
index fc1f19cb..48afc41b 100644
--- a/ui/src/main/res/values-pt-rPT/strings.xml
+++ b/ui/src/main/res/values-pt-rPT/strings.xml
@@ -210,7 +210,6 @@
<string name="tunnel_create_success">Túnel criado com sucesso “%s”</string>
<string name="tunnel_error_already_exists">O túnel “%s” já existe</string>
<string name="tunnel_error_invalid_name">Nome inválido</string>
- <string name="tunnel_list_placeholder">Adicionar um túnel com o botão azul</string>
<string name="tunnel_name">Nome do túnel</string>
<string name="tunnel_on_error">Não foi possível ligar o túnel (wgTurnOn retornou %d)</string>
<string name="tunnel_rename_error">Não foi possível renomear o túnel: %s</string>
diff --git a/ui/src/main/res/values-ro-rRO/strings.xml b/ui/src/main/res/values-ro-rRO/strings.xml
index 5caa3cae..16ee62f0 100644
--- a/ui/src/main/res/values-ro-rRO/strings.xml
+++ b/ui/src/main/res/values-ro-rRO/strings.xml
@@ -92,6 +92,8 @@
<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="error_no_qr_found">Codul QR nu a fost găsit în imagine</string>
+ <string name="error_qr_checksum">Nu a putut fi efectuată verificarea sumei de control pentru codul QR</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>
@@ -118,14 +120,19 @@
<string name="tv_select_a_storage_drive">Selectează o unitate de stocare</string>
<string name="tv_no_file_picker">Instalează un serviciu de administrare a fișierelor pentru a căuta fișiere</string>
<string name="tv_add_tunnel_get_started">Adaugă un tunel pentru a începe</string>
+ <string name="donate_title">♥ Donează pentru proiectul WireGuard</string>
+ <string name="donate_summary">Fiecare contribuţie ajută</string>
+ <string name="donate_google_play_disappointment">Vă mulțumim pentru sprijinul acordat Proiectului WireGuard!\n\nDin păcate, din cauza politicilor Google, nu avem voie să punem un link către pagina web a proiectului unde poți face o donație. Sperăm că vă puteți descurca!\n\nMulțumim din nou pentru contribuție.</string>
<string name="disable_config_export_title">Dezactivează exportarea configurației</string>
<string name="disable_config_export_description">Dezactivarea exportării configurației face mai puțin accesibile cheile private</string>
<string name="dns_servers">Servere DNS</string>
+ <string name="dns_search_domains">Domenii de căutare</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_prepare">Eroare la pregătirea tunelului: %s</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>
@@ -145,6 +152,8 @@
<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="latest_handshake">Cea mai recentă negociere</string>
+ <string name="latest_handshake_ago">%s în urmă</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>
@@ -188,6 +197,8 @@
<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="quick_settings_tile_add_title">Adaugă secțiune la panoul de setări rapide</string>
+ <string name="quick_settings_tile_add_summary">Comanda rapidă comută cel mai recent tunel</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>
@@ -223,7 +234,6 @@
<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_dns_failure">Nu se poate rezolva numele gazdei DNS: „%s”</string>
diff --git a/ui/src/main/res/values-ru/strings.xml b/ui/src/main/res/values-ru/strings.xml
index b15436de..70f38ee1 100644
--- a/ui/src/main/res/values-ru/strings.xml
+++ b/ui/src/main/res/values-ru/strings.xml
@@ -31,10 +31,10 @@
<item quantity="other">Импортировано %d туннелей</item>
</plurals>
<plurals name="set_excluded_applications">
- <item quantity="one">%d Исключенное приложение</item>
- <item quantity="few">%d Исключенных приложения</item>
- <item quantity="many">%d Исключенных приложений</item>
- <item quantity="other">%d Исключенных приложений</item>
+ <item quantity="one">%d исключенное приложение</item>
+ <item quantity="few">%d исключенных приложения</item>
+ <item quantity="many">%d исключенных приложений</item>
+ <item quantity="other">%d исключенных приложений</item>
</plurals>
<plurals name="set_included_applications">
<item quantity="one">%d включенное приложение</item>
@@ -49,7 +49,7 @@
<item quantity="other">%d исключено</item>
</plurals>
<plurals name="n_included_applications">
- <item quantity="one">%d влючено</item>
+ <item quantity="one">%d включено</item>
<item quantity="few">%d включено</item>
<item quantity="many">%d включено</item>
<item quantity="other">%d включено</item>
@@ -88,13 +88,13 @@
<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="allowed_ips">Разрешенные IP</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_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>
@@ -105,6 +105,8 @@
<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="error_no_qr_found">QR-код не найден на изображении</string>
+ <string name="error_qr_checksum">Ошибка проверки контрольной суммы QR-кода</string>
<string name="cancel">Отмена</string>
<string name="config_delete_error">Не удалось удалить файл конфигурации %s</string>
<string name="config_exists_error">Конфигурация для “%s” уже существует</string>
@@ -129,20 +131,25 @@
<string name="delete">Удалить</string>
<string name="tv_delete">Выберите туннель для удаления</string>
<string name="tv_select_a_storage_drive">Выберите накопитель</string>
- <string name="tv_no_file_picker">Пожалуйста установите утилиту управления файлами для их просмотра</string>
+ <string name="tv_no_file_picker">Пожалуйста, установите утилиту управления файлами для их просмотра</string>
<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>
+ <string name="dns_search_domains">Домены поиска</string>
<string name="edit">Изменить</string>
<string name="endpoint">Конечная точка</string>
- <string name="error_down">Ошибка при выходе из туннеля: %s</string>
+ <string name="error_down">Ошибка при отключении туннеля: %s</string>
<string name="error_fetching_apps">Ошибка при получении списка приложений: %s</string>
<string name="error_root">Пожалуйста, получите root-доступ и попробуйте снова</string>
+ <string name="error_prepare">Ошибка при подготовке туннеля: %s</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="generic_error">Неизвестная ошибка “%s”</string>
<string name="hint_automatic">(авто)</string>
<string name="hint_generated">(сгенерирован)</string>
<string name="hint_optional">(опционально)</string>
@@ -157,7 +164,9 @@
<string name="key_length_error">Неправильная длина ключа</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">: HEX ключи WireGuard должны содержать 64 символа (32 байта)</string>
+ <string name="key_length_explanation_hex">: HEX-ключи WireGuard должны содержать 64 символа (32 байта)</string>
+ <string name="latest_handshake">Последнее рукопожатие</string>
+ <string name="latest_handshake_ago">%s назад</string>
<string name="listen_port">Порт</string>
<string name="log_export_error">Не удалось экспортировать журнал: %s</string>
<string name="log_export_subject">Файл журнала WireGuard Android</string>
@@ -191,16 +200,20 @@
<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="parse_error_reason">Невозможно разобрать %1$s “%2$s”</string>
<string name="peer">Пир</string>
- <string name="permission_description">управлять туннелями WireGuard, включая и отключая туннели по желанию, потенциально перенаправляя интернет-трафик</string>
+ <string name="permission_description">контроль над туннелями WireGuard, включение и отключение туннелей по своему усмотрению, возможность неправильного управления сетевым трафиком</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="qr_code_hint">Совет: генерировать с `qrencode -t ansiutf8 &lt; tunnel.conf`.</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="restore_on_boot_summary_off">Не поднимать ранее выбранные туннели при загрузке</string>
<string name="restore_on_boot_summary_on">Поднимать ранее выбранные туннели при загрузке</string>
<string name="restore_on_boot_title">Восстановить при загрузке</string>
@@ -235,15 +248,27 @@
<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_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_dns_failure">Не удалось определить DNS имя: “%s”</string>
<string name="tunnel_rename_error">Не удалось переименовать туннель: %s</string>
<string name="tunnel_rename_success">Туннель успешно переименован в “%s”</string>
- <string name="type_name_go_userspace">Пользовательское пространство Go</string>
+ <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>
<string name="version_title">WireGuard для Android v%s</string>
@@ -254,7 +279,7 @@
<string name="zip_export_summary">Zip-файл будет сохранен в папке загрузок</string>
<string name="zip_export_title">Экспорт туннелей в zip-файл</string>
<string name="biometric_prompt_zip_exporter_title">Аутентификация для экспорта туннелей</string>
- <string name="biometric_prompt_private_key_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>
diff --git a/ui/src/main/res/values-si-rLK/strings.xml b/ui/src/main/res/values-si-rLK/strings.xml
index b5bf5927..f7941a1e 100644
--- a/ui/src/main/res/values-si-rLK/strings.xml
+++ b/ui/src/main/res/values-si-rLK/strings.xml
@@ -9,14 +9,14 @@
<item quantity="other">ඇතුළත් කළ යෙදුම් %d යි</item>
</plurals>
<plurals name="n_excluded_applications">
- <item quantity="one">%d බැහැරයි</item>
+ <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="all_applications">සියළුම යෙදුම්</string>
<string name="exclude_from_tunnel">බැහැර</string>
<string name="include_in_tunnel">ඇතුළත් දෑ පමණි</string>
<plurals name="include_n_applications">
@@ -40,56 +40,165 @@
<string name="applications">යෙදුම්</string>
<string name="allow_remote_control_intents_title">දුරස්ථ පාලක යෙදුම්වලට ඉඩදෙන්න</string>
<string name="allowed_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">%2$sන් %1$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="error_no_qr_found">රූපයේ QR කේතය හමු නොවේ</string>
+ <string name="error_qr_checksum">QR කේත චෙක්සම් සත්‍යාපනය අසාර්ථක විය</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_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 Tunnel සාදන්න</string>
+ <string name="create_bin_dir_error">දේශීය ද්විමය නාමාවලිය සෑදිය නොහැක</string>
<string name="create_downloads_file_error">බාගැනීම් නාමාවලියෙහි ගොනුව සෑදීමට නොහැකිය</string>
- <string name="create_from_file">ගොනුවකින් හෝ සංරක්‍ෂිතයකින් ආයාතකරන්න</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="copied_to_clipboard">%s පසුරුපුවරුවට පිටපත්විය</string>
<string name="dark_theme_summary_off">දැනට දීප්ත (දිවා) තේමාව භාවිතා කරයි</string>
<string name="dark_theme_summary_on">දැනට අඳුරු (රාත්‍රී) තේමාව භාවිතා කරයි</string>
- <string name="dark_theme_title">අඳුරු තේමාව භාවිතා කරන්න</string>
+ <string name="dark_theme_title">අඳුරු තේමාව භාවිතය</string>
+ <string name="delete">මකන්න</string>
+ <string name="tv_select_a_storage_drive">ගබඩා ධාවකයක් තෝරන්න</string>
+ <string name="tv_no_file_picker">කරුණාකර ගොනු පිරික්සීමට ගොනු කළමනාකරණ උපයෝගිතා ස්ථාපනය කරන්න</string>
+ <string name="disable_config_export_description">වින්‍යාස අපනයනය අක්‍රිය කිරීම පුද්ගලික යතුරු වලට ප්‍රවේශ වීම අඩු කරයි</string>
<string name="dns_servers">ව.නා.ප. සේවාදායක</string>
+ <string name="dns_search_domains">වසම් සොයන්න</string>
<string name="edit">සංස්කරණය</string>
- <string name="exclude_private_ips">පුද්ගලික යතුරු බැහැර කරන්න</string>
- <string name="generate_new_private_key">නව පුද්ගලික යතුර ජනනයකරන්න</string>
+ <string name="error_fetching_apps">යෙදුම් ලැයිස්තුව ලබා ගැනීමේ දෝෂයකි: %s</string>
+ <string name="error_root">කරුණාකර මූල ප්‍රවේශය ලබාගෙන නැවත උත්සාහ කරන්න</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_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_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 යතුරු අක්ෂර 44 (බයිට් 32) විය යුතුය.</string>
<string name="key_length_explanation_binary">: වයර්ගාඩ් යතුරු බයිට 32 ක් විය යුතුය</string>
+ <string name="key_length_explanation_hex">: WireGuard hex යතුරු අක්ෂර 64 (බයිට් 32) විය යුතුය.</string>
<string name="listen_port">සවන්දීමේ කෙවෙනිය</string>
+ <string name="log_export_error">ලොගය අපනයනය කළ නොහැක: %s</string>
+ <string name="log_export_subject">WireGuard Android ලොග් ගොනුව</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_enabler_disabled_summary">පර්යේෂණාත්මක කර්නල් මොඩියුලය කාර්ය සාධනය වැඩි දියුණු කළ හැක</string>
+ <string name="module_enabler_disabled_title">කර්නල් මොඩියුල පසුපෙළ සබල කරන්න</string>
+ <string name="module_enabler_enabled_summary">මන්දගාමී පරිශීලක අවකාශයේ පසුපෙළ ස්ථාවරත්වය වැඩි දියුණු කළ හැකිය</string>
+ <string name="module_enabler_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="pre_shared_key_enabled">සබල කර ඇත</string>
+ <string name="parse_error_reason">%1$s \"%2$s\" විග්‍රහ කළ නොහැක</string>
+ <string name="peer">සම වයසේ මිතුරන්</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">Shell හට පිටවීමේ තත්ත්වය කියවිය නොහැක</string>
+ <string name="shell_marker_count_error">Shell අපේක්ෂිත ලකුණු 4, %dලැබිණි</string>
+ <string name="shell_start_error">Shell ආරම්භ කිරීමට අසමත් විය: %d</string>
<string name="success_application_will_restart">සාර්ථකයි. යෙදුම දැන් නැවත ආරම්භ කෙරේ…</string>
+ <string name="toggle_all">සියල්ල ටොගල් කරන්න</string>
+ <string name="toggle_error">WireGuard උමං ටොගල් කිරීමේ දෝෂය: %s</string>
+ <string name="tools_installer_already">wg සහ wg-quick දැනටමත් ස්ථාපනය කර ඇත</string>
+ <string name="tools_installer_failure">විධාන රේඛා මෙවලම් ස්ථාපනය කළ නොහැක (root නැත?)</string>
+ <string name="tools_installer_initial">ස්ක්‍රිප්ටින් සඳහා විකල්ප මෙවලම් ස්ථාපනය කරන්න</string>
+ <string name="tools_installer_initial_magisk">මැජික් මොඩියුලය ලෙස ස්ක්‍රිප්ට් කිරීම සඳහා විකල්ප මෙවලම් ස්ථාපනය කරන්න</string>
+ <string name="tools_installer_initial_system">පද්ධති කොටසට ස්ක්‍රිප්ට් කිරීම සඳහා විකල්ප මෙවලම් ස්ථාපනය කරන්න</string>
+ <string name="tools_installer_success_magisk">wg සහ wg-quick Magisk මොඩියුලයක් ලෙස ස්ථාපනය කර ඇත (නැවත පණගැන්වීම අවශ්‍යයි)</string>
+ <string name="tools_installer_success_system">wg සහ wg-quick පද්ධති කොටස තුළ ස්ථාපනය කර ඇත</string>
+ <string name="tools_installer_title">විධාන රේඛා මෙවලම් ස්ථාපනය කරන්න</string>
+ <string name="tools_installer_working">wg සහ wg-ඉක්මන් ස්ථාපනය කිරීම</string>
+ <string name="tools_unavailable_error">අවශ්‍ය මෙවලම් නොමැත</string>
+ <string name="transfer">මාරු</string>
<string name="transfer_bytes">බ. %d</string>
<string name="transfer_gibibytes">ගි.බ. %.2f</string>
<string name="transfer_kibibytes">කි.බ. %.2f</string>
<string name="transfer_mibibytes">මෙ.බ. %.2f</string>
+ <string name="transfer_rx_tx">rx: %1$s, tx: %2$s</string>
<string name="transfer_tibibytes">ටෙ.බ. %.2f</string>
+ <string name="tun_create_error">ටුන් උපාංගය සෑදීමට නොහැක</string>
+ <string name="tunnel_config_error">උමග වින්‍යාස කිරීමට නොහැක (wg-quick return %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_name">උමං නම</string>
+ <string name="tunnel_on_error">උමග ක්‍රියාත්මක කළ නොහැක (wgTurnOn %dආපසු ලබා දෙන ලදී)</string>
+ <string name="tunnel_dns_failure">DNS සත්කාරක නාමය විසඳිය නොහැක: \"%s\"</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">ඇන්ඩ්‍රොයිඩ් සඳහා වයර්ගාඩ් අනු.%s</string>
+ <string name="vpn_not_authorized_error">VPN සේවාව පරිශීලකයා විසින් අනුමත කර නොමැත</string>
+ <string name="vpn_start_error">Android VPN සේවාව ආරම්භ කළ නොහැක</string>
+ <string name="zip_export_error">උමං අපනයනය කළ නොහැක: %s</string>
<string name="zip_export_success">“%s” ට සුරැකිණි</string>
+ <string name="zip_export_summary">Zip ගොනුව බාගැනීම් ෆෝල්ඩරයට සුරකිනු ඇත</string>
+ <string name="zip_export_title">zip ගොනුවට උමං අපනයනය කරන්න</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>
diff --git a/ui/src/main/res/values-sk-rSK/strings.xml b/ui/src/main/res/values-sk-rSK/strings.xml
index 30ba9f98..40df1d7e 100644
--- a/ui/src/main/res/values-sk-rSK/strings.xml
+++ b/ui/src/main/res/values-sk-rSK/strings.xml
@@ -7,9 +7,12 @@
<string name="add_peer">Pridať peera</string>
<string name="addresses">Adresy</string>
<string name="applications">Aplikácie</string>
+ <string name="allow_remote_control_intents_summary_off">Externé aplikácie nemôžu spustiť tunely (odporúčané)</string>
+ <string name="allow_remote_control_intents_summary_on">Externé aplikácie môžu spustiť tunely (pokročilé)</string>
<string name="allow_remote_control_intents_title">Povoliť aplikáciám vzdialenú správu</string>
<string name="allowed_ips">Povolené IP adresy</string>
<string name="bad_config_context_top_level">%s</string>
+ <string name="bad_config_error">%1$s v %2$s</string>
<string name="bad_config_explanation_pka">: Musí byť kladné a nie väčšie ako 65535</string>
<string name="bad_config_explanation_positive_number">: Musí byť kladné</string>
<string name="bad_config_explanation_udp_port">: Musí byť platné číslo UDP portu</string>
@@ -17,8 +20,8 @@
<string name="bad_config_reason_invalid_number">Neplatné číslo</string>
<string name="bad_config_reason_invalid_value">Neplatná hodnota</string>
<string name="bad_config_reason_missing_attribute">Chýbajúci atribút</string>
- <string name="bad_config_reason_missing_section">Chýbajúc sekcia</string>
- <string name="bad_config_reason_syntax_error">Chyba syntaxu</string>
+ <string name="bad_config_reason_missing_section">Chýbajúca sekcia</string>
+ <string name="bad_config_reason_syntax_error">Chyba syntaxe</string>
<string name="bad_config_reason_unknown_attribute">Neznámy atribút</string>
<string name="bad_config_reason_unknown_section">Neznáma sekcia</string>
<string name="bad_config_reason_value_out_of_range">Hodnota mimo povoleného rozsahu</string>
@@ -28,15 +31,17 @@
<string name="config_exists_error">Konfigurácia pre “%s” už existuje</string>
<string name="config_file_exists_error">Konfiguračný súbor pre “%s” už existuje</string>
<string name="config_not_found_error">Konfiguračný súbor “%s” sa nenašiel</string>
- <string name="config_rename_error">Nemôžete premenovať konfiguračný súbor “%s”</string>
- <string name="config_save_error">Nemôžete uložiť konfiguráciu pre “%1$s”: %2$s</string>
+ <string name="config_rename_error">Nepodarilo sa premenovať konfiguračný súbor “%s”</string>
+ <string name="config_save_error">Nepodarilo sa uložiť konfiguráciu pre “%1$s”: %2$s</string>
<string name="config_save_success">Úspešne sa podarilo uložiť konfiguráciu pre “%s”</string>
<string name="create_activity_title">Vytvoriť WireGuard tunel</string>
- <string name="create_bin_dir_error">Nemôžete vytvoriť lokálny binárny súbor</string>
- <string name="create_downloads_file_error">Nemôžete vytvoriť súbor v priečinku downloads</string>
+ <string name="create_bin_dir_error">Nepodarilo sa vytvoriť lokálny priečinok pre binárne súbory</string>
+ <string name="create_downloads_file_error">Nepodarilo sa vytvoriť súbor v priečinku stiahnuté</string>
<string name="create_empty">Vytvoriť od počiatku</string>
<string name="create_from_file">Importovať zo súboru alebo archívu</string>
<string name="create_from_qr_code">Skenovať z QR kódu</string>
+ <string name="create_output_dir_error">Nepodarilo sa vytvoriť výstupný adresár</string>
+ <string name="create_temp_dir_error">Nepodarilo sa vytvoriť lokálny dočasný priečinok</string>
<string name="create_tunnel">Vytvoriť tunel</string>
<string name="copied_to_clipboard">%s skopírované do schránky</string>
<string name="dark_theme_summary_off">Momentálne používate svetlý (denný) vzhľad</string>
@@ -45,57 +50,70 @@
<string name="delete">Odstrániť</string>
<string name="tv_delete">Vyberte tunel na odstránenie</string>
<string name="tv_select_a_storage_drive">Vyberte úložnú jednotku</string>
- <string name="tv_no_file_picker">Prosím nainštalujte manažéra súbor aby ste mohli prehliadať súbory</string>
+ <string name="tv_no_file_picker">Prosím nainštalujte manažéra súborov aby ste mohli prehliadať súbory</string>
<string name="tv_add_tunnel_get_started">Pridajte tunel aby ste mohli začať</string>
+ <string name="disable_config_export_title">Zakázať export konfigurácie</string>
+ <string name="disable_config_export_description">Zakázanie exportu konfigurácie spôsobí, že prístup k súkromným kľúčom sa stáva zložitým</string>
<string name="dns_servers">Servery DNS</string>
+ <string name="dns_search_domains">Prehľadávať domény</string>
<string name="edit">Upraviť</string>
<string name="endpoint">Koncový bod</string>
<string name="error_down">Chyba pri vypínaní tunela: %s</string>
<string name="error_fetching_apps">Chyba pri načítaní zoznamu aplikácií: %s</string>
<string name="error_root">Získajte prístup root a skúste znova</string>
- <string name="error_up">Chyba pri vyvolávaní tunela: %s</string>
- <string name="exclude_private_ips">Vynechať privátne IP</string>
- <string name="generate_new_private_key">Generovať nový privátny kľúč</string>
+ <string name="error_up">Chyba pri zapínaní tunela: %s</string>
+ <string name="exclude_private_ips">Vynechať súkromné IP</string>
+ <string name="generate_new_private_key">Generovať nový súkromný kľúč</string>
<string name="generic_error">Neznáma “%s” chyba</string>
+ <string name="hint_automatic">(automatické)</string>
<string name="hint_generated">(generované)</string>
<string name="hint_optional">(voliteľné)</string>
<string name="hint_optional_discouraged">(voliteľné, neodporúča sa)</string>
<string name="hint_random">(náhodné)</string>
- <string name="illegal_filename_error">Ilegálne meno súboru “%s”</string>
+ <string name="illegal_filename_error">Nepovolené meno súboru “%s”</string>
<string name="import_error">Nepodarilo sa importovať tunel: %s</string>
<string name="import_from_qr_code">Importovať tunel z QR kódu</string>
<string name="import_success">Podarilo sa importovať “%s”</string>
<string name="interface_title">Rozhranie</string>
- <string name="key_contents_error">Nepovolené charaktery v kľúči</string>
+ <string name="key_contents_error">Nepovolené znaky v kľúči</string>
<string name="key_length_error">Nesprávna dĺžka kľúču</string>
- <string name="key_length_explanation_base64">: WireGuard base64 kľúče musia mať 44 charakterov (32 bytes)</string>
+ <string name="key_length_explanation_base64">: WireGuard base64 kľúče musia mať 44 znakov (32 bytes)</string>
<string name="key_length_explanation_binary">: WireGuard kľúče musia byť 32 bytové</string>
- <string name="key_length_explanation_hex">: WireGuard hex kľúče musia mať 64 charakterov (32 bytes)</string>
+ <string name="key_length_explanation_hex">: WireGuard hex kľúče musia mať 64 znakov (32 bytes)</string>
+ <string name="listen_port">Otvorený port</string>
<string name="log_export_error">Nepodarilo sa exportovať log: %s</string>
+ <string name="log_export_subject">WireGuard Android Denník udalostí</string>
<string name="log_export_success">Uložené do “%s”</string>
<string name="log_export_title">Exportovať denník udalostí</string>
<string name="log_saver_activity_label">Uložiť denník udalostí</string>
- <string name="log_viewer_pref_summary">Denník udalosti môžu byt nápomocné pri ladení aplikácie</string>
+ <string name="log_viewer_pref_summary">Denníky udalostí môžu byt nápomocné pri ladení aplikácie</string>
<string name="log_viewer_pref_title">Zobraziť denník udalostí aplikácie</string>
- <string name="module_enabler_enabled_summary">Pomalšie užívatelské prostredie môže zlepšiť stabilitu</string>
+ <string name="log_viewer_title">Denník udalostí</string>
+ <string name="logcat_error">Nepodarilo sa spustiť logcat: </string>
+ <string name="module_enabler_enabled_summary">Pomalší userspace backend môže zlepšiť stabilitu</string>
<string name="module_installer_error">Niečo sa pokazilo. Prosím, skúste znova</string>
<string name="module_installer_not_found">Pre vaše zariadenie nie sú k dispozícii žiadne moduly</string>
- <string name="module_installer_title">Stiahni a nainštaluj kernel modul</string>
- <string name="module_installer_working">Sťahuje a inštalujem…</string>
+ <string name="module_installer_title">Stiahnutie a inštalácia kernelového modulu</string>
+ <string name="module_installer_working">Sťahuje sa a inštaluje sa…</string>
<string name="mtu">Maximálna prenosová jednotka</string>
<string name="multiple_tunnels_summary_off">Zapnutím jedného tunela vypnete ostatné</string>
- <string name="multiple_tunnels_summary_on">Môžu byť zapnuté viaceré tunele naraz</string>
+ <string name="multiple_tunnels_summary_on">Môžu byť zapnuté viaceré tunely naraz</string>
+ <string name="multiple_tunnels_title">Povoliť viacero tunelov naraz</string>
<string name="name">Názov</string>
- <string name="no_config_error">Pokúšam sa vyvolať tunel bez konfigurácie</string>
+ <string name="no_config_error">Pokúšam sa zapnúť tunel bez konfigurácie</string>
<string name="no_configs_error">Nenašli sa žiadne konfigurácie</string>
<string name="no_tunnels_error">Neexistujú žiadne tunely</string>
+ <string name="parse_error_generic">reťazec</string>
<string name="parse_error_inet_address">IP adresa</string>
<string name="parse_error_inet_endpoint">koncový bod</string>
<string name="parse_error_inet_network">IP sieť</string>
<string name="parse_error_integer">číslo</string>
+ <string name="parse_error_reason">Nedá sa parsovať %1$s “%2$s”</string>
+ <string name="pre_shared_key">Vopred zdieľaný kľúč</string>
<string name="pre_shared_key_enabled">povolené</string>
<string name="private_key">Súkromný kľúč</string>
<string name="public_key">Verejný kľúč</string>
+ <string name="qr_code_hint">Tip: vygenerovať s `qrencode -t ansiutf8 &lt; tunnel.conf`.</string>
<string name="restore_on_boot_title">Obnov po štarte</string>
<string name="save">Uložiť</string>
<string name="select_all">Označiť všetko</string>
@@ -103,10 +121,10 @@
<string name="toggle_all">Prepnúť všetko</string>
<string name="tools_installer_already">wg a wg-quick už sú nainštalované</string>
<string name="tools_installer_initial">Nainštalovať voliteľné nástroje pre skriptovanie</string>
- <string name="tools_installer_initial_magisk">Nainštalovať voliteľné nástroje pre skriptovanie ako Magisk module</string>
+ <string name="tools_installer_initial_magisk">Nainštalovať voliteľné nástroje pre skriptovanie ako Magisk modul</string>
<string name="tools_installer_success_magisk">wg a wg-quick sú nainštalované ako Magisk modul (reštart požadovaný)</string>
<string name="tools_installer_title">Inštalácia nástrojov príkazového riadku</string>
- <string name="tools_installer_working">Inštalácia wg a wg-quick</string>
+ <string name="tools_installer_working">Inštaluje sa wg a wg-quick</string>
<string name="tools_unavailable_error">Potrebné nástroje nie sú k dispozícii</string>
<string name="transfer">Prenos</string>
<string name="transfer_bytes">%d B</string>
@@ -120,11 +138,10 @@
<string name="tunnel_create_success">Úspešne vytvorený tunel “%s”</string>
<string name="tunnel_error_already_exists">Tunel “%s” už existuje</string>
<string name="tunnel_error_invalid_name">Neplatný názov</string>
- <string name="tunnel_list_placeholder">Pridajte tunel pomocou modrého tlačidla</string>
<string name="tunnel_name">Meno tunelu</string>
<string name="tunnel_rename_error">Nepodarilo sa premenovať tunel: %s</string>
<string name="tunnel_rename_success">Úspešne premenovaný tunel na “%s”</string>
- <string name="type_name_kernel_module">Modul jadra</string>
+ <string name="type_name_kernel_module">Kernelový modul</string>
<string name="unknown_error">Neznáma chyba</string>
<string name="version_summary_unknown">Neznáma %s verzia</string>
<string name="version_title">WireGuard pre Android v%s</string>
@@ -132,10 +149,10 @@
<string name="vpn_start_error">Nepodarilo sa spustiť Android VPN službu</string>
<string name="zip_export_error">Nepodarilo sa exportovať tunely: %s</string>
<string name="zip_export_success">Uložené ako “%s”</string>
- <string name="zip_export_summary">Zip súbor bude uložený do priečinka na sťahovanie</string>
+ <string name="zip_export_summary">Zip súbor bude uložený do priečinka stiahnuté</string>
<string name="zip_export_title">Export tunelov do zip súboru</string>
<string name="biometric_prompt_zip_exporter_title">Overovanie pre export tunelov</string>
- <string name="biometric_prompt_private_key_title">Overovanie pre zobrazenie privátneho kľúča</string>
+ <string name="biometric_prompt_private_key_title">Authenticate to view private key</string>
<string name="biometric_auth_error">Overovanie zlyhalo</string>
<string name="biometric_auth_error_reason">Overovanie zlyhalo: %s</string>
</resources>
diff --git a/ui/src/main/res/values-sl/strings.xml b/ui/src/main/res/values-sl/strings.xml
index 8c98bc12..c5600041 100644
--- a/ui/src/main/res/values-sl/strings.xml
+++ b/ui/src/main/res/values-sl/strings.xml
@@ -122,16 +122,19 @@
<string name="create_output_dir_error">Izhodnega imenika ni bilo mogoče ustvariti</string>
<string name="create_temp_dir_error">Lokalnega začasnega imenika ni bilo mogoče ustvariti</string>
<string name="create_tunnel">Ustvari tunel</string>
+ <string name="copied_to_clipboard">%s kopirano v odložišče</string>
<string name="dark_theme_summary_off">V uporabi svetla (dnevna) tema</string>
<string name="dark_theme_summary_on">V uporabi temna (nočna) tema</string>
<string name="dark_theme_title">Uporabi temno temo</string>
<string name="delete">Izbriši</string>
<string name="tv_delete">Izberi tunel za izbris</string>
+ <string name="tv_select_a_storage_drive">Izberite podatkovni pogon</string>
<string name="tv_no_file_picker">Prosim namesti orodje za upravljanje datotek za njihov ogled</string>
<string name="tv_add_tunnel_get_started">Za začetek dodaj tunel</string>
<string name="disable_config_export_title">Onemogoči izvoz nastavitev</string>
- <string name="disable_config_export_description">Onemogočavnje nastavitev za izvoz nastavitev naredi tajne kluče manj dostopne</string>
+ <string name="disable_config_export_description">Onemogočenje nastavitev za izvoz nastavitev naredi zasebne ključe manj dostopne</string>
<string name="dns_servers">Strežniki DNS</string>
+ <string name="dns_search_domains">Pripone DNS</string>
<string name="edit">Uredi</string>
<string name="endpoint">Končna točka</string>
<string name="error_down">Napaka pri zaključevanju tunela: %s</string>
@@ -217,7 +220,7 @@
<string name="tools_installer_initial_magisk">Namestitev izbirnih orodij za skripte kot modula Magisk</string>
<string name="tools_installer_initial_system">Namestitev izbirnih orodij za skripte na sistemsko particijo</string>
<string name="tools_installer_success_magisk">wg in wg-quick kot modul Magisk nameščen (zahtevan ponovni zagon)</string>
- <string name="tools_installer_success_system">wg und wg-quick nameščena na sistemsko particijo</string>
+ <string name="tools_installer_success_system">wg in wg-quick nameščena na sistemsko particijo</string>
<string name="tools_installer_title">Namesti orodja za ukazno vrstico</string>
<string name="tools_installer_working">Nameščam wg in wg-quick</string>
<string name="tools_unavailable_error">Zahtevana orodja niso na voljo</string>
@@ -234,14 +237,15 @@
<string name="tunnel_create_success">Tunel „%s“ uspešno ustvarjen</string>
<string name="tunnel_error_already_exists">Tunel „%s“ že obstaja</string>
<string name="tunnel_error_invalid_name">Neveljavno ime</string>
- <string name="tunnel_list_placeholder">Dodaj tunel s klikom na modri gumb</string>
<string name="tunnel_name">Ime tunela</string>
<string name="tunnel_on_error">Tunela ni bilo mogoče vključiti (wgTurnOn je vrnil %d)</string>
+ <string name="tunnel_dns_failure">Imena DNS gostitelja ni bilo mogoče razrešiti: \"%s\"</string>
<string name="tunnel_rename_error">Tunela ni bilo mogoče preimenovati: %s</string>
<string name="tunnel_rename_success">Tunel uspešno preimenovan v „%s“</string>
<string name="type_name_go_userspace">Uporabniški prostor Go</string>
<string name="type_name_kernel_module">Modul jedra</string>
<string name="unknown_error">Neznana napaka</string>
+ <string name="version_summary">Zaledje %1$s %2$s</string>
<string name="version_summary_checking">Preverjam verzijo zaledja %s</string>
<string name="version_summary_unknown">Neznana verzija %s</string>
<string name="version_title">WireGuard za Android v%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 16765166..910f9e05 100644
--- a/ui/src/main/res/values-sv-rSE/strings.xml
+++ b/ui/src/main/res/values-sv-rSE/strings.xml
@@ -49,7 +49,7 @@
</plurals>
<plurals name="persistent_keepalive_seconds_unit">
<item quantity="one">varje sekund</item>
- <item quantity="other">varje %d sekunder</item>
+ <item quantity="other">var %d sekund</item>
</plurals>
<plurals name="persistent_keepalive_seconds_suffix">
<item quantity="one">sekund</item>
@@ -63,6 +63,7 @@
<string name="allow_remote_control_intents_summary_on">Externa appar kan växla tunnlar (avancerat)</string>
<string name="allow_remote_control_intents_title">Tillåt fjärrstyrningsappar</string>
<string name="allowed_ips">Tillåtna IP-adresser</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 i %2$s</string>
<string name="bad_config_explanation_pka">: Måste vara positivt och högst 65535</string>
@@ -78,6 +79,8 @@
<string name="bad_config_reason_unknown_section">Okänt avsnitt</string>
<string name="bad_config_reason_value_out_of_range">Värde utanför giltigt intervall</string>
<string name="bad_extension_error">Filen måste vara .conf eller .zip</string>
+ <string name="error_no_qr_found">QR-kod hittas inte i bilden</string>
+ <string name="error_qr_checksum">QR-kods checksifferkontroll misslyckades</string>
<string name="cancel">Avbryt</string>
<string name="config_delete_error">Kan inte ta bort konfigurationsfilen %s</string>
<string name="config_exists_error">Konfiguration för ”%s” finns redan</string>
@@ -87,6 +90,9 @@
<string name="config_save_error">Kan inte spara konfigurationen för ”%1$s”: %2$s</string>
<string name="config_save_success">Konfigurationen för ”%s ” sparades</string>
<string name="create_activity_title">Skapa WireGuard Tunnel</string>
+ <string name="create_bin_dir_error">Kan inte skapa lokal binärkatalog</string>
+ <string name="create_downloads_file_error">Kan inte skapa fil i nedladdningskatalogen</string>
+ <string name="create_empty">Skapa från grunden</string>
<string name="create_from_file">Importera från fil eller arkiv</string>
<string name="create_from_qr_code">Skanna från QR-kod</string>
<string name="create_output_dir_error">Kan inte skapa utdatakatalog</string>
@@ -101,16 +107,47 @@
<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>
+ <string name="dns_search_domains">Sök domäner</string>
<string name="edit">Redigera</string>
- <string name="endpoint">Ändpunkt</string>
+ <string name="endpoint">Slutpunkt</string>
<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>
+ <string name="generic_error">Okänt ”%s” fel</string>
+ <string name="hint_automatic">(automatisk)</string>
+ <string name="hint_generated">(skapad)</string>
+ <string name="hint_optional">(valfritt)</string>
+ <string name="hint_optional_discouraged">(valfritt, rekommenderas inte)</string>
+ <string name="hint_random">(slumpmässigt)</string>
+ <string name="illegal_filename_error">Ogiltigt filnamn ”%s”</string>
+ <string name="import_error">Kan inte importera tunnel: %s</string>
+ <string name="import_from_qr_code">Importera tunnel från QR-kod</string>
+ <string name="import_success">Importerade ”%s”</string>
+ <string name="interface_title">Gränssnitt</string>
+ <string name="key_contents_error">Ogiltiga tecken i nyckel</string>
+ <string name="key_length_error">Felaktig nyckellängd</string>
+ <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>
+ <string name="log_export_success">Sparad till ”%s”</string>
+ <string name="log_export_title">Exportera loggfil</string>
<string name="log_saver_activity_label">Spara logg</string>
- <string name="log_viewer_pref_summary">Loggar kan hjälpa till med felsökning</string>
+ <string name="log_viewer_pref_summary">Loggfiler kan underlätta vid felsökning</string>
<string name="log_viewer_pref_title">Visa applikationslogg</string>
<string name="log_viewer_title">Logg</string>
<string name="logcat_error">Kunde inte köra logcat: </string>
@@ -134,23 +171,28 @@
<string name="no_tunnels_error">Inga tunnlar finns</string>
<string name="parse_error_generic">sträng</string>
<string name="parse_error_inet_address">IP-adress</string>
- <string name="parse_error_inet_endpoint">endpoint</string>
+ <string name="parse_error_inet_endpoint">slutpunkt</string>
<string name="parse_error_inet_network">IP-nätverk</string>
<string name="parse_error_integer">nummer</string>
<string name="parse_error_reason">Kan inte tolka %1$s ”%2$s”</string>
<string name="peer">Klient</string>
+ <string name="permission_description">styra WireGuard tunnlar, aktivera och inaktivera tunnlar efter behag, möjlighet till felkoppling av internettrafik</string>
<string name="permission_label">kontrollera WireGuard tunnlar</string>
<string name="persistent_keepalive">Beständig keepalive</string>
<string name="pre_shared_key">Fördelad nyckel</string>
<string name="pre_shared_key_enabled">aktiverad</string>
<string name="private_key">Privat nyckel</string>
- <string name="public_key">Publik 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>
<string name="save">Spara</string>
- <string name="select_all">Markera alla</string>
+ <string name="select_all">Välj alla</string>
<string name="settings">Inställningar</string>
<string name="shell_exit_status_read_error">Shell kan inte läsa avslutningsstatus</string>
<string name="shell_marker_count_error">Shell förväntade sig 4 markörer, tog emot %d</string>
@@ -158,4 +200,60 @@
<string name="success_application_will_restart">Framgång. Applikationen kommer nu att starta om…</string>
<string name="toggle_all">Växla alla</string>
<string name="toggle_error">Fel vid växling av WireGuard-tunnel: %s</string>
+ <string name="tools_installer_already">wg och wg-quick är redan installerade</string>
+ <string name="tools_installer_failure">Kan inte installera kommandoradsverktyg (ej root?)</string>
+ <string name="tools_installer_initial">Installera valfria verktyg för skriptprogram</string>
+ <string name="tools_installer_initial_magisk">Installera valfria verktyg för skript som Magisk modul</string>
+ <string name="tools_installer_initial_system">Installera valfria verktyg för skriptning till systempartitionen</string>
+ <string name="tools_installer_success_magisk">wg och wg-quick installerat som en Magisk modul (omstart krävs)</string>
+ <string name="tools_installer_success_system">wg och wg-quick installerat i systempartitionen</string>
+ <string name="tools_installer_title">Installera kommandoradsverktyg</string>
+ <string name="tools_installer_working">Installera wg och wg-quick</string>
+ <string name="tools_unavailable_error">Nödvändiga verktyg är inte tillgängliga</string>
+ <string name="transfer">Överföring</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">Kunde inte skapa tun-enhet</string>
+ <string name="tunnel_config_error">Går inte att konfigurera tunneln (wg-quick returnerade %d)</string>
+ <string name="tunnel_create_error">Kan inte skapa tunnel: %s</string>
+ <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>
+ <string name="tunnel_rename_error">Kan inte byta namn på tunnel: %s</string>
+ <string name="tunnel_rename_success">Lyckades döpa om tunnel till “%s”</string>
+ <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>
+ <string name="version_title">WireGuard för Android v%s</string>
+ <string name="vpn_not_authorized_error">VPN-tjänsten är inte godkänd av användaren</string>
+ <string name="vpn_start_error">Kan inte starta Android VPN-tjänst</string>
+ <string name="zip_export_error">Kan inte exportera tunnlar: %s</string>
+ <string name="zip_export_success">Sparad till ”%s”</string>
+ <string name="zip_export_summary">Zip-filen kommer att sparas till nedladdningskatalogen</string>
+ <string name="zip_export_title">Exportera tunnlar till zip-fil</string>
+ <string name="biometric_prompt_zip_exporter_title">Godkänn för att exportera tunnlar</string>
+ <string name="biometric_prompt_private_key_title">Godkänn för att visa tunnelns privata nycklar</string>
+ <string name="biometric_auth_error">Fel vid godkännande</string>
+ <string name="biometric_auth_error_reason">Fel vid godkännande: %s</string>
</resources>
diff --git a/ui/src/main/res/values-tr-rTR/strings.xml b/ui/src/main/res/values-tr-rTR/strings.xml
index b05d395d..57bc0ca5 100644
--- a/ui/src/main/res/values-tr-rTR/strings.xml
+++ b/ui/src/main/res/values-tr-rTR/strings.xml
@@ -79,6 +79,8 @@
<string name="bad_config_reason_unknown_section">Bilinmeyen bölüm</string>
<string name="bad_config_reason_value_out_of_range">Aralık dışı değer</string>
<string name="bad_extension_error">Dosya .conf veya .zip olmalıdır</string>
+ <string name="error_no_qr_found">Görselde QR kod bulunamadı</string>
+ <string name="error_qr_checksum">QR kod doğrulaması başarısız</string>
<string name="cancel">İptal</string>
<string name="config_delete_error">%s konfigürasyon dosyası silinemiyor</string>
<string name="config_exists_error">“%s” için konfigürasyon zaten var</string>
@@ -92,7 +94,7 @@
<string name="create_downloads_file_error">İndirilenler klasöründe dosya oluşturulamadı</string>
<string name="create_empty">Sıfırdan oluştur</string>
<string name="create_from_file">Dosya veya arşivden ekle</string>
- <string name="create_from_qr_code">QR kodundan ekle</string>
+ <string name="create_from_qr_code">QR Kodu Tara</string>
<string name="create_output_dir_error">Çıktı klasörü oluşturulamıyor</string>
<string name="create_temp_dir_error">Yerel geçici dizin oluşturulamıyor</string>
<string name="create_tunnel">Tünel Oluştur</string>
@@ -105,14 +107,19 @@
<string name="tv_select_a_storage_drive">Bir depolama sürücüsü seçin</string>
<string name="tv_no_file_picker">Dosyalara göz atmak için lütfen bir dosya yönetim aracı yükleyin</string>
<string name="tv_add_tunnel_get_started">Başlamak için bir tünel ekleyin</string>
+ <string name="donate_title">♥ WireGuard Projesine Bağış Yapın</string>
+ <string name="donate_summary">Her katkı yardımcı olur</string>
+ <string name="donate_google_play_disappointment">WireGuard Projesini desteklediğiniz için teşekkür ederiz!\n\nNe yazık ki, Google\'ın politikaları nedeniyle, proje web sayfasının bağış yapabileceğiniz bölümüne bağlantı vermemize izin verilmiyor. Umarım bunu çözebilirsin!\n\nKatkılarınız için tekrar teşekkürler.</string>
<string name="disable_config_export_title">Ayarları dışa aktarmayı kapa</string>
<string name="disable_config_export_description">Ayarları dışa aktarmayı kapamak gizli anahtarları daha az erişilebilir kılar</string>
<string name="dns_servers">DNS sunucuları</string>
+ <string name="dns_search_domains">Alan adı ara</string>
<string name="edit">Düzenle</string>
<string name="endpoint">Uç nokta</string>
<string name="error_down">Tünel kapatılırken hata oluştu: %s</string>
<string name="error_fetching_apps">Uygulama listesi getirilirken hata oluştu: %s</string>
<string name="error_root">Lütfen root erişimi elde edin ve tekrar deneyin</string>
+ <string name="error_prepare">Tünel hazırlanırken hata oluştu: %s</string>
<string name="error_up">Tünel açılırken hata oluştu: %s</string>
<string name="exclude_private_ips">Özel IP’leri hariç tut</string>
<string name="generate_new_private_key">Yeni özel anahtar oluştur</string>
@@ -132,6 +139,8 @@
<string name="key_length_explanation_base64">: WireGuard base64 anahtarları 44 karakter (32 bayt) olmalıdır</string>
<string name="key_length_explanation_binary">: WireGuard anahtarları 32 bayt olmalıdır</string>
<string name="key_length_explanation_hex">: WireGuard onaltılık anahtarları 64 karakter (32 bayt) olmalıdır</string>
+ <string name="latest_handshake">En son el sıkışma</string>
+ <string name="latest_handshake_ago">%s önce</string>
<string name="listen_port">Dinlenen port</string>
<string name="log_export_error">Günlük dışa aktarılamıyor: %s</string>
<string name="log_export_subject">WireGuard Android Günlük Dosyası</string>
@@ -175,6 +184,10 @@
<string name="private_key">Özel anahtar</string>
<string name="public_key">Genel anahtar</string>
<string name="qr_code_hint">İpucu: `qrencode -t ansiutf8 &lt; tunnel.conf` ile oluşturun.</string>
+ <string name="quick_settings_tile_add_title">Hızlı ayarlar paneline kutucuk ekle</string>
+ <string name="quick_settings_tile_add_summary">Kısayol kutucuğu en son tüneli değiştirir</string>
+ <string name="quick_settings_tile_add_failure">Kısayol kutucuğu eklenemiyor: hata %d</string>
+ <string name="quick_settings_tile_action">Tüneli aç/kapat</string>
<string name="restore_on_boot_summary_off">Önyüklemede etkin tünelleri açmayacak</string>
<string name="restore_on_boot_summary_on">Önyüklemede etkin tünelleri açacak</string>
<string name="restore_on_boot_title">Cihaz açılırken başlat</string>
@@ -210,7 +223,7 @@
<string name="tunnel_create_success">Tünel başarıyla oluşturuldu “%s”</string>
<string name="tunnel_error_already_exists">“%s” tüneli zaten mevcut</string>
<string name="tunnel_error_invalid_name">Geçersiz isim</string>
- <string name="tunnel_list_placeholder">Mavi düğmeyi kullanarak bir tünel ekleyin</string>
+ <string name="tunnel_list_placeholder">Aşağıdaki düğmeyi kullanarak bir tünel ekleyin</string>
<string name="tunnel_name">Tünel İsmi</string>
<string name="tunnel_on_error">Tünel aktif edilemiyor (wgTornOn döndürdü: %d)</string>
<string name="tunnel_dns_failure">DNS adı çözülemedi: \"%s\"</string>
@@ -219,6 +232,16 @@
<string name="type_name_go_userspace">Kullanıcı alanına git</string>
<string name="type_name_kernel_module">Çekirdek modülü</string>
<string name="unknown_error">Bilinmeyen hata</string>
+ <string name="updater_avalable">Bir uygulama güncellemesi mevcut. Lütfen şimdi güncelleyin.</string>
+ <string name="updater_action">İndir &amp; Güncelle</string>
+ <string name="updater_rechecking">Güncelleme meta verileri getiriliyor…</string>
+ <string name="updater_download_progress">Güncelleme indiriliyor: %1$s / %2$s (%3$.2f%%)</string>
+ <string name="updater_download_progress_nototal">Güncelleme indiriliyor: %s</string>
+ <string name="updater_installing">Güncelleme yükleniyor…</string>
+ <string name="updater_failure">Güncelleme hatası: %s. Kısa bir süre sonra tekrar denenecek…</string>
+ <string name="updater_corrupt_title">Uygulama Bozuk</string>
+ <string name="updater_corrupt_message">Bu uygulama bozuk. Lütfen APK\'yı aşağıda bağlantısı verilen web sitesinden yeniden indirin. Daha sonra bu uygulamayı kaldırın ve indirilen APK\'dan yeniden yükleyin.</string>
+ <string name="updater_corrupt_navigate">Web Sitesini Aç</string>
<string name="version_summary">%1$s arka uç %2$s</string>
<string name="version_summary_checking">%s arka uç sürümü kontrol ediliyor</string>
<string name="version_summary_unknown">Bilinmeyen %s sürümü</string>
diff --git a/ui/src/main/res/values-uk-rUA/strings.xml b/ui/src/main/res/values-uk-rUA/strings.xml
index 4ecdb781..15b51b90 100644
--- a/ui/src/main/res/values-uk-rUA/strings.xml
+++ b/ui/src/main/res/values-uk-rUA/strings.xml
@@ -105,6 +105,8 @@
<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="error_no_qr_found">QR-код не знайдено на зображенні</string>
+ <string name="error_qr_checksum">Не вдалося перевірити контрольну суму QR-коду</string>
<string name="cancel">Скасувати</string>
<string name="config_delete_error">Не вдалося видалити файл конфігурації %s</string>
<string name="config_exists_error">Конфігурація для \"%s\" вже існує</string>
@@ -131,14 +133,18 @@
<string name="tv_select_a_storage_drive">Виберіть диск зберігання</string>
<string name="tv_no_file_picker">Будь ласка, встановіть провідник для перегляду файлів</string>
<string name="tv_add_tunnel_get_started">Додайте тунель, щоб почати</string>
+ <string name="donate_title">♥️ Пожертвуйте на проект WireGuard</string>
+ <string name="donate_summary">Кожен внесок допомагає</string>
<string name="disable_config_export_title">Вимкнути експорт конфігурації</string>
<string name="disable_config_export_description">Вимкнення експорту налаштувань робить приватні ключі менш доступними</string>
<string name="dns_servers">DNS-сервери</string>
+ <string name="dns_search_domains">Пошук доменів</string>
<string name="edit">Редагувати</string>
<string name="endpoint">Endpoint</string>
<string name="error_down">Помилка вимкнення тунелю: %s</string>
<string name="error_fetching_apps">Помилка при отриманні списку додатків: %s</string>
<string name="error_root">Будь ласка, отримайте root-доступ і спробуйте ще раз</string>
+ <string name="error_prepare">Помилка підготовки тунелю: %s</string>
<string name="error_up">Помилка при увімкненні тунелю: %s</string>
<string name="exclude_private_ips">Виключити приватні IP</string>
<string name="generate_new_private_key">Згенерувати новий приватний ключ</string>
@@ -158,6 +164,8 @@
<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">: hex ключі WireGuard повинні мати довжину 64 символи (32 байти)</string>
+ <string name="latest_handshake">Останнє рукостискання</string>
+ <string name="latest_handshake_ago">%s тому</string>
<string name="listen_port">Порт</string>
<string name="log_export_error">Не вдалося експортувати журнал: %s</string>
<string name="log_export_subject">Файл журналу WireGuard</string>
@@ -236,7 +244,7 @@
<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_list_placeholder">Додайте тунель, використовуючи кнопку нижче</string>
<string name="tunnel_name">Назва тунелю</string>
<string name="tunnel_on_error">Неможливо ввімкнути тунель (wgTurnon returned %d)</string>
<string name="tunnel_dns_failure">Не вдалося знайти DNS хост: “%s</string>
diff --git a/ui/src/main/res/values-v23/styles.xml b/ui/src/main/res/values-v23/styles.xml
new file mode 100644
index 00000000..13feb8c3
--- /dev/null
+++ b/ui/src/main/res/values-v23/styles.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <style name="AppTheme" parent="AppThemeBase">
+ <item name="android:statusBarColor">?android:colorBackground</item>
+ <item name="android:windowLightStatusBar">@bool/light_status_bar</item>
+ </style>
+</resources>
diff --git a/ui/src/main/res/values-v27/styles.xml b/ui/src/main/res/values-v27/styles.xml
index b797e105..f94cadb1 100644
--- a/ui/src/main/res/values-v27/styles.xml
+++ b/ui/src/main/res/values-v27/styles.xml
@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
- <style name="AppTheme" parent="WireGuardTheme">
- <item name="android:navigationBarColor">@color/navigation_bar_color</item>
- <item name="android:windowLightNavigationBar">@bool/light_navigation_bar</item>
+ <style name="AppTheme" parent="AppThemeBase">
+ <item name="android:statusBarColor">?android:colorBackground</item>
<item name="android:windowLightStatusBar">@bool/light_status_bar</item>
+ <item name="android:navigationBarColor">?android:colorBackground</item>
+ <item name="android:windowLightNavigationBar">@bool/light_navigation_bar</item>
</style>
</resources>
diff --git a/ui/src/main/res/values-vi-rVN/strings.xml b/ui/src/main/res/values-vi-rVN/strings.xml
new file mode 100644
index 00000000..79d8d6c3
--- /dev/null
+++ b/ui/src/main/res/values-vi-rVN/strings.xml
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="other">Không thể xóa %d tunnel(s): %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="other">Đã xóa thành công %d tunnel(s)</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="other">Đã chọn %d tunnel(s)</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="other">Đã nhập %1$d trong số %2$d tunnel(s)</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="other">Đã nhập %d tunnel(s)</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="other">%d Ứng dụng được loại trừ</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="other">%d Ứng dụng được bao gồm</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="other">Đã loại trừ %d</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <item quantity="other">Đã thêm vào %d</item>
+ </plurals>
+ <string name="all_applications">Tất cả các ứng dụng</string>
+ <string name="exclude_from_tunnel">Ngoại trừ</string>
+ <string name="include_in_tunnel">Chỉ bao gồm</string>
+ <plurals name="include_n_applications">
+ <item quantity="other">Thêm vào %d ứng dụng</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="other">Loại trừ %d ứng dụng</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="other">Mỗi %d giây</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="other">Giây</item>
+ </plurals>
+ <string name="use_all_applications">Xài dùng tất cả app</string>
+ <string name="add_peer">Thêm cộng tác viên</string>
+ <string name="addresses">Địa chỉ</string>
+ <string name="applications">Ứng dụng</string>
+ <string name="allow_remote_control_intents_summary_off">Các ứng dụng bên ngoài không thể bật/tắt tunnels (khuyến nghị)</string>
+ <string name="allow_remote_control_intents_summary_on">Các ứng dụng bên ngoài có thể bật/tắt tunnels (nâng cao)</string>
+ <string name="allow_remote_control_intents_title">Cho phép điều khiển ứng dụng từ xa</string>
+ <string name="allowed_ips">IP Cho phép </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 trong %2$s</string>
+ <string name="bad_config_explanation_pka">: Phải là số dương và không lớn hơn 65535</string>
+ <string name="bad_config_explanation_positive_number">Giá trị phải là số dương</string>
+ <string name="bad_config_explanation_udp_port">: Phải là port UDP hợp lý</string>
+ <string name="bad_config_reason_invalid_key">Khoá không hợp lệ</string>
+ <string name="bad_config_reason_invalid_number">Số không hợp lệ</string>
+ <string name="bad_config_reason_invalid_value">Giá trị không hợp lệ</string>
+ <string name="bad_config_reason_missing_attribute">Thuộc tính bị thiếu</string>
+ <string name="bad_config_reason_missing_section">Phần bị thiếu</string>
+ <string name="bad_config_reason_syntax_error">Lỗi cú pháp</string>
+ <string name="bad_config_reason_unknown_attribute">Thuộc tính không tồn tại</string>
+ <string name="bad_config_reason_unknown_section">Mục không xác định</string>
+ <string name="bad_config_reason_value_out_of_range">Giá trị vượt ngoài khoảng cho phép</string>
+ <string name="bad_extension_error">File phải là .conf hoặc .zip</string>
+ <string name="error_no_qr_found">Không tìm thấy QR code trong ảnh</string>
+ <string name="error_qr_checksum">Kiểm tra checksum QR code không thành công</string>
+ <string name="cancel">Hủy</string>
+ <string name="config_delete_error">Không thể xóa file cấu hình \"%s\"</string>
+ <string name="config_exists_error">Cấu hình cho \"%s\" đã tồn tại</string>
+ <string name="config_file_exists_error">File cấu hình cho \"%s\" đã tồn tại</string>
+ <string name="config_not_found_error">Không tìm thấy file cấu hình \"%s\"</string>
+ <string name="config_rename_error">Không thể xóa file cấu hình \"%s\"</string>
+ <string name="config_save_error">Không thể lưu cấu hình cho \"%1$s\": %2$s</string>
+ <string name="config_save_success">Đã lưu cấu hình thành công cho \"%s\"</string>
+ <string name="create_activity_title">Tạo ra Wireguard VPN</string>
+ <string name="create_bin_dir_error">Không thế tạo local binary directory</string>
+ <string name="create_downloads_file_error">Không thể tạo file trong thư mục download</string>
+ <string name="create_empty">Làm lại từ đầu</string>
+ <string name="create_from_file">Nhập từ file hoặc archive</string>
+ <string name="create_from_qr_code">Quét mã QR</string>
+ <string name="create_output_dir_error">Không thể tạo tập tin xuất ra</string>
+ <string name="create_temp_dir_error">Không thế tạo local binary directory</string>
+ <string name="create_tunnel">Tạo VPN</string>
+ <string name="copied_to_clipboard">%s đã sao chép vào bộ nhớ tạm</string>
+ <string name="dark_theme_summary_off">Đang sử dụng ánh sáng (ngày)</string>
+ <string name="dark_theme_summary_on">Đang sử dụng đề tối (ban đêm)</string>
+ <string name="dark_theme_title">Sử dụng đề tối</string>
+ <string name="delete">Xóa</string>
+ <string name="tv_delete">Chọn tunnel để xóa</string>
+ <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>
+ <string name="dns_search_domains">Tên miền của DNS tìm kiếm</string>
+ <string name="edit">Chỉnh sửa</string>
+ <string name="endpoint">Đầu cuối</string>
+ <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 60e84c7e..7757e2fe 100644
--- a/ui/src/main/res/values-zh-rCN/strings.xml
+++ b/ui/src/main/res/values-zh-rCN/strings.xml
@@ -31,10 +31,10 @@
<string name="exclude_from_tunnel">不生效</string>
<string name="include_in_tunnel">生效</string>
<plurals name="include_n_applications">
- <item quantity="other">设定 %d 个应用</item>
+ <item quantity="other">选定 %d 个应用</item>
</plurals>
<plurals name="exclude_n_applications">
- <item quantity="other">设定 %d 个应用</item>
+ <item quantity="other">选定 %d 个应用</item>
</plurals>
<plurals name="persistent_keepalive_seconds_unit">
<item quantity="other">每隔 %d 秒</item>
@@ -46,14 +46,14 @@
<string name="add_peer">添加节点</string>
<string name="addresses">局域网 IP 地址</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_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="bad_config_context">%1$s 的 %2$s 字段</string>
<string name="bad_config_context_top_level">%s</string>
<string name="bad_config_error">在 %2$s发生了%1$s的问题</string>
- <string name="bad_config_explanation_pka">:必须为正整数且不超过 65535</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>
@@ -66,6 +66,8 @@
<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="error_no_qr_found">图片中未发现二维码</string>
+ <string name="error_qr_checksum">二维码校验失败</string>
<string name="cancel">取消</string>
<string name="config_delete_error">无法删除配置 “%s”</string>
<string name="config_exists_error">“%s” 的配置已存在</string>
@@ -91,15 +93,20 @@
<string name="tv_delete">选择要删除的隧道</string>
<string name="tv_select_a_storage_drive">选择一个存储驱动器</string>
<string name="tv_no_file_picker">请安装一个文件管理工具以浏览文件</string>
- <string name="tv_add_tunnel_get_started">添加第一个隧道</string>
+ <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只可惜,受谷歌的政策所限,我们不能在此展示项目捐赠页面的链接,还望您自行访问捐赠页面!\n\n再次感谢您的贡献。</string>
<string name="disable_config_export_title">禁止导出配置</string>
<string name="disable_config_export_description">禁止导出配置可降低私钥泄露的风险</string>
<string name="dns_servers">DNS 服务器</string>
+ <string name="dns_search_domains">搜索域名</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_prepare">准备连接时出错:%s</string>
<string name="error_up">建立连接时出错:%s</string>
<string name="exclude_private_ips">排除局域网</string>
<string name="generate_new_private_key">生成新的私钥</string>
@@ -113,12 +120,14 @@
<string name="import_error">无法导入隧道:%s</string>
<string name="import_from_qr_code">从二维码导入隧道</string>
<string name="import_success">导入了 “%s”</string>
- <string name="interface_title">接口 / Interface</string>
+ <string name="interface_title">本地(Interface)</string>
<string name="key_contents_error">密钥中含有错误字符</string>
<string name="key_length_error">密钥长度错误</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 的十六进制密钥长度必须为 64 个字符(32 字节)</string>
+ <string name="latest_handshake">上次握手时间</string>
+ <string name="latest_handshake_ago">%s之前</string>
<string name="listen_port">监听端口</string>
<string name="log_export_error">无法导出日志:%s</string>
<string name="log_export_subject">WireGuard 日志文件</string>
@@ -129,7 +138,7 @@
<string name="log_viewer_pref_title">查看应用日志</string>
<string name="log_viewer_title">日志</string>
<string name="logcat_error">无法运行 logcat: </string>
- <string name="module_enabler_disabled_summary">内核空间的模块性能较强,但可能不稳定</string>
+ <string name="module_enabler_disabled_summary">内核模块(实验性)能够增强性能,启用时需谨慎</string>
<string name="module_enabler_disabled_title">启用内核模块</string>
<string name="module_enabler_enabled_summary">用户空间的模块性能较弱,但稳定性更好</string>
<string name="module_enabler_enabled_title">停用内核模块</string>
@@ -153,7 +162,7 @@
<string name="parse_error_inet_network">\u0020IP 网络</string>
<string name="parse_error_integer">数字</string>
<string name="parse_error_reason">无法解析%1$s “%2$s”\u0020</string>
- <string name="peer">节点 / Peer</string>
+ <string name="peer">远程(Peer)</string>
<string name="permission_description">自由控制 WireGuard 隧道的开启或关闭,但可能会导致流量误传</string>
<string name="permission_label">控制 WireGuard 隧道</string>
<string name="persistent_keepalive">连接保活间隔</string>
@@ -162,6 +171,10 @@
<string name="private_key">私钥</string>
<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="restore_on_boot_summary_off">未启用</string>
<string name="restore_on_boot_summary_on">设备启动时自动开启上次使用的隧道</string>
<string name="restore_on_boot_title">启动时恢复</string>
@@ -172,7 +185,7 @@
<string name="shell_marker_count_error">Shell 应获取 4 个标记,获取到 %d 个</string>
<string name="shell_start_error">Shell 启动失败:%d</string>
<string name="success_application_will_restart">成功,应用即将重启…</string>
- <string name="toggle_all">反选</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">无法安装命令行工具(尚未获取 root 权限?)</string>
@@ -206,7 +219,17 @@
<string name="type_name_go_userspace">Go userspace</string>
<string name="type_name_kernel_module">Kernel module</string>
<string name="unknown_error">未知错误</string>
- <string name="version_summary">%1$s 后端 %2$s</string>
+ <string name="updater_avalable">WireGuard 可以更新了,请立即更新。</string>
+ <string name="updater_action">下载 &amp; 更新</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,然后卸载此应用并重新安装。</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>
<string name="version_summary_unknown">未知的 %s 版本</string>
<string name="version_title">WireGuard for Android v%s</string>
@@ -216,8 +239,8 @@
<string name="zip_export_success">已保存至 “%s”</string>
<string name="zip_export_summary">zip 压缩包将保存至下载文件夹</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_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>
diff --git a/ui/src/main/res/values-zh-rTW/strings.xml b/ui/src/main/res/values-zh-rTW/strings.xml
index f4150e2c..14e83a20 100644
--- a/ui/src/main/res/values-zh-rTW/strings.xml
+++ b/ui/src/main/res/values-zh-rTW/strings.xml
@@ -4,19 +4,19 @@
<item quantity="other">無法刪除 %d 個通道: %s</item>
</plurals>
<plurals name="delete_success">
- <item quantity="other">成功刪除了 %d 個通道</item>
+ <item quantity="other">成功刪除 %d 個通道</item>
</plurals>
<plurals name="delete_title">
- <item quantity="other">選擇了 %d 個通道</item>
+ <item quantity="other">已選擇 %d 個通道</item>
</plurals>
<plurals name="import_partial_success">
- <item quantity="other">已匯入 %1$d 個隧道(共 %2$d 個)</item>
+ <item quantity="other">已匯入 %1$d 個通道 (共 %2$d 個)</item>
</plurals>
<plurals name="import_total_success">
- <item quantity="other">匯入了 %d 個通道</item>
+ <item quantity="other">已匯入 %d 個通道</item>
</plurals>
<plurals name="set_excluded_applications">
- <item quantity="other">排除了 %d 個應用程式</item>
+ <item quantity="other">已排除 %d 個應用程式</item>
</plurals>
<plurals name="set_included_applications">
<item quantity="other">套用到 %d 個應用程式</item>
@@ -31,10 +31,10 @@
<string name="exclude_from_tunnel">排除</string>
<string name="include_in_tunnel">只套用到</string>
<plurals name="include_n_applications">
- <item quantity="other">套用到 %d 個應用程式</item>
+ <item quantity="other">已套用 %d 個應用程式</item>
</plurals>
<plurals name="exclude_n_applications">
- <item quantity="other">排除了 %d 個應用程式</item>
+ <item quantity="other">已排除 %d 個應用程式</item>
</plurals>
<plurals name="persistent_keepalive_seconds_unit">
<item quantity="other">每隔 %d 秒</item>
@@ -66,12 +66,14 @@
<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="error_no_qr_found">圖片中找不到二維碼</string>
+ <string name="error_qr_checksum">二維碼校驗和驗證失敗</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_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>
@@ -93,34 +95,35 @@
<string name="tv_no_file_picker">請安裝一個檔案管理程式以便瀏覽檔案</string>
<string name="tv_add_tunnel_get_started">新增第一個通道</string>
<string name="disable_config_export_title">禁止匯出設定檔</string>
- <string name="disable_config_export_description">禁止匯出設定檔可以讓私鑰比較難存取到</string>
+ <string name="disable_config_export_description">禁止匯出設定檔可以降低私鑰被存取的機會</string>
<string name="dns_servers">DNS 伺服器</string>
+ <string name="dns_search_domains">搜尋網域</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="error_root">請取得 root 存取權後再試一次</string>
+ <string name="error_up">啟動通道錯誤: %s</string>
<string name="exclude_private_ips">排除私有 IPs</string>
<string name="generate_new_private_key">產生新的私鑰</string>
- <string name="generic_error">不明的「%s」錯誤</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_optional_discouraged">(選填,不建議)</string>
<string name="hint_random">(隨機)</string>
<string name="illegal_filename_error">不合規定的檔名「%s」</string>
- <string name="import_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="import_success">已匯入「%s」</string>
<string name="interface_title">界面</string>
- <string name="key_contents_error">金鑰中有錯誤的字元</string>
- <string name="key_length_error">金鑰的長度錯誤</string>
+ <string name="key_contents_error">密鑰包含錯誤的字元</string>
+ <string name="key_length_error">密鑰長度不正確</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_error">沒辦法匯出日誌: %s</string>
+ <string name="log_export_error">無法匯出日誌: %s</string>
<string name="log_export_subject">WireGuard Android 日誌檔</string>
<string name="log_export_success">儲存到「%s」</string>
<string name="log_export_title">匯出日誌檔</string>
@@ -129,9 +132,94 @@
<string name="log_viewer_pref_title">檢視應用程式日誌</string>
<string name="log_viewer_title">日誌</string>
<string name="logcat_error">無法執行 logcat: </string>
- <string name="module_enabler_disabled_summary">使用還在實驗階段的 kernel module 以便改善效能</string>
- <string name="module_enabler_disabled_title">啟用 kernel module</string>
+ <string name="module_enabler_disabled_summary">使用實驗階段的核心模組以提升效能</string>
+ <string name="module_enabler_disabled_title">啟用後端核心模組</string>
+ <string name="module_enabler_enabled_summary">較慢的用戶空間後端可以提升穩定性</string>
+ <string name="module_enabler_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">最大傳輸單元</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">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">允許控制 WireGuard 隧道,這將隨意啟用和禁用隧道,可能會誤導 Internet 流量</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="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">Shell 無法讀取停止狀態</string>
+ <string name="shell_marker_count_error">殼牌預計有 4 個標記,但收到了 %d</string>
+ <string name="shell_start_error">Shell 啟動失敗: %d</string>
+ <string name="success_application_will_restart">成功。 應用程式即將重新啟動…</string>
<string name="toggle_all">切換全部</string>
+ <string name="toggle_error">切換 WireGuard 通道時發生錯誤:%s</string>
+ <string name="tools_installer_already">wg 及 wg-quick 已安裝</string>
+ <string name="tools_installer_failure">無法安裝命令行工具 (沒有 root 存取權?)</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 安裝為 Magisk 模組 (需要重新開機)</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="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">已接收:%1$s, 已傳送:%2$s</string>
+ <string name="transfer_tibibytes">%.2f TiB</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_name">通道名稱</string>
+ <string name="tunnel_on_error">無法開啟通道 (wgTurnOn 回傳值: %d)</string>
+ <string name="tunnel_dns_failure">無法解析 DNS 主機名稱: \"%s\"</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 於 Android 版本 %s</string>
+ <string name="vpn_not_authorized_error">使用者未授權 VPN 服務</string>
+ <string name="vpn_start_error">無法開啟 Android VPN 服務</string>
+ <string name="zip_export_error">無法匯入通道: %s</string>
+ <string name="zip_export_success">儲存到「%s」</string>
+ <string name="zip_export_summary">Zip 檔將被儲存到「下載」資料夾內</string>
+ <string name="zip_export_title">匯出通道設定至 zip 檔</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>
diff --git a/ui/src/main/res/values/attrs.xml b/ui/src/main/res/values/attrs.xml
index b5b614e9..b91ac60e 100644
--- a/ui/src/main/res/values/attrs.xml
+++ b/ui/src/main/res/values/attrs.xml
@@ -2,10 +2,9 @@
<resources>
<declare-styleable name="Multiselected">
<attr name="state_multiselected" format="boolean" />
- <attr name="colorMultiselectActiveBackground" format="reference|color" />
</declare-styleable>
-
- <declare-styleable name="custom_color">
- <attr name="colorBackground" format="reference|color" />
+ <declare-styleable name="TvCardView">
+ <attr name="state_isUp" format="boolean" />
+ <attr name="state_isDeleting" format="boolean" />
</declare-styleable>
</resources>
diff --git a/ui/src/main/res/values/colors.xml b/ui/src/main/res/values/colors.xml
index 989c6fc1..65e82389 100644
--- a/ui/src/main/res/values/colors.xml
+++ b/ui/src/main/res/values/colors.xml
@@ -1,28 +1,63 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources xmlns:tools="http://schemas.android.com/tools">
- <!-- Base palette -->
- <color name="primary_color">#ffffffff</color>
- <color name="primary_light_color">#ffffffff</color>
- <color name="primary_dark_color">#ffcccccc</color>
- <color name="secondary_color">#ff1a73e8</color>
- <color name="secondary_light_color">#ff1a73e8</color>
- <color name="secondary_dark_color">#ff1a73e8</color>
- <color name="primary_text_color">#ff000000</color>
- <color name="secondary_text_color">#ffffffff</color>
-
- <!-- Theme variables -->
- <color name="color_control_normal">@color/primary_text_color</color>
- <color name="status_bar_color">@color/primary_color</color>
- <color name="navigation_bar_color">#aaffffff</color>
- <color name="list_multiselect_background">#ffeeeeee</color>
- <color name="mtrl_textinput_default_box_stroke_color" tools:override="true">
- @color/secondary_color
- </color>
- <color name="white">#ffffffff</color>
-
- <!-- Log viewer tag colors -->
- <color name="debug_tag_color">#444444</color>
- <color name="error_tag_color">#aa0000</color>
- <color name="info_tag_color">#00aa00</color>
- <color name="warning_tag_color">#aaaa00</color>
+<resources>
+ <color name="seed">#1a73e8</color>
+ <color name="md_theme_light_primary">#005BC0</color>
+ <color name="md_theme_light_onPrimary">#FFFFFF</color>
+ <color name="md_theme_light_primaryContainer">#D8E2FF</color>
+ <color name="md_theme_light_onPrimaryContainer">#001A41</color>
+ <color name="md_theme_light_secondary">#565E71</color>
+ <color name="md_theme_light_onSecondary">#FFFFFF</color>
+ <color name="md_theme_light_secondaryContainer">#DBE2F9</color>
+ <color name="md_theme_light_onSecondaryContainer">#131B2C</color>
+ <color name="md_theme_light_tertiary">#715574</color>
+ <color name="md_theme_light_onTertiary">#FFFFFF</color>
+ <color name="md_theme_light_tertiaryContainer">#FBD7FC</color>
+ <color name="md_theme_light_onTertiaryContainer">#29132D</color>
+ <color name="md_theme_light_error">#BA1A1A</color>
+ <color name="md_theme_light_errorContainer">#FFDAD6</color>
+ <color name="md_theme_light_onError">#FFFFFF</color>
+ <color name="md_theme_light_onErrorContainer">#410002</color>
+ <color name="md_theme_light_background">#FEFBFF</color>
+ <color name="md_theme_light_onBackground">#1B1B1F</color>
+ <color name="md_theme_light_surface">#FEFBFF</color>
+ <color name="md_theme_light_onSurface">#1B1B1F</color>
+ <color name="md_theme_light_surfaceVariant">#E1E2EC</color>
+ <color name="md_theme_light_onSurfaceVariant">#44474F</color>
+ <color name="md_theme_light_outline">#74777F</color>
+ <color name="md_theme_light_inverseOnSurface">#F2F0F4</color>
+ <color name="md_theme_light_inverseSurface">#303033</color>
+ <color name="md_theme_light_inversePrimary">#ADC7FF</color>
+ <color name="md_theme_light_shadow">#000000</color>
+ <color name="md_theme_light_surfaceTint">#005BC0</color>
+ <color name="md_theme_light_outlineVariant">#C4C6D0</color>
+ <color name="md_theme_light_scrim">#000000</color>
+ <color name="md_theme_dark_primary">#ADC7FF</color>
+ <color name="md_theme_dark_onPrimary">#002E68</color>
+ <color name="md_theme_dark_primaryContainer">#004493</color>
+ <color name="md_theme_dark_onPrimaryContainer">#D8E2FF</color>
+ <color name="md_theme_dark_secondary">#BFC6DC</color>
+ <color name="md_theme_dark_onSecondary">#283041</color>
+ <color name="md_theme_dark_secondaryContainer">#3F4759</color>
+ <color name="md_theme_dark_onSecondaryContainer">#DBE2F9</color>
+ <color name="md_theme_dark_tertiary">#DEBCDF</color>
+ <color name="md_theme_dark_onTertiary">#402843</color>
+ <color name="md_theme_dark_tertiaryContainer">#583E5B</color>
+ <color name="md_theme_dark_onTertiaryContainer">#FBD7FC</color>
+ <color name="md_theme_dark_error">#FFB4AB</color>
+ <color name="md_theme_dark_errorContainer">#93000A</color>
+ <color name="md_theme_dark_onError">#690005</color>
+ <color name="md_theme_dark_onErrorContainer">#FFDAD6</color>
+ <color name="md_theme_dark_background">#1B1B1F</color>
+ <color name="md_theme_dark_onBackground">#E3E2E6</color>
+ <color name="md_theme_dark_surface">#1B1B1F</color>
+ <color name="md_theme_dark_onSurface">#E3E2E6</color>
+ <color name="md_theme_dark_surfaceVariant">#44474F</color>
+ <color name="md_theme_dark_onSurfaceVariant">#C4C6D0</color>
+ <color name="md_theme_dark_outline">#8E9099</color>
+ <color name="md_theme_dark_inverseOnSurface">#1B1B1F</color>
+ <color name="md_theme_dark_inverseSurface">#E3E2E6</color>
+ <color name="md_theme_dark_inversePrimary">#005BC0</color>
+ <color name="md_theme_dark_shadow">#000000</color>
+ <color name="md_theme_dark_surfaceTint">#ADC7FF</color>
+ <color name="md_theme_dark_outlineVariant">#44474F</color>
+ <color name="md_theme_dark_scrim">#000000</color>
</resources>
diff --git a/ui/src/main/res/values/dimens.xml b/ui/src/main/res/values/dimens.xml
index c6abf8eb..8ea07dfb 100644
--- a/ui/src/main/res/values/dimens.xml
+++ b/ui/src/main/res/values/dimens.xml
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="fab_margin">16dp</dimen>
- <dimen name="extra_margin">12dp</dimen>
<dimen name="bottom_sheet_item_height">56dp</dimen>
<dimen name="normal_margin">8dp</dimen>
<dimen name="bottom_sheet_top_padding">8dp</dimen>
<dimen name="bottom_sheet_icon_padding">16dp</dimen>
+ <dimen name="tunnel_list_placeholder_margin">16dp</dimen>
</resources>
diff --git a/ui/src/main/res/values/logviewer_colors.xml b/ui/src/main/res/values/logviewer_colors.xml
new file mode 100644
index 00000000..0d4b1a0d
--- /dev/null
+++ b/ui/src/main/res/values/logviewer_colors.xml
@@ -0,0 +1,6 @@
+<resources>
+ <color name="debug_tag_color">#444444</color>
+ <color name="error_tag_color">#aa0000</color>
+ <color name="info_tag_color">#00aa00</color>
+ <color name="warning_tag_color">#aaaa00</color>
+</resources>
diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml
index 2754c632..df3d3340 100644
--- a/ui/src/main/res/values/strings.xml
+++ b/ui/src/main/res/values/strings.xml
@@ -80,6 +80,8 @@
<string name="bad_config_reason_unknown_section">Unknown section</string>
<string name="bad_config_reason_value_out_of_range">Value out of range</string>
<string name="bad_extension_error">File must be .conf or .zip</string>
+ <string name="error_no_qr_found">QR code not found in image</string>
+ <string name="error_qr_checksum">QR code checksum verification failed</string>
<string name="cancel">Cancel</string>
<string name="config_delete_error">Cannot delete configuration file %s</string>
<string name="config_exists_error">Configuration for “%s” already exists</string>
@@ -106,6 +108,9 @@
<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="donate_title">♥ Donate to the WireGuard Project</string>
+ <string name="donate_summary">Every contribution helps</string>
+ <string name="donate_google_play_disappointment">Thank you for supporting the WireGuard Project!\n\nUnfortunately, due to Google\'s policies, we\'re not allowed to link to the part of the project webpage where you can make a donation. Hopefully you can figure this out!\n\nThanks again for your contribution.</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>
@@ -115,6 +120,7 @@
<string name="error_down">Error bringing down tunnel: %s</string>
<string name="error_fetching_apps">Error fetching apps list: %s</string>
<string name="error_root">Please obtain root access and try again</string>
+ <string name="error_prepare">Error preparing tunnel: %s</string>
<string name="error_up">Error bringing up tunnel: %s</string>
<string name="exclude_private_ips">Exclude private IPs</string>
<string name="generate_new_private_key">Generate new private key</string>
@@ -134,6 +140,8 @@
<string name="key_length_explanation_base64">: WireGuard base64 keys must be 44 characters (32 bytes)</string>
<string name="key_length_explanation_binary">: WireGuard keys must be 32 bytes</string>
<string name="key_length_explanation_hex">: WireGuard hex keys must be 64 characters (32 bytes)</string>
+ <string name="latest_handshake">Latest handshake</string>
+ <string name="latest_handshake_ago">%s ago</string>
<string name="listen_port">Listen port</string>
<string name="log_export_error">Unable to export log: %s</string>
<string name="log_export_subject">WireGuard Android Log File</string>
@@ -177,6 +185,10 @@
<string name="private_key">Private key</string>
<string name="public_key">Public key</string>
<string name="qr_code_hint">Tip: generate with `qrencode -t ansiutf8 &lt; tunnel.conf`.</string>
+ <string name="quick_settings_tile_add_title">Add tile to quick settings panel</string>
+ <string name="quick_settings_tile_add_summary">The shortcut tile toggles the most recent tunnel</string>
+ <string name="quick_settings_tile_add_failure">Unable to add shortcut tile: error %d</string>
+ <string name="quick_settings_tile_action">Toggle tunnel</string>
<string name="restore_on_boot_summary_off">Will not bring up enabled tunnels at boot</string>
<string name="restore_on_boot_summary_on">Will bring up enabled tunnels at boot</string>
<string name="restore_on_boot_title">Restore on boot</string>
@@ -212,7 +224,7 @@
<string name="tunnel_create_success">Successfully created tunnel “%s”</string>
<string name="tunnel_error_already_exists">Tunnel “%s” already exists</string>
<string name="tunnel_error_invalid_name">Invalid name</string>
- <string name="tunnel_list_placeholder">Add a tunnel using the blue button</string>
+ <string name="tunnel_list_placeholder">Add a tunnel using the button below</string>
<string name="tunnel_name">Tunnel Name</string>
<string name="tunnel_on_error">Unable to turn tunnel on (wgTurnOn returned %d)</string>
<string name="tunnel_dns_failure">Unable to resolve DNS hostname: “%s”</string>
@@ -221,6 +233,16 @@
<string name="type_name_go_userspace">Go userspace</string>
<string name="type_name_kernel_module">Kernel module</string>
<string name="unknown_error">Unknown error</string>
+ <string name="updater_avalable">An application update is available. Please update now.</string>
+ <string name="updater_action">Download &amp; Update</string>
+ <string name="updater_rechecking">Fetching update metadata…</string>
+ <string name="updater_download_progress">Downloading update: %1$s / %2$s (%3$.2f%%)</string>
+ <string name="updater_download_progress_nototal">Downloading update: %s</string>
+ <string name="updater_installing">Installing update…</string>
+ <string name="updater_failure">Update failure: %s. Will retry momentarily…</string>
+ <string name="updater_corrupt_title">Application Corrupt</string>
+ <string name="updater_corrupt_message">This application is corrupt. Please re-download the APK from the website linked below. After, uninstall this application, and reinstall it from the downloaded APK.</string>
+ <string name="updater_corrupt_navigate">Open Website</string>
<string name="version_summary">%1$s backend %2$s</string>
<string name="version_summary_checking">Checking %s backend version</string>
<string name="version_summary_unknown">Unknown %s version</string>
diff --git a/ui/src/main/res/values/styles.xml b/ui/src/main/res/values/styles.xml
index b90f111f..39c38247 100644
--- a/ui/src/main/res/values/styles.xml
+++ b/ui/src/main/res/values/styles.xml
@@ -1,53 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
- <style name="WireGuardTheme" parent="Theme.MaterialComponents.DayNight">
- <item name="colorPrimary">@color/primary_color</item>
- <item name="colorOnPrimary">@color/color_control_normal</item>
- <item name="colorPrimaryDark">@color/primary_color</item>
- <item name="colorPrimaryVariant">@color/primary_light_color</item>
- <item name="colorSecondary">@color/secondary_color</item>
- <item name="colorOnSecondary">@color/secondary_text_color</item>
- <item name="colorSurface">@color/primary_color</item>
- <item name="colorOnSurface">@color/color_control_normal</item>
- <item name="colorBackground">@color/primary_color</item>
- <item name="colorMultiselectActiveBackground">@color/list_multiselect_background</item>
- <item name="colorControlNormal">@color/color_control_normal</item>
- <item name="elevationOverlayColor">@color/primary_light_color</item>
- <item name="elevationOverlayEnabled">true</item>
- <item name="android:statusBarColor">@color/status_bar_color</item>
- <item name="android:windowBackground">@color/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/AppTheme.MaterialCardView</item>
+ <style name="WireGuardTheme.Toolbar" parent="Widget.Material3.Toolbar">
+ <item name="android:background">?attr/colorSurface</item>
</style>
- <style name="AppTheme" parent="WireGuardTheme" />
+ <style name="AppThemeBase" parent="WireGuardTheme">
+ <item name="materialCardViewStyle">@style/WireGuardTheme.MaterialCardView</item>
+ <item name="toolbarStyle">@style/WireGuardTheme.Toolbar</item>
+ <item name="bottomSheetDialogTheme">@style/WireGuardTheme.BottomSheetDialog</item>
+ <item name="android:statusBarColor">@null</item>
+ </style>
+
+ <!-- Various additional API-specific features in values-v*/styles.xml -->
+ <style name="AppTheme" parent="AppThemeBase" />
- <style name="AppTheme.MaterialCardView" parent="Widget.MaterialComponents.CardView">
+ <style name="WireGuardTheme.MaterialCardView" parent="Widget.Material3.CardView.Elevated">
<item name="cornerRadius">4dp</item>
- <item name="cardElevation">4dp</item>
<item name="contentPadding">8dp</item>
- <item name="cardBackgroundColor">?attr/elevationOverlayColor</item>
</style>
- <style name="AppTheme.Dialog" parent="Theme.MaterialComponents.DayNight.Dialog.Alert">
- <item name="colorPrimary">@color/secondary_color</item>
- <item name="colorSecondary">@color/secondary_color</item>
- <item name="android:windowBackground">?attr/colorBackground</item>
- </style>
-
- <style name="BottomSheetDialogTheme" parent="ThemeOverlay.MaterialComponents.BottomSheetDialog">
+ <style name="WireGuardTheme.BottomSheetDialog" parent="ThemeOverlay.Material3.BottomSheetDialog">
<item name="android:windowIsFloating">false</item>
- <item name="android:navigationBarColor">?attr/colorBackground</item>
- <item name="android:statusBarColor">@android:color/transparent</item>
- <item name="android:windowTranslucentNavigation">false</item>
- <item name="android:windowIsTranslucent">false</item>
- <item name="android:backgroundDimEnabled">true</item>
- <item name="android:backgroundDimAmount">0.5</item>
- <item name="android:windowTranslucentStatus">false</item>
- <item name="android:colorBackground">@android:color/transparent</item>
+ <item name="android:navigationBarColor">@android:color/transparent</item>
</style>
<style name="NoBackgroundTheme" parent="AppTheme">
@@ -63,19 +38,8 @@
<item name="android:windowExitAnimation">@android:anim/fade_out</item>
</style>
- <style name="DetailText" parent="TextAppearance.MaterialComponents.Body1" />
-
- <style name="SectionText" parent="TextAppearance.MaterialComponents.Subtitle1" />
-
- <style name="ThemeOverlay.AppTheme.TextInputEditText.OutlinedBox" parent="ThemeOverlay.MaterialComponents.TextInputEditText.OutlinedBox">
- <item name="colorControlActivated">@color/color_control_normal</item>
- </style>
-
- <style name="TextInputLayoutBase" parent="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
- <item name="boxStrokeColor">?attr/colorSecondary</item>
- <item name="hintTextColor">?attr/colorOnPrimary</item>
- <item name="materialThemeOverlay">
- @style/ThemeOverlay.AppTheme.TextInputEditText.OutlinedBox
- </item>
+ <style name="TvTheme" parent="AppTheme">
+ <item name="windowActionBar">false</item>
+ <item name="windowNoTitle">true</item>
</style>
</resources>
diff --git a/ui/src/main/res/values/themes.xml b/ui/src/main/res/values/themes.xml
new file mode 100644
index 00000000..0153d346
--- /dev/null
+++ b/ui/src/main/res/values/themes.xml
@@ -0,0 +1,31 @@
+<resources>
+
+ <style name="WireGuardTheme" parent="Theme.Material3.Light">
+ <item name="colorPrimary">@color/md_theme_light_primary</item>
+ <item name="colorOnPrimary">@color/md_theme_light_onPrimary</item>
+ <item name="colorPrimaryContainer">@color/md_theme_light_primaryContainer</item>
+ <item name="colorOnPrimaryContainer">@color/md_theme_light_onPrimaryContainer</item>
+ <item name="colorSecondary">@color/md_theme_light_secondary</item>
+ <item name="colorOnSecondary">@color/md_theme_light_onSecondary</item>
+ <item name="colorSecondaryContainer">@color/md_theme_light_secondaryContainer</item>
+ <item name="colorOnSecondaryContainer">@color/md_theme_light_onSecondaryContainer</item>
+ <item name="colorTertiary">@color/md_theme_light_tertiary</item>
+ <item name="colorOnTertiary">@color/md_theme_light_onTertiary</item>
+ <item name="colorTertiaryContainer">@color/md_theme_light_tertiaryContainer</item>
+ <item name="colorOnTertiaryContainer">@color/md_theme_light_onTertiaryContainer</item>
+ <item name="colorError">@color/md_theme_light_error</item>
+ <item name="colorErrorContainer">@color/md_theme_light_errorContainer</item>
+ <item name="colorOnError">@color/md_theme_light_onError</item>
+ <item name="colorOnErrorContainer">@color/md_theme_light_onErrorContainer</item>
+ <item name="android:colorBackground">@color/md_theme_light_background</item>
+ <item name="colorOnBackground">@color/md_theme_light_onBackground</item>
+ <item name="colorSurface">@color/md_theme_light_surface</item>
+ <item name="colorOnSurface">@color/md_theme_light_onSurface</item>
+ <item name="colorSurfaceVariant">@color/md_theme_light_surfaceVariant</item>
+ <item name="colorOnSurfaceVariant">@color/md_theme_light_onSurfaceVariant</item>
+ <item name="colorOutline">@color/md_theme_light_outline</item>
+ <item name="colorOnSurfaceInverse">@color/md_theme_light_inverseOnSurface</item>
+ <item name="colorSurfaceInverse">@color/md_theme_light_inverseSurface</item>
+ <item name="colorPrimaryInverse">@color/md_theme_light_inversePrimary</item>
+ </style>
+</resources>
diff --git a/ui/src/main/res/values/tv_colors.xml b/ui/src/main/res/values/tv_colors.xml
deleted file mode 100644
index f330bedc..00000000
--- a/ui/src/main/res/values/tv_colors.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?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>
diff --git a/ui/src/main/res/values/tv_styles.xml b/ui/src/main/res/values/tv_styles.xml
deleted file mode 100644
index c5477f6a..00000000
--- a/ui/src/main/res/values/tv_styles.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?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>
diff --git a/ui/src/main/res/xml/app_restrictions.xml b/ui/src/main/res/xml/app_restrictions.xml
index fefa8a80..2eaa7bc5 100644
--- a/ui/src/main/res/xml/app_restrictions.xml
+++ b/ui/src/main/res/xml/app_restrictions.xml
@@ -1,5 +1,5 @@
<!--
- ~ Copyright © 2017-2021 WireGuard LLC. All Rights Reserved.
+ ~ Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
~ SPDX-License-Identifier: Apache-2.0
-->
diff --git a/ui/src/main/res/xml/preferences.xml b/ui/src/main/res/xml/preferences.xml
index 5ab5c790..a8b66df7 100644
--- a/ui/src/main/res/xml/preferences.xml
+++ b/ui/src/main/res/xml/preferences.xml
@@ -7,23 +7,28 @@
<CheckBoxPreference
android:defaultValue="false"
android:key="restore_on_boot"
+ android:singleLineTitle="false"
android:summaryOff="@string/restore_on_boot_summary_off"
android:summaryOn="@string/restore_on_boot_summary_on"
android:title="@string/restore_on_boot_title" />
<com.wireguard.android.preference.ZipExporterPreference android:key="zip_exporter" />
+ <com.wireguard.android.preference.QuickTilePreference android:key="quick_tile" />
<Preference
android:key="log_viewer"
+ android:singleLineTitle="false"
android:summary="@string/log_viewer_pref_summary"
android:title="@string/log_viewer_pref_title" />
<CheckBoxPreference
android:defaultValue="false"
android:key="dark_theme"
+ android:singleLineTitle="false"
android:summaryOff="@string/dark_theme_summary_off"
android:summaryOn="@string/dark_theme_summary_on"
android:title="@string/dark_theme_title" />
<CheckBoxPreference
android:defaultValue="false"
android:key="multiple_tunnels"
+ android:singleLineTitle="false"
android:summaryOff="@string/multiple_tunnels_summary_off"
android:summaryOn="@string/multiple_tunnels_summary_on"
android:title="@string/multiple_tunnels_title" />
@@ -32,7 +37,9 @@
<CheckBoxPreference
android:defaultValue="false"
android:key="allow_remote_control_intents"
+ android:singleLineTitle="false"
android:summaryOff="@string/allow_remote_control_intents_summary_off"
android:summaryOn="@string/allow_remote_control_intents_summary_on"
android:title="@string/allow_remote_control_intents_title" />
+ <com.wireguard.android.preference.DonatePreference android:singleLineTitle="false" />
</androidx.preference.PreferenceScreen>